1、它基于Spring,提供了一整套完整的web应用安全性的解决方案
2、主要包括两个部分:认证和授权
认证:通俗点说就是系统认为用户是否能登录
授权:通俗点就是系统判断用户是否有权限去做某些事情
与Spring框架类似,简化权限开发的过程,更加高效、快捷并且安全的实现项目的权限管理。本质上就是一个权限管理框架。
SpringSecurity的特点:
1、新建一个SpringBoot项目
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
3、编写conroller测试
@RestController
@RequestMapping("/test")
public class HellpController {
@GetMapping(value ="/hello")
public String hello(){
return "hello security";
}
}
4、浏览器控制端访问:localhost:8080/test/hello
用户名默认为:user
密码在控制台已经自动生成:
1、本质上就是一个过滤器链
2、提供给用户自定义实现功能的接口有两个,一个是UserDetailsService,另外一个是PasswordEncoder
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。 如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。PasswordEncoder接口主要是用于加密。
SpringSecurity实现web的权限管理,有三种方式,
spring.security.user.name=beim
spring.security.user.password=123
2、通过配置类实现
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123"); // 要设置的密码
/*
withUser() : 用户名
password() : 密码
roles() : 角色
*/
auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
}
@Bean
protected PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
先创建一个配置类
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
/**
*
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean
protected PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
自定义业务接口实现UserDetailsService接口
@Service("userDetailsService")
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// auths:权限集合
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("marry",new BCryptPasswordEncoder().encode("123"),auths);
}
}
使用mybatisplus完成数据库操作(运用mybatisX插件),基于上文代码进行的增加以及修改
1、第一步:导入相关的依赖
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.0.5version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
2、第二步:创建数据库和数据库表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`uid` int(11) NOT NULL AUTO_INCREMENT,
`uname` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`upass` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`uid`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;
INSERT INTO `users` VALUES (1, 'lisi', '123456');
INSERT INTO `users` VALUES (2, 'zhangsan', '123456');
SET FOREIGN_KEY_CHECKS = 1;
3、第三步:创建user表对应的实体类
@TableName(value ="users")
@Data
public class Users implements Serializable {
@TableId(type = IdType.AUTO)
private Integer uid;
private String uname;
private String upass;
}
4、第四步:创建接口继承BaseMapper
@Repository
public interface UsersMapper extends BaseMapper<Users> {
Users selectAllByUname(@Param("uname") String uname);
}
5、第五步:在UsersServiceImpl调用mapper中的方法
@Service("userDetailsService")
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users>
implements UsersService, UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = usersMapper.selectAllByUname(username);
if (users == null){
throw new UsernameNotFoundException("用户未找到!");
}else {
// auths:权限集合
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(users.getUname(),new BCryptPasswordEncoder().encode(users.getUpass()),auths);
}
}
}
6、在启动类上添加mapper扫描器
@SpringBootApplication
@MapperScan("com.beim.springsecuritydemo.mapper")
public class SpringsecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringsecuritydemoApplication.class, args);
}
}
7、配置properties数据库信息
# 配置数据库
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mimissm?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=333
mybatis-plus.mapper-locations=classpath:mapper/**/*.xml
1、在配置类中实现相关的配置,(基于上文代码进行的修改)
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义登录页面,默认是在static目录下
http.formLogin().loginPage("/login.html")
/*
登录页访问路径,即action属性的值,当页面提交后给此路径后,框架会对其进去拦截,并做登录验证
如果验证成功,则进入到后台的登录成功controller路径,如果验证失败,则依旧跳转回登录页面
*/
.loginProcessingUrl("/user/login")
// 登录成功之后跳转路径,后台controller对应处理
.defaultSuccessUrl("/test/index").permitAll()
.and().authorizeRequests()
// 设置哪些路径可以直接访问,不需要认证
.antMatchers("/","/test/hello","/user/login").permitAll()
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf防护
}
2、controller对应方法
@RestController
@RequestMapping("/test")
public class HellpController {
@GetMapping(value ="/hello")
public String hello(){
return "hello security";
}
@GetMapping(value ="/index")
public String login(){
return "hello security1111";
}
}
3、编写静态页面(默认是在static目录下创建login.html)
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面设置title>
head>
<body>
<form method="post" action="user/login">
用户名:<input type="text" name="username">
<br>
密码:<input type="password" name="password">
<br>
<input type="submit" value="提交表单">
form>
body>
html>
需要注意的是,表单中的name属性必须为username和password,因为这是SpringSecurity框架内部定义的,否则会识别不到。
如果当前的主体具有**某一个(注意是只有一个)**指定的访问权限,则返回true否则返回false
1、在配置类设置当前访问地址有哪些权限,配置类中重写configure方法
@Override
protected void configure(HttpSecurity http) throws Exception {
// 访问该页面需要有admins权限
http.authorizeRequests().antMatchers("/test/index")
.hasAuthority("admins");
}
2、在UserDetailService中设置返回对象的权限
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = usersMapper.selectAllByUname(username);
if (users == null){
throw new UsernameNotFoundException("用户未找到!");
}else {
// auths:权限集合, 设置权限集合
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
return new User(users.getUname(),new BCryptPasswordEncoder().encode(users.getUpass()),auths); // 给返回的User对象添加权限集合
}
}
如果页面的访问权限与用户认证的权限不一致,则会出现拒绝访问页面的错误
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C09n4Zhx-1646824781573)(E:\Pictures\Typora-pictures\1646720710069.png)]
如果当前页面的访问权限不止一个,则用此方法来设置多个访问权限。注意:这个是指只要满足两个权限中的某一个就可以进行访问,而不是两个都需要满足才能进行访问
// 设置具有admins或manager两个访问权限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/test/index")
.hasAnyAuthority("admins","manager");
}
用户的访问权限为manager:
@Service("userDetailsService")
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users>
implements UsersService, UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = usersMapper.selectAllByUname(username);
if (users == null){
throw new UsernameNotFoundException("用户未找到!");
}else {
// 访问权限为manager
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("manager");
return new User(users.getUname(),new BCryptPasswordEncoder().encode(users.getUpass()),auths);
}
}
}
如果当前访问主体具有指定的角色,则返回true
// 指定当前路径下的访问角色为admins
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/test/index")
.hasRole("admins");
}
// 权限名称前需要添加ROLE_
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admins");
需要注意的是:此方法在添加权限的时候要加上“ROLE_”前缀
设置多个角色,用户只要具备其中任何一个,都可以进行访问。
// 指定多个角色
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/test/index")
.hasAnyRole("role","role1")
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_role1");
需要注意的是:此方法在添加权限的时候要加上“ROLE_”前缀
第一步,先在static目录下建立一个页面表示自定义的403页面
第二步:在配置类中配置页面路径即可
// 配置自定义没有权限访问时跳转的页面
http.exceptionHandling().accessDeniedPage("/error.html");
判断是否具有角色,可以访问方法,另外需要注意两点:
1、在启动类中添加启动注解
// 开启全局安全验证注解
@EnableGlobalMethodSecurity(securedEnabled=true)
2、在方法上使用此注解
@GetMapping(value ="/insert")
// 表明用户需要具备admin或manager其中一个权限时才能访问
@Secured({"ROLE_admin","ROLE_manager"})
public String insert(){
return "hello insert";
}
3、在UserDetailService设置用户角色
// auths:权限集合
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_manager");
表示进入方法前进行权限验证
1、在启动类或者配置类上,开启此注解
@SpringBootApplication
@MapperScan("com.beim.springsecuritydemo.mapper")
// 开启全局安全验证注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
2、在方法上面添加注解
@GetMapping(value ="/insert")
@PreAuthorize("hasAnyAuthority('admins')")
public String insert(){
return "hello insert";
}
3、在UserDetailService设置用户角色
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
在方法执行之后做一个权限验证,用得比较少
1、在启动类或者是配置类上,开启此注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
2、在方法上添加注解
@GetMapping(value ="/insert")
@PostAuthorize("hasAnyAuthority('admins')")
public String insert(){
System.out.println("insert......");
return "hello insert";
}
}
3、在UserDetailService设置用户角色
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
权限验证之后对数据进行过滤
@GetMapping(value ="/update")
@PreAuthorize("hasAnyAuthority('admins')")
@PostFilter("filterObject.uname == 'admin1'")
public List<Users> update(){
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(12,"admin1","123",0,1,0));
list.add(new Users(13,"admin2","456",1,0,1));
return list;
}
最后返回给前端的数据是admin1的,需要注意的是PostFilter中的参数,filterObject为一个默认的内置对象,必须是这个名称
对传入方法的数据进行过滤
@GetMapping(value ="/update")
@PreAuthorize("hasAnyAuthority('admins')")
@PreFilter("filterObject.uname == 'admin2'")
public List<Users> update(List<Users> list){
list.forEach(t-> {
System.out.println(t.getUid()+"\t"+t.getUname());
});
return list;
}
在配置类添加退出的配置
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
/**
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置自定义没有权限访问时跳转的页面
http.exceptionHandling().accessDeniedPage("/error.html");
// 设置退出的路径
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello");
http.formLogin().loginPage("/login.html") // 自定义登录页面,默认是在static目录下
.loginProcessingUrl("/user/login") // 登录访问路径
.defaultSuccessUrl("/success.html").permitAll() // 登录成功之后跳转路径,后台controller对应处理
.and().authorizeRequests()
.antMatchers("/", "/test/hello", "/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf防护
}
@Bean
protected PasswordEncoder password() {
return new BCryptPasswordEncoder();
}
}
测试
1、修改配置类,登录成功之后跳转到成功的页面
2、在登录成功的页面中添加超链接,链接到设置退出的路径
3、登录成功之后再去访问其他的controller中的方法是不能进行访问的。
SpringSecurity实现自动登录是通过cookie实现的,其基本原理流程可参考下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJUKjUtw-1646824781574)(E:\Pictures\Typora-pictures\1646729168258.png)]
1、创建数据表
drop table if exists persistent_logins;
CREATE TABLE persistent_logins (
username varchar(64) NOT NULL,
series varchar(64) NOT NULL,
token varchar(64) NOT NULL,
last_used timestamp NOT NULL
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (series)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2、配置类,注入数据源,配置数据库操作对象
JdbcTokenRepositoryImpl实现类里封装了现成的SQL建表语句,因此我们不需要自己建立数据表,如果已经自己建立好了数据表,则需要将setCreateTableOnStartup的参数设置为false。
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
/*
自动创建表,第一次执行会创建,以后要执行就要删除掉!
需要注意的是,如果数据库中已经创建了persistent_logins表,则需要设置为false,
否则会报异常这里因为已经创建了数据表,所以设置为false
*/
jdbcTokenRepository.setCreateTableOnStartup(false);
return jdbcTokenRepository;
}
3、配置类,添加记住我功能
http.rememberMe()
// 传入PersistentTokenRepository对象
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) // 设置cookie的有效时长,单位是秒
.userDetailsService(userDetailsService); // 将用户的信息传入
4、表单中添加复选框
<input type="checkbox" name="remember-me"> 自动登录
这里唯一需要注意的就是:name字段的值必须为remember-me,否则框架会识别不到。
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已 登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买 商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的 浏览器,却不能保证请求本身是用户自愿发出的。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用 程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
使用步骤:
1、在提交的表单中添加一个隐藏域
2、关闭CSRF防御
// http.csrf().disable();
需要注意的是,CSRF对于:GET、HEAD、TRACE、OPTIONS请求是直接放行的,不会进行安全验证
3、防护的基本原理
第一步:会将隐藏域中生成的csrfToken 保存到 HttpSession 或者 Cookie 中。
第二步:当请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当 前请求是否合法。主要通过 CsrfFilter 过滤器来完成。