Spring Security 重点解析
文章目录
- Spring Security 重点解析
- 1. 简介
- 2. 依赖
- 3. 登录认证
- 3.1 登录校验流程
- 3.2 Spring Security 默认登录的原理
- 3.2.1 Spring Security 完整流程
- 3.2.2 登录逻辑探究
- 3.3 自定义改动
- 3.3.1 自定义用户密码校验
- 3.3.2 自定义 UserDetails 获取方式 F1
- 3.3.3 自定义加密算法 F2
- 3.4 自定义登录认证
- 3.4.1 自定义登录接口
- 3.4.2 自定义过滤器
- 3.4.3 Security 配置
- 3.4.4 退出登录
- 4. 验权处理
- 4.1 “权限”的存在形式
- 4.2 开启权限管理
- 4.3 设置权限要求
- 4.3.1 通过注解
- 4.3.2 通过配置
- 4.3.3 自定义设置权限方法
- 4.4 RBAC 权限模型
- 4.5 封装权限信息
- 4.6 查询权限用于构造 UserDetails
- 4.7 在 SecurityContextHolder 记录时设置权限
- 5. 自定义失败处理
- 5.1 实现处理器类
- 5.2 配置
- 6. 跨域问题
- 6.1 配置类
- 6.2 Security 配置
- 7. CSRF 防御
- 8. 自定义成功处理
- 8.1 处理器
- 8.2 Security 配置
- 8.3 自定义登录模式的成功处理器
1. 简介
Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
- 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
- 一般Web应用的需要进行认证和授权。
认证:
- 验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:
- 经过认证后判断当前用户是否有权限进行某个操作
而**认证和授权**也是SpringSecurity作为安全框架的核心功能。
接下来以用户名密码登录这个案例来讲解!
- 利用 JWT 机制,文章:JWT 重点讲解-CSDN博客
- Redis 缓存,文章:Redis 工具类 与 Redis 布隆过滤器-CSDN博客
Spring Security 推荐学习视频:SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-挑战黑马&尚硅谷_哔哩哔哩_bilibili
2. 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.0.4</version>
</dependency>
引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个 SpringSecurity的默认登陆页面
-
默认用户名是user,密码会输出在控制台。
-
必须登陆之后才能对接口进行访问。
退出登录:
等一下你就知道原理了~
3. 登录认证
3.1 登录校验流程
3.2 Spring Security 默认登录的原理
3.2.1 Spring Security 完整流程
SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
- 经过所有过滤器才能访问 API,过滤器也能处理响应啥的~
图中只展示了 核心过滤器,其它的非核心过滤器并没有在图中展示。
- UsernamePasswordAuthenticationFilter
- 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责
- ExceptionTranslationFilter
- 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException ,有异常抛出就肯定过不了这个过滤器了
- FilterSecurityInterceptor
- 负责权限校验的过滤器。
3.2.2 登录逻辑探究
UsernamePasswordAuthenticationFilter 就是默认登录方式,默认是 Cookie-Session 机制
概念速查:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装
成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
-
访问
/test/hello
第一次没有会话,UsernamePasswordAuthenticationFilter 尝试获取,获取不到,强制让用户登录(重定向到[GET] /login
页面,本次请求不算数) -
访问
[POST]/login
携带用户名密码,验证成功后,将 “用户相关信息(UserDetails)”构造出一个核心对象(Authentication),放入这次请求的 Security 框架上下文对象中,前往之后的拦截器 -
其他过滤器都没问题了(登录没啥权限要求),请求成功到达
[POST]/login
,进行对应逻辑,返回响应~ -
访问
/test/hello
携带 Cookie,UsernamePasswordAuthenticationFilter 获取到会话记录 UserDetails,”构造出一个核心对象(Authentication),放入这次请求的 Security 框架上下文对象中,前往之后的拦截器会话记录:
-
其他过滤器都没问题了(登录没啥权限要求),请求成功到达
/test/hello
,进行对应逻辑,返回响应~
3.3 自定义改动
- 自定义用户名密码校验方式
- 自定义拦截器,可调节在哪个拦截器之前或之后,或者删除哪些拦截器
3.3.1 自定义用户密码校验
- 自定义的 UserDetails 获取方式
F1
- 自定义密码的加密算法
F2
设[POST]/login
携带的用户名为 U
,密码为 P
,UserDetails 为 D
D = F1(U);
- 判断
F2(P)
与D.getPassword()
,是否相同
3.3.2 自定义 UserDetails 获取方式 F1
我知道我没有给出一些数据库与MP相关的代码,不过思路看得懂即可!
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser user;
private List<String> permissions;
@JSONField(serialize = false)//代表,由不通过fastjson序列化,用系统redis其他的方式存入redis
private List<GrantedAuthority> authorities;//com.alibaba.fastjson.JSONException: autoType is not support.
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(Objects.isNull(this.authorities)) {
// 将 permissions 的权限信息封装成 SimpleGrantedAuthority 象,并且也是集合
this.authorities = this.permissions.stream()
.parallel()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
return this.authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public LoginUser(SysUser user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
}
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private SysMenuService menuService;
// 查询用户信息
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 查询用户信息
SysUser user = Db.lambdaQuery(SysUser.class)
.eq(SysUser::getUserName, s)
.one();
if(Objects.isNull(user)) {
log.warn("用户名或者密码错误");
throw new RuntimeException("用户名或者密码错误");
}
// todo 查询对应的权限信息
List<String> permissions = menuService.selectMenuById(user.getId());
// 把数据封装成UserDetails返回
return new LoginUser(user, permissions);
}
}
认证的时候,就会用我们实现的加载数据的方法
3.3.3 自定义加密算法 F2
//BCryptPasswordEncoder的Bean对象加入到容器里
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
在认证的时候,就是用我们的这个 PasswordEncoder 去加密(默认不加密,不用加密算法)
用同种加密算法去注册两个账号:
@Resource private PasswordEncoder encoder; @Test public void createUser() { List<SysUser> list = new ArrayList<SysUser>() {{ add(new SysUser(){{ setId(1L); setNickName("马1号"); setUserName("mara1"); setPassword(encoder.encode("123456")); }}); add(new SysUser(){{ setId(2L); setNickName("马2号"); setUserName("mara2"); setPassword(encoder.encode("123456")); }}); }}; Db.saveBatch(list); }
3.4 自定义登录认证
对于以上内容,依赖默认的登录页面,获取是用户名密码的机制也比较单一,我们往往需要一些其他的登录方式~
接下来,我们来搞一下自定义登录(还是以用户名密码,但是这次是我们自己写的业务逻辑)
3.4.1 自定义登录接口
@Data
public class FormUserDTO {
private String userName;
private String password;
}
@RestController
//@RequestMapping("/user")
public class LoginController {
@Resource
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody FormUserDTO user) {
//登录
return loginService.login(user);
}
}
public interface LoginService {
ResponseResult login(FormUserDTO user);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
实现:
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisCache redisCache;
@Override
public ResponseResult<Map<String, String>> login(FormUserDTO user) {
// 获取AuthenticationManager authenticate认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证不通过,提示
if(Objects.isNull(authenticate)) {
log.warn("登录失败");
throw new RuntimeException("登录失败");
}
//如果认证通过了,用userid生成一个jwt
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();//认证的时候已经调用了getAuthorities了
String userid = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userid);
//把完整的用户信息存入redis,userid作为key
redisCache.setCacheObject("login:" + userid, loginUser);
// redisCache.getCacheObject("login:" + userid);//是完完整整的存进去的
// 符合jwt
return new ResponseResult(200, "登录成功", new HashMap<String, String>(){{
this.put("token", jwt);
}});
}
这就相当于替 UsernamePasswordAuthenticationFilter 调用了认证方法获得 authenticate:
自然就是一轮认证:
认证失败也是在这个过程中抛出异常的
3.4.2 自定义过滤器
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {// 保证请求只会经过这个过滤器一次
@Resource
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = httpServletRequest.getHeader("token");
if(!StringUtils.hasText(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
return;
}
// 解析token
String userid = null;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
log.error("token非法");
filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
return;
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if(Objects.isNull(loginUser)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
return;
}
// 存入SecurityContextHolder
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());//代表已认证
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
}
}
认证成功要记录!
3.4.3 Security 配置
yaml 配置文件去配置不灵活,不鲜明,在这里不演示
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限控制,不开启这个,注解的权限控制不能生效
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Resource
private AuthenticationEntryPoint authenticationEntryPoint;
@Resource
private AccessDeniedHandler accessDeniedHandler;
//BCryptPasswordEncoder的Bean对象加入到容器里
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// super.configure(http); 默认是cookie session
//内部指定表单,用户输入表单就去验证,也就是UsernamePasswordAuthenticationFilter的认证方式
// 既然注释掉了,则代表这个过滤器(可能不止这一个)失效了~(不被调用 -> 过滤器不工作),并且还注释掉“控制”相关的配置
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/login") /*匿名访问*/.anonymous()
// 添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//添加过滤器在某个过滤器之前
}
}
不必调用父类的configure方法,UsernamePasswordAuthenticationFilter就相当于没用了
UsernamePasswordAuthenticationFilter 无法进行重定向
不请求`[POST]/login` ,UsernamePasswordAuthenticationFilter就不会调用认证的方法,SecurityContextHolder就不会有对应的那个Authentication对象的记录
最终需要认证的接口就访问不了!
- “哪些可以匿名访问不用认证,哪些需要进行认证”,只不过是一些“认证/权限等等的控制”罢了,没有实际的认证机制
- 每个请求都会经过过滤器链,只不过一些没通过检查也无所谓!
这样我们就需要自己写个拦截器,去规定怎么样才算认证成功
-
这样,必须提前在/user/login (接口内用了认证方法,这一步代表体检做认证),将userid存到redis,生成token返回
-
请求必须携带token,由 jwtAuthenticationTokenFilter 进行认证是否有token,redis是否有记录
-
有则代表提前做了认证,SecurityContextHolder 设置那个Authentication对象,放行即可,就不需要进行UsernamePasswordAuthenticationFilter的那个默认login页面去认证了
把默认的关了的话,UsernamePasswordAuthenticationFilter 就相当于没用了,UsernamePasswordAuthenticationFilter 并不会让认证结果有任何变化
没有 jwtAuthenticationTokenFilter 的话,那么 SecurityContextHolder 就没有记录,那么请求必然不通过认证/验权
jwtAuthenticationTokenFilter 没有让 SecurityContextHolder 有记录,那么请求也必然不通过认证/验权
3.4.4 退出登录
@GetMapping("/user/logout")
public ResponseResult logout() {
return loginService.logout();
}
public interface LoginService {
ResponseResult login(FormUserDTO user);
ResponseResult logout();
}
实现:
@Override
public ResponseResult logout() {
//获取用户id
UsernamePasswordAuthenticationToken authentication
= (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userid = loginUser.getUser().getId().toString();
// 删除redis中的值
String redisKey = "login:" + userid;
redisCache.deleteObject(redisKey);
return new ResponseResult(200, "注销成功");
}
补充,用户名为 null,在 loadUserByUsername 方法的 userName 为"NONE_PROVIDED"
- 如果真的有用户名是这个,密码还没错,还真的能登录成功!
这是借用了 UsernamePasswordAuthenticationFilter 的认证方法,我们当然也可以自己写一个认证方法,构造UserDetails 啊
- 也可以研究其他的 SpringSecurity 提供的认证方法~
4. 验权处理
4.1 “权限”的存在形式
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
而我们这里的权限就是一个字符串
- 例如"limit",获取用户的权限字符串列表,有"limit"就代表有权限,反之没有
而权限信息要交给 UserDetails,从而在认证成功后在 SecurityContextHolder 中记录,在FilterSecurityInterceptor 过滤器将其拿出来,对比 api 所需的权限,决定是否可以访问
4.2 开启权限管理
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限控制,不开启这个,注解的权限控制不能生效
4.3 设置权限要求
4.3.1 通过注解
Spring表达式语言(Spring Expression Language,SpEL)是 Spring Framework 的核心技术之一,其支持在运行时查询和操作对象图。
SpEL语法类似于 Unified Expression Language,但提供了更加丰富的功能,最特别的是方法调用与字符串模板功能。
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/test")
public class HelloController {
@GetMapping("/hello")
@PreAuthorize("hasAuthority('system:test:list')")
public String hello() {
return "<h1>hello!</h1>";
}
@GetMapping("/hi")
@PreAuthorize("hasAuthority('system:dept:list')")
// @PreAuthorize("hasRole('system:dept:list')")//拼接ROLE_前缀 后去验证
public String hi() {
return "<h1>hi!</h1>";
}
@GetMapping("/haha")
@PreAuthorize("hasAnyAuthority('system:dept:list', 'haha')")
// @PreAuthorize("hasAnyRole('system:dept:list', 'haha')")//拼接ROLE_前缀 后去验证
public String haha() {
return "<h1>haha!</h1>";
}
}
按住 ctrl 点击查看对应的方法~
4.3.2 通过配置
4.3.3 自定义设置权限方法
@Component("mex")
public class MacakuExpressionRoot {
public boolean hasAuthority(String authority) {
// 获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//判断用户权限集合中是否存在authority
return authentication.getAuthorities()
.stream()
.map(x -> x.getAuthority())
.distinct()
.collect(Collectors.toList())
.contains(authority);
}
}
4.4 RBAC 权限模型
Role-Based Access Control
一个用户可以有多个角色,一个角色可以有多个权限
- 某个角色可以是不同用户,某个权限也可以由多个角色拥有
通过 id 查询权限的 mapper xml:
<select id="selectMenuById" resultMap="BaseResultMap" parameterType="java.lang.Long"> SELECT DISTINCT m.`perms` perms FROM sys_user u LEFT JOIN `sys_user_role` ur ON u.`id` = ur.`user_id` LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` WHERE u.`id` = #{id, jdbcType=BIGINT} AND r.`status` = 0 AND m.`status` = 0 </select>
4.5 封装权限信息
4.6 查询权限用于构造 UserDetails
tips:这里还可以查询角色,因为角色也可以看成是一种特殊的权限
记得加上“ROLE_”前缀
4.7 在 SecurityContextHolder 记录时设置权限
5. 自定义失败处理
不可以直接用异常处理器去解决问题,因为这个时候抛出异常并不会被我们的全局异常处理器捕获!
(业务逻辑抛的异常会导致 500)
- 认证失败 抛出的 AuthenticationException 异常 ,由 AuthenticationEntryPoint 处理
- 验权失败 抛出的 AccessDeniedException ,由 AccessDeniedHandler 处理
由于不能直接进行异常处理,所以自定义异常处理还是很有必要的~
- 不过在处理的时候不要抛异常,因为这样会直接导致请求响应是 500(因为不会被我们的异常处理器捕获)
- 可以重定向之类的~
提几个重定向的坑:
HttpServletResponse 对象的 sendRedirect 方法,是相对路径下的重定向,不要填绝对路径否则会导致循环重定向
- (例如 sendRedirect(“127.0.0.1:8081/user/login”))会导致重定向的请求是 “127.0.0.1:8081/127.0.0.1:8081/user/login”)
可以写成这样:
public final static String LOCATION_HEADER = "Location";
- 通过 HttpServletRequest 对象获取的 URL 是 http 的(即使请求是 https ),如果要重定向到 https 要自己去解决!
- 可以在配置文件中获取域名与协议啊~
5.1 实现处理器类
验权失败:
@Component
@Slf4j
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.warn(e.getMessage());
ResponseResult responseResult = new ResponseResult(HttpStatus.FORBIDDEN.value(), "用户授权不足");
//处理异常
WebUtils.renderString(httpServletResponse, JSON.toJSONString(responseResult));
}
}
认证失败:
@Component
@Slf4j
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {//
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
log.warn(e.getMessage());
ResponseResult responseResult = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败请查询登录");
//处理异常
WebUtils.renderString(httpServletResponse, JSON.toJSONString(responseResult));
}
}
5.2 配置
6. 跨域问题
浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。
所以我们就要处理一下,让前端能进行跨域请求。
6.1 配置类
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
6.2 Security 配置
当然,你也可以写个过滤器,让响应带上一些东西来允许跨域~
@Component public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setHeader("Access-Control-Allow-Origin", "*"); // 可以设置允许访问的域,也可以是具体的域名 httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); httpResponse.setHeader("Access-Control-Max-Age", "3600"); httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); httpResponse.setHeader("Access-Control-Allow-Credentials", "true"); chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException { // 可以在这里进行一些初始化操作 } @Override public void destroy() { // 可以在这里进行一些清理操作 } }
7. CSRF 防御
CSRF(Cross-Site Request Forgery)的相关介绍视频:CSRF 攻击和防御 - Web 安全常识_哔哩哔哩_bilibili
可以取消对 CSRF 的关闭
Spring Security默认情况下会启用CSRF防护。
Spring Security 对 CSRF 的防御手段是自定义请求头
通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现 CSRF 攻击依靠的是 Cookie 中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而 token 并不是存储中 Cookie 中,并且需要前端代码去把 token 设置到请求头中才可以,所以 CSRF 攻击也就不用担心了。
8. 自定义成功处理
成功处理器只能在默认登录方式中加入,我也不知道为什么🤣,硬性限制~
8.1 处理器
登录成功:
@Component
@Slf4j
public class SQSAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
log.warn("认证成功");
//由于顶替了原本的成功处理,所以是没有跳转的
}
}
还有个失败处理器(针对默认登录方式的)
@Component @Slf4j public class SQSAuthenticationFailHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { log.warn("认证失败"); } }
登出成功:
@Component
@Slf4j
public class SQSLogoutHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
log.warn("登出成功");
}
}
8.2 Security 配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private AuthenticationSuccessHandler successHandler;
@Resource
private AuthenticationFailureHandler failureHandler;
@Resource
private LogoutSuccessHandler logoutSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().successHandler(successHandler).failureHandler(failureHandler);
http.authorizeRequests().anyRequest().authenticated();
http.logout().logoutSuccessHandler(logoutSuccessHandler);
http.httpBasic();
}
}
8.3 自定义登录模式的成功处理器
认证成功:
- 不一定到达这个过滤器就一定是认证成功,所以要进行一些判断(只是模拟那个效果罢了)
@Component
public class AuthSuccessFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if(SecurityContextHolder.getContext().getAuthentication().getDetails() instanceof LoginUser) {
System.out.println("认证成功!");
}
filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
}
}
Security 配置 (判断成功的原理:在特定的过滤器之后加上对应的过滤器):
// 添加过滤器
http.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(authSuccessFilter, JwtAuthenticationTokenFilter.class);
文章到此结束,Spring Security 还有很多知识,一篇文章是讲不完的,所以还要继续学习才行,不会的去查,边用边学~
但是大部分的功能,利用 Spring Security 加上自己的想法去灵活地自定义实现一些业务吧~
代码地址:java-research/SecurityDemo at master · CarefreeState/java-research (github.com)