Spring Security 实战干货:实现自定义退出登录

logout.png

1. 前言

上一篇对 Spring Security 所有内置的 Filter 进行了介绍。今天我们来实战如何安全退出应用程序。

2. 我们使用 Spring Security 登录后都做了什么

这个问题我们必须搞清楚!一般登录后,服务端会给用户发一个凭证。常见有以下的两种:

  • 基于 Session 客户端会存 cookie 来保存一个 sessionId ,服务端存一个 Session

  • 基于 token 客户端存一个 token 串,服务端会在缓存中存一个用来校验此 token 的信息。

2. 退出登录需要我们做什么

  1. 当前的用户登录状态失效。这就需要我们清除服务端的用户状态。
  2. 退出登录接口并不是 permitAll, 只有携带对应用户的凭证才退出。
  3. 将退出结果返回给请求方。
  4. 退出登录后用户可以通过重新登录来认证该用户。

3. Spring Security 中的退出登录

接下来我们来分析并实战 如何定制退出登录逻辑。首先我们要了解 LogoutFilter

3.1 LogoutFilter

通过 Spring Security 实战干货:内置 Filter 全解析 我们知道退出登录逻辑是由过滤器 LogoutFilter 来执行的。 它持有三个接口类型的属性:

  1. RequestMatcher logoutRequestMatcher 这个用来拦截退出请求的 URL
  2. LogoutHandler handler 用来处理退出的具体逻辑
  3. LogoutSuccessHandler logoutSuccessHandler 退出成功后执行的逻辑

我们通过对以上三个接口的实现就能实现我们自定义的退出逻辑。

3.2 LogoutConfigurer

我们一般不会直接操作 LogoutFilter ,而是通过 LogoutConfigurer 来配置 LogoutFilter。 你可以通过 HttpSecurity#logout() 方法来初始化一个 LogoutConfigurer 。 接下来我们来实战操作一下。

3.2.1 实现自定义退出登录请求URL

LogoutConfigurer 提供了 logoutRequestMatcher(RequestMatcher logoutRequestMatcher)logoutUrl(Sring logoutUrl) 两种方式来定义退出登录请求的 URL 。它们作用是相同的,你选择其中一种方式即可。

3.2.2 处理具体的逻辑

默认情况下 Spring Security 是基于 Session 的。LogoutConfigurer 提供了一些直接配置来满足你的需要。如下:

  • clearAuthentication(boolean clearAuthentication) 是否在退出时清除当前用户的认证信息
  • deleteCookies(String... cookieNamesToClear) 删除指定的 cookies
  • invalidateHttpSession(boolean invalidateHttpSession) 是否移除 HttpSession

如果上面满足不了你的需要就需要你来定制 LogoutHandler 了。

3.2.3 退出成功逻辑
  • logoutSuccessUrl(String logoutSuccessUrl) 退出成功后会被重定向到此 URL你可以写一个Controller 来完成最终返回,但是需要支持 GET 请求和 匿名访问 。 通过 setDefaultTargetUrl 方法注入到 LogoutSuccessHandler
  • defaultLogoutSuccessHandlerFor(LogoutSuccessHandler handler, RequestMatcher preferredMatcher) 用来构造默认的 LogoutSuccessHandler 我们可以通过添加多个来实现从不同 URL 退出执行不同的逻辑。
  • LogoutSuccessHandler logoutSuccessHandler 退出成功后执行的逻辑的抽象根本接口。

3.3 Spring Security 退出登录实战

现在前后端分离比较多,退出后返回json。 而且只有用户在线才能退出登录。否则不能进行退出操作。我们采用实现 LogoutHandlerLogoutSuccessHandler 接口这种编程的方式来配置 。退出请求的 url 依然通过 LogoutConfigurer#logoutUrl(String logoutUrl)来定义。

3.3.1 自定义 LogoutHandler

默认情况下清除认证信息 (invalidateHttpSession),和Session 失效(invalidateHttpSession) 已经由内置的SecurityContextLogoutHandler 来完成。我们自定义的 LogoutHandler 会在SecurityContextLogoutHandler 来执行。

 @Slf4j
 public class CustomLogoutHandler implements LogoutHandler {
     @Override
     public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
         User user = (User) authentication.getPrincipal();
         String username = user.getUsername();
         log.info("username: {}  is offline now", username);
     }
 }

以上是我们实现的 LogoutHandler 。 我们可以从 logout 方法的 authentication 变量中 获取当前用户信息。你可以通过这个来实现你具体想要的业务。比如记录用户下线退出时间、IP 等等。

3.3.2 自定义 LogoutSuccessHandler

如果我们实现了自定义的 LogoutSuccessHandler 就不必要设置 LogoutConfigurer#logoutSuccessUrl(String logoutSuccessUrl) 了。该处理器处理后会响应给前端。你可以转发到其它控制器。重定向到登录页面,也可以自行实现其它 MediaType ,可以是 json 或者页面

  @Slf4j
  public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
      @Override
      public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
          User user = (User) authentication.getPrincipal();
          String username = user.getUsername();
          log.info("username: {}  is offline now", username);
  
  
          responseJsonWriter(response, RestBody.ok("退出成功"));
      }
  
      private static void responseJsonWriter(HttpServletResponse response, Rest rest) throws IOException {
          response.setStatus(HttpServletResponse.SC_OK);
          response.setCharacterEncoding("utf-8");
          response.setContentType(MediaType.APPLICATION_JSON_VALUE);
          ObjectMapper objectMapper = new ObjectMapper();
          String resBody = objectMapper.writeValueAsString(rest);
          PrintWriter printWriter = response.getWriter();
          printWriter.print(resBody);
          printWriter.flush();
          printWriter.close();
      }
  }
3.3.4 自定义退出的 Spring Security 配置

为了方便调试我 注释掉了我们 实现的自定义登录,你可以通过 http:localhost:8080/login 来登录,然后通过 http:localhost:8080/logout 测试退出。

       @Override
          protected void configure(HttpSecurity http) throws Exception {
              http.csrf().disable()
                      .cors()
                      .and()
                      .authorizeRequests().anyRequest().authenticated()
                      .and()
  //                    .addFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)
                      // 登录
                      .formLogin().loginProcessingUrl(LOGIN_PROCESSING_URL).successForwardUrl("/login/success").failureForwardUrl("/login/failure")
                      .and().logout().addLogoutHandler(new CustomLogoutHandler()).logoutSuccessHandler(new CustomLogoutSuccessHandler());
  
          }

4. 总结

本篇 我们实现了 在 Spring Security 下的自定义退出逻辑。相对比较简单,你可以根据你的业务需要来实现你的退出逻辑。有什么疑问可以通过 关注公众号:Felordcn 来私信提问 。相关DEMO代码也可以通过关注后回复 ss04 获取。

关注公众号:Felordcn获取更多资讯

个人博客:https://felord.cn

  • 3
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
实现自定义短信验证码登录,可以按照以下步骤进行: 1. 添加依赖 在项目中添加 Spring SecuritySpring Security SMS 模块的依赖。 ``` <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>5.2.2.RELEASE</version> </dependency> <dependency> <groupId>com.github.lanceshohara</groupId> <artifactId>spring-security-sms</artifactId> <version>1.0.2</version> </dependency> ``` 2. 配置 Spring SecuritySpring Security 配置文件中添加配置,包括短信验证码登录相关的配置。 ``` @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login/sms").permitAll() .anyRequest().authenticated() .and() .apply(smsCodeAuthenticationSecurityConfig) .and() .formLogin() .loginPage("/login") .loginProcessingUrl("/login/form") .usernameParameter("username") .passwordParameter("password") .defaultSuccessUrl("/") .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/") .permitAll(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } ``` 其中,`SmsCodeAuthenticationSecurityConfig` 是短信验证码登录的相关配置类,需要单独实现。 3. 实现短信验证码登录相关配置 实现 `SmsCodeAuthenticationSecurityConfig` 配置类,其中包括一个短信验证码过滤器和一个短信验证码认证提供者。 ``` @Configuration public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private UserDetailsService userDetailsService; @Autowired private SmsCodeAuthenticationSuccessHandler smsCodeAuthenticationSuccessHandler; @Autowired private SmsCodeAuthenticationFailureHandler smsCodeAuthenticationFailureHandler; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(smsCodeAuthenticationSuccessHandler); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(smsCodeAuthenticationFailureHandler); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } } ``` 其中,`SmsCodeAuthenticationFilter` 是短信验证码过滤器,需要单独实现。`SmsCodeAuthenticationSuccessHandler` 和 `SmsCodeAuthenticationFailureHandler` 分别是短信验证码认证成功和失败的处理器,也需要单独实现。 4. 实现短信验证码过滤器 实现 `SmsCodeAuthenticationFilter` 过滤器,重写 `attemptAuthentication` 方法,来处理短信验证码认证请求。 ``` public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; public static final String SPRING_SECURITY_FORM_CODE_KEY = "code"; private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY; private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher("/login/sms", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); String code = obtainCode(request); if (mobile == null) { mobile = ""; } if (code == null) { code = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, code); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected String obtainCode(HttpServletRequest request) { return request.getParameter(codeParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public void setMobileParameter(String mobileParameter) { this.mobileParameter = mobileParameter; } public void setCodeParameter(String codeParameter) { this.codeParameter = codeParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getMobileParameter() { return mobileParameter; } public final String getCodeParameter() { return codeParameter; } } ``` 其中,`SmsCodeAuthenticationToken` 是短信验证码认证的 token 类型,需要单独实现。 5. 实现短信验证码认证提供者 实现 `SmsCodeAuthenticationProvider` 提供者,重写 `authenticate` 方法,来进行短信验证码认证。 ``` public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; UserDetails userDetails = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } } ``` 6. 实现短信验证码认证成功和失败的处理器 实现 `SmsCodeAuthenticationSuccessHandler` 和 `SmsCodeAuthenticationFailureHandler` 处理器,来处理短信验证码认证成功和失败的情况。 ``` public class SmsCodeAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { super.onAuthenticationSuccess(request, response, authentication); } } ``` ``` public class SmsCodeAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { super.onAuthenticationFailure(request, response, exception); } } ``` 7. 编写控制器 编写控制器,处理短信验证码登录的请求。 ``` @Controller public class LoginController { private final static String SMS_LOGIN_PAGE = "sms-login"; @RequestMapping("/login/sms") public String smsLogin() { return SMS_LOGIN_PAGE; } @RequestMapping(value = "/login/sms", method = RequestMethod.POST) public void smsLogin(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { String mobile = request.getParameter("mobile"); String code = request.getParameter("code"); SmsCodeAuthenticationToken token = new SmsCodeAuthenticationToken(mobile, code); AuthenticationManager authenticationManager = new ProviderManager(Collections.singletonList(new SmsCodeAuthenticationProvider())); Authentication authentication = authenticationManager.authenticate(token); SecurityContextHolder.getContext().setAuthentication(authentication); request.getRequestDispatcher("/").forward(request, response); } } ``` 其中,`SmsCodeAuthenticationToken` 是短信验证码认证的 token 类型,需要单独实现。 以上就是实现自定义短信验证码登录的步骤。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农小胖哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值