SpringSecurity
SpringSecurity
一:快速入门:
创建好一个springboot-maven项目,写好启动类,并且编写简单的controller,加上以下依赖:
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
此时,我们会发现访问之前的简单页面,会出现:
并且我们在控制台可以发现:
此时,我们输入user(账号)和以上密码,可以看到:
即登陆成功。
二:认证:
2.1.登录流程:
2.2.SpringSecurity流程分析:
具体如何debug如下:
以Debug模式运行,使它运行完run命令,即创建好bean。
搜索并且查看即可。
2.3.认证流程:
注意,这里的有些需要被替换,比如userdetailsSevice从内存中查询需要我们改成从数据库中查询。其次,在有的时候,我们需要的是给前端响应token,所以第一个实现类也需要我们改变。
我们可以把信息存储在redis中,以前我们是查询数据库,现在这样处理之后,查询效率更快。
2.4.替换UserDetails:
具体代码如下:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Resourceprivate SysUserService sysUserService;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(SysUser::getUserName,username);SysUser user = sysUserService.getOne(lambdaQueryWrapper);if(Objects.isNull(user)){throw new RuntimeException("用户不存在或者密码错误。");}return new LoginUser(user);}
}
注意,查询完数据库之后,会返回一个UserDetails,所以我们还要编写下面的代码:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {private SysUser sysUser;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return sysUser.getPassword();}@Overridepublic String getUsername() {return sysUser.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
注意,现在密码的解析有默认的处理器,所以如果直接在数据库中插入数据,记得加上{noop}
前缀。为什么呢:
在上面的流程图中,它会通过PasswordEncoder来进行密码比对:
@Configuration
public class SecurityConfig extends WebSecurityConfiguration {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
}
使用举例:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;public class PasswordEncryptionTest {public static void main(String[] args) {// 创建密码编码器实例PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();// 原始密码String rawPassword = "password123";// 加密密码String encodedPassword = passwordEncoder.encode(rawPassword);System.out.println("加密后的密码:" + encodedPassword);// 验证密码boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);System.out.println("验证密码是否匹配:" + matches);}
}
注意,此时数据库中的密码必须是存储的加盐之后的密码:
$2a$10$6.coJVZ02XeCRy0AOvHF5unJDE/54sfP9OfxqctmPUUoK3HooaxlW
2.5.编写登录代码:
注意,我们要改变SpringSecurity的一些默认操作,比如拦截登录页面。
具体业务代码如下:
@Service
public class LoginServiceIml implements LoginService {@Autowiredprivate AuthenticationConfiguration authenticationConfiguration;@Autowiredprivate RedisCache redisCache;@Overridepublic ResponseResult login(SysUser sysUser) throws Exception {UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUserName(),sysUser.getPassword());AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();Authentication authenticate = authenticationManager.authenticate(authenticationToken);if((Objects.isNull(authenticate))){throw new Exception("用户名或密码错误");}LoginUser loginUser = (LoginUser) authenticate.getPrincipal();Long id = loginUser.getSysUser().getId();String jwt = JwtUtil.createJWT(id.toString());redisCache.setCacheObject("login:"+id,loginUser);return new ResponseResult(200,"登录成功",new HashMap<String,String>().put("token",jwt));}
}
注意,这里我们的目标是取消登录页面的拦截,但是想要依然保留它的认证功能。
逻辑:
- 创建一个
UsernamePasswordAuthenticationToken
对象,其中包含了用户提供的用户名和密码。 - 从
authenticationConfiguration
中获取AuthenticationManager
对象,并使用authenticate()
方法进行身份验证,返回一个Authentication
对象。 - 如果
authenticate
对象为空(即认证失败),则抛出异常并提示“用户名或密码错误”。 - 如果认证成功,则从
authenticate
对象中获取登录用户的信息。 - 根据用户的 ID 创建一个 JWT 令牌。
- 将登录用户信息存储到 Redis 缓存中,以便后续使用。
- 返回一个包含登录成功信息和生成的 JWT 令牌的响应对象。
注意:这里要记得放行登录页面:
@Override
protected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();
}
代码详细解释
//关闭csrf.csrf().disable()
关闭CSRF保护
CSRF(Cross-Site Request Forgery)是跨站请求伪造攻击,Spring Security默认开启CSRF保护。然而,对于基于JWT的无状态API认证,通常不需要CSRF保护,因此这里将其禁用。
//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
无状态会话管理
设置会话创建策略为无状态(STATELESS),意味着Spring Security不会创建会话来存储用户的认证信息。每个请求都必须通过JWT令牌来进行认证和授权。这对于RESTful API非常重要,因为它们通常是无状态的。
.and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()
请求授权配置
.and()
:连接前后的配置,使其生效。.authorizeRequests()
:开始配置请求级别的安全性。.antMatchers("/user/login").anonymous()
:允许匿名访问/user/login
端点。即,未认证的用户也可以访问这个URL,用于用户登录。
// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();
其他请求需认证
除了显式允许匿名访问的端点(如登录端点)外,其他所有请求都需要经过认证。这意味着,所有其他请求都必须携带有效的JWT令牌。
//把token校验过滤器添加到过滤器链中//http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
添加自定义JWT校验过滤器(注释掉的部分)
这行代码添加了自定义的JWT校验过滤器到过滤器链中,位置在 UsernamePasswordAuthenticationFilter
之前。虽然这行代码目前被注释掉了,但它的作用如下:
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
:将jwtAuthenticationTokenFilter
这个自定义过滤器插入到过滤器链中,在UsernamePasswordAuthenticationFilter
之前执行。
为什么要添加自定义JWT校验过滤器
JWT校验过滤器的作用是:
- 提取JWT令牌:从HTTP请求的头部提取JWT令牌。
- 验证JWT令牌:验证令牌的有效性,包括签名和过期时间。
- 设置认证信息:如果令牌有效,从中提取用户信息并在Spring Security上下文中设置认证信息,以便后续的授权检查。
2.6.编写过滤器代码:
我们把认证改成了基于jwt的认证模式,而在默认过滤器链中,不支持这种写法,所以我们要自己编写过滤器链。这里我们选择继承OncePerRequstFilter。
选择继承 OncePerRequestFilter
而不是传统的 Filter
在Spring Security中有几个重要的原因:
1. 确保每个请求只执行一次
OncePerRequestFilter
是一个基类,它确保其子类过滤器每个请求只执行一次。这对于处理诸如JWT验证这样的任务非常重要,因为我们希望每个请求只进行一次身份验证,而不是在请求链中的每一个环节都重复执行。
2. 简化实现
继承 OncePerRequestFilter
简化了过滤器的实现。它为开发者提供了一些有用的功能,简化了过滤器的实现过程,避免了手动处理某些常见的复杂情况。
3. 集成Spring框架
OncePerRequestFilter
是Spring框架的一部分,它与Spring的其他部分集成良好,特别是Spring Security。这使得在实现自定义过滤器时,可以更轻松地使用Spring提供的各种功能和工具。
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Autowiredprivate RedisCache redisCache;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");if (!StringUtils.hasText(token)) {filterChain.doFilter(request, response);return;}String userId;try {Claims claims = JwtUtil.parseJWT(token);userId = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}String redisKet = "login:" + userId;LoginUser cacheObject = redisCache.getCacheObject(redisKet);if(Objects.isNull(cacheObject)){throw new RuntimeException("用户未登录");}//TODO 获取权限信息封装到Authentication中//标识已经认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(cacheObject,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}
}
注意,这里的未获取到token依然放行是因为这是一个过滤器链,此时的请求未认证,会被其他的过滤器拦截,所以我们直接放行。其次,我们这里用的是三个参数的构造函数,因为在三个参数的函数中,会进行认证操作。因为后续其他过滤器操作是在 SecurityContextHolder
中获取信息,所以我们还要设置这一步。
在代码片段中:
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(cacheObject, null, null);
这个构造方法有三个参数:
principal
(第一个参数):表示当前经过认证的用户信息。在这个例子中,cacheObject
是UserDetails
的实现类对象,它包含了用户的身份信息。credentials
(第二个参数):表示用户的凭证信息(例如密码)。在这个例子中传递null
,因为用户已经被认证,凭证不再需要,这里是通过redis验证了。authorities
(第三个参数):表示用户的权限(角色)。在这个例子中传递null
,后续在授权部分会改进代码。
2.7.配置过滤器:
在我们定义好过滤器之后,还要将它加入我们的过滤器链,并且指明执行顺序。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//把token校验过滤器添加到过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}
指明在UsernamePasswordAuthenticationFilter
之前即可。
2.8.退出登录:
@Overridepublic ResponseResult<String> logout() {UsernamePasswordAuthenticationToken authentication= (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();Long id = loginUser.getSysUser().getId();redisCache.deleteObject("login:"+id);return new ResponseResult<>(200,"退出成功");}
SecurityContextHolder.getContext().getAuthentication():这是Spring Security提供的API,用于获取当前线程的安全上下文。在默认实现中,SecurityContextHolder使用ThreadLocal来存储SecurityContext,这样可以确保每个线程有自己的安全上下文。
三:权限:
3.1.基本流程:
在前面存入的Holder代码中,有个TODO如下:
@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {String token = request.getHeader("token");if (!StringUtils.hasText(token)) {filterChain.doFilter(request, response);return;}String userId;try {Claims claims = JwtUtil.parseJWT(token);userId = claims.getSubject();} catch (Exception e) {e.printStackTrace();throw new RuntimeException("token非法");}String redisKet = "login:" + userId;LoginUser cacheObject = redisCache.getCacheObject(redisKet);if(Objects.isNull(cacheObject)){throw new RuntimeException("用户未登录");}//TODO 获取权限信息封装到Authentication中//标识已经认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(cacheObject,null,null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);filterChain.doFilter(request, response);}
3.2.授权实现:
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置springSecurity里面加。
@EnableGlobalMethodSecurity(prePostEnabled = true)
新版本可以用@EnableMethodSecurity 。
然后就可以使用对应的注解。@PreAuthorize
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){return "hello";}
}
3.3.封装权限:
回顾之前的流程:
首先需要我们将查询的权限加入UserDetails中:
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {private SysUser sysUser;private List<String> permissions;@JSONField(serialize = false)private List<SimpleGrantedAuthority> authorities;public LoginUser(SysUser sysUser,List<String> permissions) {this.sysUser = sysUser;this.permissions = permissions;}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if(authorities == null){authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());}return authorities;}@Overridepublic String getPassword() {return sysUser.getPassword();}@Overridepublic String getUsername() {return sysUser.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
注意这里的注解:
@JSONField(serialize = false)
是一个来自于阿里巴巴的 fastjson
库的注解,用于控制 Java 对象在序列化和反序列化时的行为。具体来说,serialize = false
表示在对象转换为 JSON 字符串时,这个字段将被忽略,不会出现在 JSON 输出中。
再在过滤器中加上信息的封装:
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(cacheObject,null,cacheObject.getAuthorities());
3.4.从数据库中查询权限:
之前的业务是直接写好具体权限的,实际上在实际业务中需要我们去数据库中去查找,所以我们会用到:RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
如果只有这两个表,那么在实际运用中可能会比较麻烦,因为这些权限比较小,一个用户可能会涉及多个小的权限,就不是很方便了。所以我们引入下面的角色表:
最终就当是下面的五张表:
3.5.代码实现:
编写mapper以及xml文件:
@Mapper
public interface MenuMapper extends BaseMapper<Menu> {List<String>selectPermsByUserId(Long userId);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.shelly.mapper.MenuMapper"><select id="selectPermsByUserId" resultType="java.lang.String">SELECTDISTINCT m.`perms`FROM sys_user_role urLEFT 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 user_id = #{userId} and r.status = 0 and m.status = 0</select>
</mapper>
返回UserDetails对象:
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapper<SysUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();lambdaQueryWrapper.eq(SysUser::getUserName,username);SysUser user = sysUserService.getOne(lambdaQueryWrapper);if(Objects.isNull(user)){throw new RuntimeException("用户不存在或者密码错误。");}List<String> list = menuMapper.selectPermsByUserId(user.getId());return new LoginUser(user,list);}
接口权限要求:
@RequestMapping("/hello")@PreAuthorize("hasAuthority('system:dept:list')")public String hello(){return "hello";}
四:其他
4.1.自定义异常处理:
4.1.1.认证异常处理:
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户名认证失败请重新登录");String json = JSON.toJSONString(result);//处理异常WebUtils.renderString(response, json);}
}
4.1.2.授权异常处理:
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "授权不足");String json = JSON.toJSONString(result);//处理异常WebUtils.renderString(response, json);}
}
4.1.3.配置处理器:
@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {http//关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 对于登录接口 允许匿名访问.antMatchers("/user/login").anonymous()// 除上面外的所有请求全部需要鉴权认证.anyRequest().authenticated();//把token校验过滤器添加到过滤器链中http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
}
4.2.跨域:
浏览器出于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端和后端项目一般都不是同源的,所以会出现跨域问题。
1.先对SpringBoot配置,允许跨域请求:
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {//设置允许跨域的路径registry.addMapping("/**")//设置允许跨域请求的域名.allowedOriginPatterns("*")//是否允许cookie.allowCredentials(true)//设置允许的请求方式.allowedMethods("GET","POST","DELETE","PUT")//设置允许的header属性.allowedHeaders("*")//跨域允许的时间.maxAge(3600);}
}
2. 开启SpringSecurity的跨域访问:
http.cors();
在Security的config配置即可。
4.3.其他权限验证:
我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验,Springsecurity还为我们提供了其它方法例如: hasAnyAuthority,hasRole,hasAnyRole等。
hasAuthority:
方法实际是执行到了SecurityExpressionRoot的hasAuthority,只要断点调试既可知道它内部的校验原理。它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。
hasAnyAuthority:
方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
hasRole:
要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE这个前缀才可可以。
hasAnyRole:
有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_这个前缀才可以。
4.4.自定义权限验证:
自定义配置类:
@Component("ex")
public class SGExpressionRoot{public boolean hasAuthority(String authority){//获取当前用户的权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();List<String> permissions = loginUser.getPermissions();//判断用户权限集合中是否存在authorityreturn permissions.contains(authority);}
}
在SPEL表达式中使用@ex相当于获取容器中bean的名字为ex的对象,然后在调用这个对象的hasAuthority方法
@RestController
public class HelloController{@RequestMapping("/hello")@PreAuthorize("@ex.hasAuthority('system:dept:list')")public String hello(){return "hello";}
}
基于配置的权限控制:
//基于配置的权限控制
.antMatchers("/hello").hasAuthority("system:dept:list")
4.5.CSRF:
CSRF是指跨站请求伪造 (Cross-site request forgery) ,是web常见的攻击之一。
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
4.6.认证处理器
在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。
在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录失败了是会调用AuthenticationFailureHandler的方法进行认证成功后的处理的。AuthenticationFailureHandler就是登录失败处理器。
我们也可以自己去自定义成功、失败处理器进行相应处理。
@Component
public class SGSuccessHandler implements AuthenticationSuccessHandler{@Overridepublic void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException,ServletException{System.out.println("认证成功了");}
}
@Component
public class SGFailureHandler implements AuthenticationFailureHandler{@Overridepublic void onAuthenticationFailure(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException,ServletException{System.out.println("认证失败了");}
}
@Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler{@Overridepublic void onLogoutSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException,ServletException{System.out.println("注销成功了");}
}
配置处理器:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{@Autowiredprivate AuthenticationSuccessHandler successHandler;@Autowiredprivate AuthenticationFailureHandler failureHandler;@Autowired private LogoutSuccessHandler logoutSuccessHandler;@Overrideprotected void configure(HttpSecurity http) throws Exception{http.formLogin() //配置认证成功处理器.successHandler(successHandler)//配置认证失败处理器.failureHandler(failureHandler); http.logout()//配置注销成功处理器.logoutSuccessHandler(logoutSuccessHandler);http.authorizeRequests().anyRequest().authenticated(); }
}