使用Spring Security和JWT构建安全的身份验证系统
首先我们需要创建一个配置,而 SecurityFilterChain
是 SpringSecurity 中的一个关键,用于定义一组安全过滤链,会按照顺序依次执行。用于处理认证、授权。
本文主要是说明前后端分离开发模式需要做的配置
禁用 CSRF
先说一下 CSRF 是做什么的吧,CSRF 叫做跨站请求伪造,是一种网络攻击的方式,攻击者通过欺骗用户在已登录网站上执行非预期的动作。
为了防范 CSRF 攻击,常见的做法是使用令牌(Token),服务器在响应中随机生成一个 CSRF 令牌,在后续的请求中都需要携带此令牌,保证请求的正确性。
两种方式都是确保请求的是否是合法用户,而 JWT 是一种基于 JSON 的令牌,它由三部分头部、载荷和签名组成,JWT 优点就是在不同系统之间传递身份信息,允许无状态验证。
既然我们使用了 JWT 无状态认证,那么就不再需要使用 Session 去维护用户的信息,也减轻服务器的压力。
所以下面我们禁用 Csrf 和 Session
java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// 禁用 csrf,前后端分离模式不需要,因为是无状态的
httpSecurity.csrf(AbstractHttpConfigurer::disable)
// 禁用 Session存储 -- SessionCreationPolicy.STATELESS 表示永远不会创建会话
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
设置允许请求
配置验证码,登录请求安全开放,通过 antMatchers 匹配设置允许的请求
通过 .anyRequest().authenticated()) 设置其他请求需要认证
java
httpSecurity.csrf(AbstractHttpConfigurer::disable)
// 配置允许请求
.authorizeRequests(expressionInterceptUrlRegistry ->
expressionInterceptUrlRegistry.antMatchers("/login", "/captchaImage").permitAll()
// 下面是追加对资源的释放,并设置get请求
.antMatchers(HttpMethod.GET, "/", "222.html").permitAll()
// 其他请求需要认证
.anyRequest().authenticated())
禁用 X-Frame-Options
禁用 X-Frame-Options 是一种安全机制,用于防止网页被嵌入到 iframe 中,也是一种安全的行为。
java
httpSecurity
// 禁用网页嵌套
.headers().frameOptions().disable();
配置过滤器链
因为默认是没有过滤器链的,无法实现拦截相关的不合法的请求,以及认证,首先将过滤器链交给 SpringSecurity
java
@Autowired
private SecurityAdminConfig securityAdminConfig;
httpSecurity.apply(securityAdminConfig);
具体的过滤器链需要继承 SecurityConfigurerAdapter
java
@Configuration
public class SecurityAdminConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
在这里添加过滤器,过滤器链中可以添加认证、授权的具体实现,例如:设置授权规则。
SecurityConfigurerAdapter 是 SpringSecurity 提供的用于配置 Security 的适配器,通过继承它,可以操作过滤器链。
SecurityConfigurerAdapter 中有一个 configure 方法,在这里面实现,身份验证的过滤器。
创建身份验证过滤器
身份验证器的作用是处理认证,包括配置认证失败,登录失败等等,认证的方法,认证的路由等
需要继承 AbstractAuthenticationProcessingFilter
类,它提供了通用的身份验证处理框架
其中需要实现方法是 attemptAuthentication 在这个里面需要对身份验证
那我们就需要将路由转到身份验证中
在下面的构造方法中,创建了一个 /login 的 post 请求路径,表示拦截此路由到这里。
java
public AdminUsernamePasswordAuthenticationFilter() {
// 默认匹配 "/admin/login" 路径的 POST 请求
super(new AntPathRequestMatcher("/login", "POST"));
}
获取用户密码
我这里是前端是通过 JSON 的方式传递的,所以获取用户密码,需要读取流,后面 JWT 认证需要这个对象的数据,在这里存入
java
// 由于前端传递的是 JSON,首先要读取流,转换为字符串,解析出来对象类型
LoginBody loginBody = JSONUtil.toBean(
new String(IoUtil.readBytes(request.getInputStream()), StandardCharsets.UTF_8), LoginBody.class);
// 后面 JWT 认证需要这个对象的数据,在这里存入
// 存入请求头中,这样后面才能使用
request.setAttribute("LoginBody", loginBody);
开始身份验证
要进行身份验证,需要先创建验证令牌类,它默认是无状态的,状态随着验证过程逐渐改变UsernamePasswordAuthenticationToken
,unauthenticated 方法可以获取无状态的验证类
java
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
那怎么验证呢
首先要获取当前过滤器链所属的 AuthenticationManager
身份验证类,调用 authenticate 方法将携带用户信息的验证令牌类传递进去进行验证。
java
this.getAuthenticationManager().authenticate(authRequest);
下面我们写好了,就要使用 身份验证类了
在过滤器链中配置身份验证类,其中需要设置一个身份验证类,这个类是进行身份验证的,否则在验证中就会获得 null
通过 builder.getSharedObject(AuthenticationManager.class) 来获取已配置的AuthenticationManager 实例
又通过 addFilterAt 将自定义过滤器,放到默认过滤器 UsernamePasswordAuthenticationFilter
之前
java
@Configuration
public class SecurityAdminConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) {
// 自定义身份验证过滤器
AdminUsernamePasswordAuthenticationFilter adminUsernamePasswordAuthenticationFilter = new AdminUsernamePasswordAuthenticationFilter();
// 设置身份验证类--每个过滤器链都要设置
adminUsernamePasswordAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
builder.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
认证失败类
在身份验证类中,需要做一些校验规则,比如下面这样对账户密码进行了校验、验证码校验等,如果为空,就抛出 BadCredentialsException
异常,这表示参数为空,400
java
// 由于前端传递的是 JSON,首先要读取流,转换为字符串,解析出来对象类型
LoginBody loginBody = JSONUtil.toBean(
new String(IoUtil.readBytes(request.getInputStream()), StandardCharsets.UTF_8), LoginBody.class);
if (StringUtils.isEmpty(loginBody.getUsername()))
throw new BadCredentialsException("用户名不能为空!");
if (StringUtils.isEmpty(loginBody.getPassword()))
throw new BadCredentialsException("密码不能为空!");
抛出了异常,就要拦截该异常,做一些友好的操作,创建一个类实现 AuthenticationFailureHandler
类,重写 onAuthenticationFailure
方法,之前抛出的异常就会拦截到此处,这里切换不要设置 Code,因为前后端分离汇中,一般会使用 axios 检测后端返回的 response code 码,如果不等于 200,会进入前端的 Error 方法中
java
@Component
public class FailHandle implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
// response.setStatus(400);----设置200,或者不要设置
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
// 转 JSON 友好的返回数据
response.getWriter().write(JSONUtil.toJsonStr(AjaxResult.error(exception.getMessage())));
}
}
配置好,我们装载这个认证失败类
在配置过滤器链类中对身份验证过滤器安装,通过 setAuthenticationFailureHandler 方法装载
java
@Autowired
private FailHandle failHandle; // 刚刚配置的认证失败类
// 自定义身份验证过滤器
AdminUsernamePasswordAuthenticationFilter adminUsernamePasswordAuthenticationFilter = new AdminUsernamePasswordAuthenticationFilter();
// 设置身份验证类--每个过滤器链都要设置
adminUsernamePasswordAuthenticationFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
// 设置认证失败异常类
adminUsernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(failHandle);
密码校验
BCryptPasswordEncoder 是 SpringSecurity 提供的加密器之一,密码加密是为了保护用户密码的安全性,BCryptPasswordEncoder 是基于 Blowfish 密码哈希算法的强大哈希函数,是单向不可逆的,使用了盐增加密码的安全性。
java
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
这一步是由 SpringSecurity 自动校验的,但是我们需要修改为从数据库查询,上面已经配置了密码校验器,也配置了校验功能,就是下面这个
java
// 使用认证管理器进行验证
this.getAuthenticationManager().authenticate(authRequest);
我们首先要创建类去实现 UserDetailsService
接口,它是 Spring Security 提供的接口,用于从特定的数据源加载用户信息。
loadUserByUsername 方法是该接口中唯一的方法,它接收一个用户名作为参数,并返回一个实现了 UserDetails 接口的对象。
该对象一般会包含用户的角色,用户的信息,用户的部门和权限等。
java
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
下面我们就来看一下实现了 UserDetails 接口的对象
java
/**
* 登录用户身份权限
*/
@Data
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
public LoginUser() {
}
@JSONField(serialize = false)
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
/**
* 权限集合
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
返回该对象即可完成身份校验
认证成功类
与认证失败步骤一样,实现 AuthenticationSuccessHandler 接口,重写第一个,三个参数的方法,最后在身份验证类中装载
java
/**
* 认证成功
*/
@Component
public class SuccessHandle implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(AjaxResult.success("登录成功")));
}
}
注意:认证成功之后是需要写入 JWT 的,这里先使用,下一段讲解一下 JWT 的基础知识
首先需要准备一个 Map,第一个加入的是令牌的前缀,我们需要根据这个token获取存入 Redis 的对象
java
Map<String, Object> claims = new HashMap<>();
claims.put("login_user_key", token);
//增加标识,可以加多个
claims.put("222", "2222");
java
@Autowired
private SuccessHandle successHandle;
// 自定义身份验证过滤器
AdminUsernamePasswordAuthenticationFilter adminUsernamePasswordAuthenticationFilter = new AdminUsernamePasswordAuthenticationFilter();
// 设置认证成功类
adminUsernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(successHandle);
到这里就已经配置完成了。
JWT 认证
什么是 JWT 呢,就是将用户数据存在客户端,服务器身份验证后,生成 JSON 对象发送回用户,当用户与服务器进行通信时,发回 JSON 对象,在生成对象时添加签名,对发回的数据进行验证。
这样做的好处就是,可以防止用户恶意修改用户数据。
JWT 由三部分组成
- Header 头部:原数据的JSON对象
- Payload 载荷:包含传递的数据,和一些默认字段可供选择,签发人、主题、用户、过期时间、生效时间、签发时间和标识JWT
- signature 签名:需要指定一个存在服务器上面并且不能向外公开的 secret,这个部分需要使用 base64 URL加密的 head 和 base64 url加密的 Payload 并使用点连接的字符串,还要使用 head 中声明的加密算法进行加盐、secret 组合加密。最后得出签名,并且无法反向解密。
注意这是一行,不需要换行
验证过程
- header 做 base64 url 解密
- 对 header 和 payload 做一次签名
- 比较签名
下面我们先创建一个 Token 过滤器,为了验证 JWT
需要继承 OncePerRequestFilter 过滤器,主要为了验证 Token 的有效性,它集成自 GenericFilterBean 确保在请求处理过程中只会执行一次,不会重复执行。
java
/**
* token过滤器 验证token有效性
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
}
}
需要重写 doFilterInternal 方法,该方法可以处理一下逻辑
我们还需要将 jwt 过滤器装载到过滤器链中,我们为什么要放到密码校验之前呢,因为我们需要首先判断 JWT 存不存在,存在就进行身份校验,不存在直接交给下一个过滤器
java
/**
* jwt 认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
public void configure(HttpSecurity builder) {
builder.addFilterAt(adminUsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
// 装载
.addFilterBefore(jwtAuthenticationTokenFilter, AdminUsernamePasswordAuthenticationFilter.class);
}
那我们开始 JWT 认证吧
首先我们应该在登录成功之后,将 JWT 令牌存入起来,并且返回给前端。
这里要做的呢,就是生成 JWT 令牌,并且存入 Redis,下面代码传入的 principal 是实现了 UserDetails 的用户对象
java
// 生成令牌
String token = tokenService.createToken(principal);
看下面代码,UUID 是用于在 redis 中获取信息,再继续我们就设置登录时间,过期时间等信息,并存入 Redis,过期时间 = 当前登录时间 * 30 * 60 * 10000 就等于 30 分钟的毫秒数,拼接 redis key 为:login_tokens:9193dec8-db26-4261-9192-00496564675e
刷新令牌有效期
java
// 生成一个 UUID
String token = IdUtils.fastUUID();
// 存入
loginUser.setToken(token);
// 设置登录时间
loginUser.setLoginTime(System.currentTimeMillis());
// 设置有效期
loginUser.setExpireTime(loginUser.getLoginTime() + 30 * 60 * 1000);
// 拼接 redis key
String userKey = "login_tokens:" + token;
// 存入 redis,设置过期时间 30 分钟
stringRedisTemplate.opsForValue().set(userKey, JSONUtil.toJsonStr(loginUser), 30, TimeUnit.MINUTES);
到这里呢,只是存入了,但是并没有 JWT 加密呢,下面这段代码前面已经说过了,是做了一个基本的信息,使用这个信息呢,开始加密。
java
Map<String, Object> claims = new HashMap<>();
claims.put("login_user_key", token);
//增加标识,可以加多个
claims.put("222", "2222");
下面我们开始加密,第一步,创建一个 JWT 构建器对象,用于构建 JWT,加入的 claims 是包含有关用户和其他数据的 JSON 对象。通常,它包含标准的声明(例如,过期时间、发行时间等)以及自定义的声明。而我们存入的是一个 key,根据key 获取用户信息即可。
加密算法,对 JWT 进行签名。在这里,使用 HS512 算法进行签名,其中 secret 是用于生成签名的密钥。而 secret 盐是自定义的,可以写到 yml文件中。最后构建即可。
返回给前端最终的 JWT 令牌即可。
java
// 创建 JWT 构建器
JwtBuilder builder = Jwts.builder();
// JWT 的声明
builder.setClaims(claims);
// 声明加密算法
builder.signWith(SignatureAlgorithm.HS512, secret);
// 构建并返回最终的 JWT 字符串
String compact = builder.compact();
认证流程
我们看一下下面代码,跳转到下面代码后,首先判断是否携带 jwt token,如果携带了,是访问资源,如果未携带访问的是公共请求请求。
如果为空,直接放行。如果携带了,就去解密 JWT 字符串。
java
// 获取用户信息
LoginUser loginUser = tokenService.getLoginUser(token);
看下具体实现,首先第一步肯定是解析 JWT 字符串,然后获取 JWT 字符串内容,根据之前存入的 UUID 获取 Redis 中的用户信息。
首先 JwtParser 创建一个 JWT 解析器对象,拿到了解析器对象,第一步先验证之前的盐也就是 secret 是否一致,盐都不一致的话,这个 Token 肯定是被篡改了。
然后我们解析 Token,获取 Jws<Claims>,这里包含了 JWT的声明和签名信息。
最后获取 JWT 声明的主体部分。
java
// 创建 JWT 解析器对象
JwtParser parser = Jwts.parser();
// 设置解析器的签名密钥,用于验证 JWT 的签名是否有效
parser.setSigningKey(secret);
// 使用解析器解析传入的 JWT 字符串
// parseClaimsJws 方法返回一个 Jws<Claims> 对象,其中包含了 JWT 的声明(claims)和签名信息
Jws<Claims> claimsJws = parser.parseClaimsJws(token);
// 从 Jws 对象中获取 JWT 的声明部分
// Claims 对象包含了 JWT 中存储的各种声明信息,比如用户ID、过期时间等
Claims body = claimsJws.getBody();
还记得我们前面存入的用户信息的 key 吗,我们获取 JWT 中的信息
java
// 解析对应的权限以及用户信息
String uuid = (String) claims.get("login_user_key");
// 拼接一下
String userKey = Constants.LOGIN_TOKEN_KEY + uuid;
// 通过redis获取用户信息
String cacheObject = stringRedisTemplate.opsForValue().get(userKey);
// 获取用户信息
LoginUser loginUser = JSONUtil.toBean(cacheObject, LoginUser.class);
拿到用户信息后,如果用户信息是空的,那代表授权已经过期了,或者无效。直接就抛出异常就可以了。如果不是直接就放行。
我们看下面这一段代码,重要
检查 Authentication 是否为空,表示获取当前已经通过认证的用户信息,如果没有则表示可以认证。
java
// 判断是否已经认证过了
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (StringUtils.isNull(authentication)) {
}
如果没有认证过,下面我们就可以进行认证了
首先刷新有效期,为什么会有刷新有效期呢,原因很简单,用户携带了 Token,但是 Security 没有认证信息,这个时候是需要刷新 JWT 的,并重新给 Security 认证。
20 * 60 * 1000L 表示20分钟
Security 认证步骤:
UsernamePasswordAuthenticationToken 要 SpringSecurity 提供的 Authentication 接口的实现类,通过传入一个实现 UserDetails 的实体类,包含用户信息以及具体的权限信息,进行验证。
UsernamePasswordAuthenticationToken表示验证信息,具体参数为;参数一:是用户信息,参数二:是凭证信息,但是之前密码是已经验证过的,无需再次验证。参数三:是所拥有的权限。
setDetails 方法设置认证的详细信息 其中 new WebAuthenticationDetailsSource() 对象封装认证相关的web请求信息,目的是将认证有关的信息存入到 Authentication 对象中。
SecurityContextHolder.getContext().setAuthentication(authenticationToken); 表示通过认证。
java
// 刷新令牌有效期
tokenService.verifyToken(loginUser);
// 获取过期时间
long expireTime = loginUser.getExpireTime();
// 获取当前时间
long currentTime = System.currentTimeMillis();
// 如果有效期不足 20 分钟,就自动刷新缓存
if (expireTime - currentTime <= 20 * 60 * 1000L) {
// 下面执行的代码,上面已经说过了,刷新令牌有效期
// Security 认证步骤
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
下面是具体代码。
java
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// 获取请求携带的令牌
String token = tokenService.getToken(request);
// 如果为空直接放行
if (StringUtils.isEmpty(token)) {
chain.doFilter(request, response);
return;
}
// 获取用户信息
LoginUser loginUser = tokenService.getLoginUser(token);
if (StringUtils.isNull(loginUser)) {
throw new BadCredentialsException("令牌无效");
}
if (StringUtils.isNull(SecurityContextHolder.getContext().getAuthentication())) {
// 刷新令牌有效期
tokenService.verifyToken(loginUser);
// 认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
JWT 认证失败怎么处理
在 Security 配置类中添加一下代码,其中 httpSecurity.exceptionHandling 是 Spring Security 配置中用于处理异常的一部分,其中 ex.authenticationEntryPoint 用于处理访问未授权的资源。我们添加一个实现 AuthenticationEntryPoint 接口的类即可
java
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.exceptionHandling(ex -> ex.authenticationEntryPoint(authenticationEntryPoint));
return httpSecurity.build();
}
我们具体看一下这个处理认证失败的类 AuthenticationEntryPointImpl
java
/**
* 认证失败处理类 返回未授权
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(AjaxResult.error(msg)));
}
}
退出类
首先定义这样一个 Bean
这里就不过多介绍了,就是记得删除 redis 中的数据。有个注意事项哈,就是退出过滤器是需要认证的,不能公开。
java
@Service
public class LogoutSuccessHandle implements LogoutSuccessHandler {
@Autowired
private TokenService tokenService;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = tokenService.getToken(request);
if (token != null) {
LoginUser loginUser = tokenService.getLoginUser(token);
if (StringUtils.isNotNull(loginUser)) {
// 删除用户的缓存
tokenService.delLoginUser(loginUser.getToken());
}
}
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(AjaxResult.error("退出登录成功!")));
}
安装在 Security 的配置类中
java
@Autowired
private LogoutSuccessHandle logoutSuccessHandle;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
// 退出过滤器
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandle);
return httpSecurity.build();
}
最后补充一下,处理身份验证和注销请求之前,先进行 CORS 过滤,以确保跨域请求能够正确处理。
因为现在前后端都在不同的域下
在 Security 配置类中添加一下代码,暂时留空