文档章节

Spring Security 实战干货:玩转自定义登录

码农小胖哥
 码农小胖哥
发布于 10/18 00:18
字数 2024
阅读 72
收藏 3

1. 前言

前面的关于 Spring Security 相关的文章只是一个预热。为了接下来更好的实战,如果你错过了请从 Spring Security 实战系列 开始。安全访问的第一步就是认证(Authentication),认证的第一步就是登录。今天我们要通过对 Spring Security 的自定义,来设计一个可扩展,可伸缩的 form 登录功能。

2. form 登录的流程

下面是 form 登录的基本流程:

只要是 form 登录基本都能转化为上面的流程。接下来我们看看 Spring Security 是如何处理的。

3. Spring Security 中的登录

昨天 Spring Security 实战干货:自定义配置类入口WebSecurityConfigurerAdapter 中已经讲到了我们通常的自定义访问控制主要是通过 HttpSecurity 来构建的。默认它提供了三种登录方式:

  • formLogin() 普通表单登录
  • oauth2Login() 基于 OAuth2.0 认证/授权协议
  • openidLogin() 基于 OpenID 身份认证规范

以上三种方式统统是 AbstractAuthenticationFilterConfigurer 实现的,

4. HttpSecurity 中的 form 表单登录

启用表单登录通过两种方式一种是通过 HttpSecurityapply(C configurer) 方法自己构造一个 AbstractAuthenticationFilterConfigurer 的实现,这种是比较高级的玩法。 另一种是我们常见的使用 HttpSecurityformLogin() 方法来自定义 FormLoginConfigurer 。我们先搞一下比较常规的第二种。

4.1 FormLoginConfigurer

该类是 form 表单登录的配置类。它提供了一些我们常用的配置方法:

  • loginPage(String loginPage) : 登录 页面而并不是接口,对于前后分离模式需要我们进行改造 默认为 /login
  • loginProcessingUrl(String loginProcessingUrl) 实际表单向后台提交用户信息的 Action,再由过滤器UsernamePasswordAuthenticationFilter 拦截处理,该 Action 其实不会处理任何逻辑。
  • usernameParameter(String usernameParameter) 用来自定义用户参数名,默认 username
  • passwordParameter(String passwordParameter) 用来自定义用户密码名,默认 password
  • failureUrl(String authenticationFailureUrl) 登录失败后会重定向到此路径, 一般前后分离不会使用它。
  • failureForwardUrl(String forwardUrl) 登录失败会转发到此, 一般前后分离用到它。 可定义一个 Controller (控制器)来处理返回值,但是要注意 RequestMethod
  • defaultSuccessUrl(String defaultSuccessUrl, boolean alwaysUse) 默认登陆成功后跳转到此 ,如果 alwaysUsetrue 只要进行认证流程而且成功,会一直跳转到此。一般推荐默认值 false
  • successForwardUrl(String forwardUrl) 效果等同于上面 defaultSuccessUrlalwaysUsetrue 但是要注意 RequestMethod
  • successHandler(AuthenticationSuccessHandler successHandler) 自定义认证成功处理器,可替代上面所有的 success 方式
  • failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定义失败成功处理器,可替代上面所有的 failure 方式
  • permitAll(boolean permitAll) form 表单登录是否放开

知道了这些我们就能来搞个定制化的登录了。

5. Spring Security 聚合登录 实战

接下来是我们最激动人心的实战登录操作。 有疑问的可认真阅读 Spring 实战 的一系列预热文章。

5.1 简单需求

我们的接口访问都要通过认证,登陆错误后返回错误信息(json),成功后前台可以获取到对应数据库用户信息(json)(实战中记得脱敏)。

我们定义处理成功失败的控制器:

 @RestController
 @RequestMapping("/login")
 public class LoginController {
     @Resource
     private SysUserService sysUserService;
 
     /**
      * 登录失败返回 401 以及提示信息.
      *
      * @return the rest
      */
     @PostMapping("/failure")
     public Rest loginFailure() {
 
         return RestBody.failure(HttpStatus.UNAUTHORIZED.value(), "登录失败了,老哥");
     }
 
     /**
      * 登录成功后拿到个人信息.
      *
      * @return the rest
      */
     @PostMapping("/success")
     public Rest loginSuccess() {
           // 登录成功后用户的认证信息 UserDetails会存在 安全上下文寄存器 SecurityContextHolder 中
         User principal = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
         String username = principal.getUsername();
         SysUser sysUser = sysUserService.queryByUsername(username);
         // 脱敏
         sysUser.setEncodePassword("[PROTECT]");
         return RestBody.okData(sysUser,"登录成功");
     }
 }

然后 我们自定义配置覆写 void configure(HttpSecurity http) 方法进行如下配置(这里需要禁用crsf):

 @Configuration
 @ConditionalOnClass(WebSecurityConfigurerAdapter.class)
 @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
 public class CustomSpringBootWebSecurityConfiguration {
 
     @Configuration
     @Order(SecurityProperties.BASIC_AUTH_ORDER)
     static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
         @Override
         protected void configure(AuthenticationManagerBuilder auth) throws Exception {
             super.configure(auth);
         }
 
         @Override
         public void configure(WebSecurity web) throws Exception {
             super.configure(web);
         }
 
         @Override
         protected void configure(HttpSecurity http) throws Exception {
             http.csrf().disable()
                     .cors()
                     .and()
                     .authorizeRequests().anyRequest().authenticated()
                     .and()
                     .formLogin()
                     .loginProcessingUrl("/process")
                     .successForwardUrl("/login/success").
                     failureForwardUrl("/login/failure");
 
         }
     }
 }

使用 Postman 或者其它工具进行 Post 方式的表单提交 http://localhost:8080/process?username=Felordcn&password=12345 会返回用户信息:

 {
     "httpStatus": 200,
     "data": {
         "userId": 1,
         "username": "Felordcn",
         "encodePassword": "[PROTECT]",
         "age": 18
     },
     "msg": "登录成功",
     "identifier": ""
 }

把密码修改为其它值再次请求认证失败后 :

  {
      "httpStatus": 401,
      "data": null,
      "msg": "登录失败了,老哥",
      "identifier": "-9999"
  }

6. 多种登录方式的简单实现

就这么完了了么?现在登录的花样繁多。常规的就有短信、邮箱、扫码 ,第三方是以后我要讲的不在今天范围之内。 如何应对想法多的产品经理? 我们来搞一个可扩展各种姿势的登录方式。我们在上面 2. form 登录的流程 中的 用户判定 之间增加一个适配器来适配即可。 我们知道这个所谓的 判定就是 UsernamePasswordAuthenticationFilter

我们只需要保证 uri 为上面配置的/process 并且能够通过 getParameter(String name) 获取用户名和密码即可

我突然觉得可以模仿 DelegatingPasswordEncoder 的搞法, 维护一个注册表执行不同的处理策略。当然我们要实现一个 GenericFilterBeanUsernamePasswordAuthenticationFilter 之前执行。同时制定登录的策略。

6.1 登录方式定义

定义登录方式枚举 ``。


  public enum LoginTypeEnum {
  
      /**
       * 原始登录方式.
       */
      FORM,
      /**
       * Json 提交.
       */
      JSON,
      /**
       * 验证码.
       */
      CAPTCHA
  
  }

6.2 定义前置处理器接口

定义前置处理器接口用来处理接收的各种特色的登录参数 并处理具体的逻辑。这个借口其实有点随意 ,重要的是你要学会思路。我实现了一个 默认的 form' 表单登录 和 通过RequestBody放入json` 的两种方式,篇幅限制这里就不展示了。具体的 DEMO 参见底部。

   public interface LoginPostProcessor {
   
   
   
       /**
        * 获取 登录类型
        *
        * @return the type
        */
       LoginTypeEnum getLoginTypeEnum();
   
       /**
        * 获取用户名
        *
        * @param request the request
        * @return the string
        */
       String obtainUsername(ServletRequest request);
   
       /**
        * 获取密码
        *
        * @param request the request
        * @return the string
        */
       String obtainPassword(ServletRequest request);
   
   }

6.3 实现登录前置处理过滤器

该过滤器维护了 LoginPostProcessor 映射表。 通过前端来判定登录方式进行策略上的预处理,最终还是会交给 UsernamePasswordAuthenticationFilter 。通过 HttpSecurityaddFilterBefore(preLoginFilter, UsernamePasswordAuthenticationFilter.class)方法进行前置。

 package cn.felord.spring.security.filter;
 
 import cn.felord.spring.security.enumation.LoginTypeEnum;
 import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
 import org.springframework.security.web.util.matcher.RequestMatcher;
 import org.springframework.util.Assert;
 import org.springframework.util.CollectionUtils;
 import org.springframework.web.filter.GenericFilterBean;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 
 import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY;
 import static org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY;
 
 /**
  * 预登录控制器
  *
  * @author Felordcn
  * @since 16 :21 2019/10/17
  */
 public class PreLoginFilter extends GenericFilterBean {
 
 
     private static final String LOGIN_TYPE_KEY = "login_type";
 
 
     private RequestMatcher requiresAuthenticationRequestMatcher;
     private Map<LoginTypeEnum, LoginPostProcessor> processors = new HashMap<>();
 
 
     public PreLoginFilter(String loginProcessingUrl, Collection<LoginPostProcessor> loginPostProcessors) {
         Assert.notNull(loginProcessingUrl, "loginProcessingUrl must not be null");
         requiresAuthenticationRequestMatcher = new AntPathRequestMatcher(loginProcessingUrl, "POST");
         LoginPostProcessor loginPostProcessor = defaultLoginPostProcessor();
         processors.put(loginPostProcessor.getLoginTypeEnum(), loginPostProcessor);
 
         if (!CollectionUtils.isEmpty(loginPostProcessors)) {
             loginPostProcessors.forEach(element -> processors.put(element.getLoginTypeEnum(), element));
         }
 
     }
 
 
     private LoginTypeEnum getTypeFromReq(ServletRequest request) {
         String parameter = request.getParameter(LOGIN_TYPE_KEY);
 
         int i = Integer.parseInt(parameter);
         LoginTypeEnum[] values = LoginTypeEnum.values();
         return values[i];
     }
 
 
     /**
      * 默认还是Form .
      *
      * @return the login post processor
      */
     private LoginPostProcessor defaultLoginPostProcessor() {
         return new LoginPostProcessor() {
 
 
             @Override
             public LoginTypeEnum getLoginTypeEnum() {
 
                 return LoginTypeEnum.FORM;
             }
 
             @Override
             public String obtainUsername(ServletRequest request) {
                 return request.getParameter(SPRING_SECURITY_FORM_USERNAME_KEY);
             }
 
             @Override
             public String obtainPassword(ServletRequest request) {
                 return request.getParameter(SPRING_SECURITY_FORM_PASSWORD_KEY);
             }
         };
     }
 
 
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
         ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper((HttpServletRequest) request);
         if (requiresAuthenticationRequestMatcher.matches((HttpServletRequest) request)) {
 
             LoginTypeEnum typeFromReq = getTypeFromReq(request);
 
             LoginPostProcessor loginPostProcessor = processors.get(typeFromReq);
 
 
             String username = loginPostProcessor.obtainUsername(request);
 
             String password = loginPostProcessor.obtainPassword(request);
 
 
             parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_USERNAME_KEY, username);
             parameterRequestWrapper.setAttribute(SPRING_SECURITY_FORM_PASSWORD_KEY, password);
 
         }
 
         chain.doFilter(parameterRequestWrapper, response);
 
 
     }
 }

6.4 验证

通过 POST 表单提交方式 http://localhost:8080/process?username=Felordcn&password=12345&login_type=0 可以请求成功。或者以下列方式也可以提交成功:

更多的方式 只需要实现接口 LoginPostProcessor 注入 PreLoginFilter

7. 总结

今天我们通过各种技术的运用实现了从简单登录到可动态扩展的多种方式并存的实战运用。相信对你来说会有不小的收货 ,本次 **代码DEMO可通过关注公众号:Felordcn 回复 ss03 获取,后面会更加精彩。

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

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

© 著作权归作者所有

码农小胖哥

码农小胖哥

粉丝 55
博文 102
码字总数 118910
作品 1
郑州
程序员
私信 提问
Spring Security 实战干货: 登录后返回 JWT Token

前言 欢迎阅读 Spring Security 实战干货 系列文章,上一文 我们实现了 JWT 工具。本篇我们将一起探讨如何将 JWT 与 Spring Security 结合起来,在认证成功后不再跳转到指定页面而是直接返回...

码农小胖哥
10/28
1K
3
Spring Security 实战干货:实现自定义退出登录

前言 上一篇对 Spring Security 所有内置的 Filter 进行了介绍。今天我们来实战如何安全退出应用程序。 2. 我们使用 Spring Security 登录后都做了什么 这个问题我们必须搞清楚!一般登录后,...

码农小胖哥
10/23
83
0
Spring Security 实战干货:必须掌握的一些内置 Filter

前言 上一文我们使用 Spring Security 实现了各种登录聚合的场面。其中我们是通过在 之前一个自定义的过滤器实现的。我怎么知道自定义过滤器要加在 之前。我在这个系列开篇说了 Spring Secur...

码农小胖哥
10/22
114
0
Spring Security 实战干货:自定义配置类入口WebSecurityConfigurerAdapter

前言 今天我们要进一步的的学习如何自定义配置 Spring Security 我们已经多次提到了 ,而且我们知道 Spring Boot 中的自动配置实际上是通过自动配置包下的 总配置类上导入的 Spring Boot We...

码农小胖哥
10/16
116
0
Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密...

小致Daddy
2018/08/03
23.7K
1

没有更多内容

加载失败,请刷新页面

加载更多

Vue.js学习笔记2 - better-scroll滚动条

better-scroll滚动条 使用作者自制的better-scroll库,实现内容的滚动。 先在package.json加上依赖: "better-scroll": "^0.1.7" 接着再npm install安装依赖。 import BScroll from 'better-......

swanf
今天
7
0
设计模式之适配器模式

定义 将一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工 作。 UML类图 适配器分为两种,类适配器与对象适配器。 类适配器的UML图...

陈年之后是青葱
今天
8
0
教你玩转Linux—磁盘管理

导读 Linux磁盘管理好坏直接关系到整个系统的性能问题,Linux磁盘管理常用三个命令为df、du和fdisk。 df df命令参数功能:检查文件系统的磁盘空间占用情况。可以利用该命令来获取硬盘被占用了...

问题终结者
今天
11
0
KMP

字符串匹配算法 针对被匹配字段生产一个部分匹配表 A B C D A B D 0 0 0 0 1 2 0 部分匹配表 熟悉前缀与后缀的概念 ,“部分匹配表” 的生产就是根据前缀、后缀的最苍的共有元素的长度 前缀:...

鬼才王
昨天
6
0
快速搭建Jenkins集群

关于Jenkins集群 在Jenkins上同时执行多个任务时,单机性能可能达到瓶颈,使用Jenkins集群可以有效的解决此问题,让多台机器同时处理这些任务可以将压力分散,对单机版Jenkins的单点故障的隐...

程序员欣宸
昨天
13
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部