1. 简介
SpringSecurity 是一个强大且高度可定制的认证和授权框架
认证:身份验证,判断访问者是否为系统用户,某些操作只有系统用户才能访问
授权:访问控制,判断系统用户是否具备访问某个功能的权限
依赖导入
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 原理
Spring Security 的核心实现是一条过滤器链!Spring Security的所有功能、特性都建立在这条过滤器链上!
2.1 通过Debug查看过滤器链:
2.2 核心过滤器:
UsernamePasswordAuthenticationFilter: 认证核心。
ExceptionTranslationFilter: 处理认证和授权异常。
FilterSecurityInterceptor: 授权核心。
3. 认证
3.1 核心认证流程
3.2 核心接口
Authentication:身份认证令牌,封装了用户相关信息
AuthenticationManager:认证管理对象,用于对 Authentication 对象进行认证
UserDetailsService:用户信息查询服务,定义了 loadUserByUsername 方法查询用户信息
UserDetails:封装了用户的详细信息,UserDetailsService 查询的用户信息都会封装到这个对象中
3.3 问题一
3.3.1 问题介绍
UserDetailsService 在查询用户信息时,是从内存中查询的,而实际上,用户信息是存储在数据库中的,所以我们需要修改为从数据库中查询用户信息
3.3.2 解决方案
自定义 UserDetailsService 接口的实现类,从数据库中查询用户相关信息。
3.3.3 执行步骤
1. 自定义 UserDetailsService 接口的实现类
2. 创建 UserDetails 的实现类,用于分装 User 对象
3. 在输入正确的用户名和密码的情况下,登录失败了,控制台报如下错误
问题简介
原因:
- 如果未注入 PasswordEncoder 接口的加密实现,则数据库中的密码格式必须为:{id}encodedPassword
解释:
id:表示加密方式,可以是 noop(不加密)、bcrypt、MD5、SHA-1、SHA-256等
encodedPassword:表示使用id指定的方式进行加密后的结果
SpringSecurity 会根据 id 指定的加密规则对密码进行验证,如果不指定加密方式,就会出现上面的错误!
修改数据库密码格式是可以实现的,但是不推荐,我们使用注入 PasswordEncoder 的方式!!!
解决方案
将 PasswordEncoder 的具体实现注入到Spring容器中,注册时密码的加密也要使用 PasswordEncoder
4. 使用自定的加密方式
使用上面现成的 BCryptPasswordEncoder 加密方式已经很安全了,但是如果有特定的需求也可以自己定义
- 自定义加密类
public class MyPasswordEncoder implements PasswordEncoder {
public String encode(CharSequence rawPassword) {
//这里填写自己自定义加密方式
if (StringUtils.hasText(rawPassword))
return DigestUtils.md5DigestAsHex(rawPassword.toString().getBytes(StandardCharsets.UTF_8));
return null;
}
//自定义密码匹配规则(就是讲传来的密码加密后比对即可)
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encode(rawPassword).equals(encodedPassword);
}
}
- 在配置类中,将自定义的加密类注入容器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
//return NoOpPasswordEncoder.getInstance(); // 不加密处理,不推荐!
//return new BCryptPasswordEncoder(); // 推荐!
return new MyPasswordEncoder(); // 自定义加密处理
}
}
3.4 问题二
3.4.1 问题描述
之所有使用 SpringScurity 登录一次后下一次不需要在登录了,他主要是基于 Session 实现的这个功能的,但是在前后端分离的情况下,前后端项目分别部署在不同的服务器上,导致 session 无法正常使用。
需要使用 **token(令牌)**认证:在用户登录成功之后,服务器需要为客户端创建一个 token,并将这个 token 作为 key 保存用户信息到 redis,响应时将这个 token 连同其它信息一并返回给客户端,客户端需要将这个 token 保存来,之后的请求都要将这个 token 发送给服务器,服务器通过这个 token 从 redis 中获取用户信息。
3.4.2 解决方案
自定义登录接口,将用户名和密码封装成 Authentication 对象,调用 AuthenticationManager的 authenticate 方法进行认证,认证通过后会返回 Authentication用户,生成 token 并将返回的Authentication对象存入redis,将token返回给客户端
自定义认证过滤器,根据请求中的token从redis中获取身份认证令牌Authentication对象,并调用SecurityContextHolder.getContext().setAuthentication方法将Authentication对象保存下来
将自定义认证过滤器注入到Spring容器中,并让这个过滤器在UsernamePasswordAuthenticationFilter之前执行
3.4.3 执行步骤
1. 修改 SpringSecurity 的配置类中的配置
- 将认证管理对象 AuthenticationManager 注入到Spring容器中,我们自定义登录接口时要用到
- 对请求进行认证配置:访问登录接口时,不需要认证
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
// return NoOpPasswordEncoder.getInstance(); // 不加密处理,不推荐!
return new BCryptPasswordEncoder(); // 强烈推荐!
}
//向Spring 注入 AuthenticationManager 对象
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//这里可以指定你自定义的登录路径,无需经过认证
@Override
protected void configure(HttpSecurity http) throws Exception {
// 前后端分离固定配置
separationModel(http);
// 对请求进行认证配置
http.authorizeRequests()
// (匿名访问)无需认证也可以访问 anonymous:匿名
.antMatchers("/user/login").anonymous()
// 其它请求认证通过后可以访问
.anyRequest().authenticated();
}
//前后端分离固定配置
private void separationModel(HttpSecurity http) throws Exception {
// 禁用csrf
http.csrf().disable();
// 允许跨域
http.cors();
// 不使用session
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
2. 自定义登录接口
3.4.4. 编写认证过滤器
主要实现判断用户是否已经登录,如果已经登录,则无需再登录
3.4.5 设置自定义过滤器放过滤器链的位置
4. 授权
4.1 简介
实际场景应用
一个系统中有很多功能,而某些功能需要一定的权限才能使用,例如商城中的商品,普通用户只能查看和购买,但不具备删除、修改的权限,这时就需要权限控制,也就是所谓的授权。
但是要注意的是这里并不是简单的将前端页面跟该功能相关的按钮抹去(此时可以通过路径也可以执行该功能),还要对访问接口进行权限限制。
4.2 快速使用
这里用户的权限不通过数据库的方式去获取,而是通过写死的方式,这样便于演示。
1. 首先需要在 SpringSecurity 的配置类上添加下面注解,来开启权限控制的注解开发
@EnableGlobalMethodSecurity(prePostEnabled = true)
2. 然后在 Controller 的方法上使用 @PreAuthorize 注解进行权限控制
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
登录成功之后,“/test” 接口可以正常访问,而"/delete"接口访问失败,因为不具备”delete“权限
用户的权限信息,需要在认证成功之后查询,连同用户信息一起封装到UserDetails中
3. 为对象添加权限(写死的方式)
- 修改 UserDetail 字段
- 为 Userdetail 的权限列表赋值
4.3 通过数据库获取权限信息
4.3.1 RBAC模型(数据库设计模型)
RBAC(Role-Based Access Control)权限模型:基于角色的权限控制,是一种比较通用的权限模型,最常用!通俗一点就是在用户与权限之间加入一个角色,以角色为桥梁获取用户所具有的权限。
4.3.2 编写 Mapper
4.3.3 业务实现
4.4 其他权限控制注解
4.5 自定义权限校验方法
用的不多,了解即可
定义用于校验权限的类
@Component("ex") public class MyExpressionRoot { public boolean hasAuthority(String authority){ //获取身份令牌 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); //获取权限 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); //循环判断 for (GrantedAuthority grantedAuthority : authorities) { String role = grantedAuthority.getAuthority(); if (role.equals(authority)) { return true; } } return false; } }
在@PreAuthorize注解中使用自定义校验方法
@DeleteMapping("delete") //ex是校验实力在Spring容器中的唯一标识 @PreAuthorize("@ex.hasAuthority('delete')") public String delete() { return "删除成功!"; }