点击上方蓝色“程序猿DD”,选择“设为星标”

回复“资源”获取独家整理的学习资料!

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 表单登录

启用表单登录通过两种方式一种是通过 HttpSecurity 的 apply(C configurer) 方法自己构造一个 AbstractAuthenticationFilterConfigurer 的实现,这种是比较高级的玩法。 另一种是我们常见的使用 HttpSecurity 的 formLogin() 方法来自定义 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) 默认登陆成功后跳转到此 ,如果 alwaysUse 为 true 只要进行认证流程而且成功,会一直跳转到此。一般推荐默认值 false

  • successForwardUrl(String forwardUrl) 效果等同于上面 defaultSuccessUrl 的 alwaysUse 为 true 但是要注意 RequestMethod

  • successHandler(AuthenticationSuccessHandler successHandler) 自定义认证成功处理器,可替代上面所有的 success 方式

  • failureHandler(AuthenticationFailureHandler authenticationFailureHandler) 自定义失败成功处理器,可替代上面所有的 success 方式

  • 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 的搞法, 维护一个注册表执行不同的处理策略。当然我们要实现一个 GenericFilterBean 在 UsernamePasswordAuthenticationFilter 之前执行。同时制定登录的策略。

6.1 登录方式定义

定义登录方式枚举 ``。

   public enum LoginTypeEnum {

       /**        * 原始登录方式.        */       FORM,       /**        * Json 提交.        */       JSON,       /**        * 验证码.        */       CAPTCHA

   }

6.2 定义前置处理器接口

   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 。通过 HttpSecurity 的 addFilterBefore(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

本文通过OpenWrite的Markdown转换工具发布

关注我,回复“加群”加入各种主题讨论群

  • Spring Security 实战:Spring Boot 下的自动配置

  • Spring Security 实战:路径Uri中的 Ant 风格

  • Spring Security 实战:自定义配置类入口

  • Spring Security 实战:搞清楚 UserDetails

  • Spring Security 实战:登录成功后返回 JWT Token

朕已阅 

Spring Security 实战干货:玩转自定义登录相关推荐

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

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 1. 前言 上一篇对 Spring Security 所 ...

  2. Spring Security 实战干货:自定义异常处理

    Spring Security 实战干货:自定义异常处理 转自:https://www.cnblogs.com/felordcn/p/12142514.html 文章目录 1. 前言 2. Sprin ...

  3. Spring Security 实战干货:OAuth2登录获取Token的核心逻辑

    作者 | 码农小胖哥 来源 | https://mp.weixin.qq.com/s/zdTBdSVunqwVGx-spHjLjw 1. 前言 在上一篇Spring Security 实战干货:OAu ...

  4. Spring Security 实战干货: RBAC权限控制概念的理解

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 作者 | 码农小胖哥 来源 | 公众号「码农小胖哥」 1 ...

  5. Spring Security 实战干货:OAuth2授权回调的核心认证流程

    1. 前言 我们在上一篇 Spring Security 实战干货:OAuth2 授权回调的处理机制 对 OAuth2 服务端调用客户端回调的流程进行了图解, 今天我们来深入了解 OAuth2 在回调 ...

  6. Spring Security 实战干货:客户端OAuth2授权请求的入口在哪里

    1. 前言 在Spring Security 实战干货:OAuth2 第三方授权初体验一文中我先对 OAuth2.0 涉及的一些常用概念进行介绍,然后直接通过一个 DEMO 来让大家切身感受了 OAu ...

  7. Spring Security 实战干货:自定义配置类入口 WebSecurityConfigurerAdapter

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 1. 前言 今天我们要进一步的的学习如何自定义配置 Sp ...

  8. springsecurity 不允许session并行登录_Spring Security 实战干货:实现自定义退出登录...

    我是 码农小胖哥.天天有编程干货分享.觉得写的不错.点个赞,转发一下,关注一下.本文为个人原创文章,转载请注明出处,非法转载抄袭将追究其责任. 1. 前言 上一篇对 Spring Security 所 ...

  9. Spring Security 实战内容:实现自定义退出登录

    1. 前言 今天我们来实战如何安全退出应用程序. 2. 我们使用 Spring Security 登录后都做了什么 这个问题我们必须搞清楚!一般登录后,服务端会给用户发一个凭证.常见有以下的两种: 基 ...

最新文章

  1. Java实现文件复制的四种方式
  2. 步步深入:MySQL架构总览-gt;查询执行流程-gt;SQL解析顺序
  3. 19.C++-(=)赋值操作符、初步编写智能指针
  4. resultSet.next() 位置处报错:java.lang.OutOfMemoryError: Java heap space
  5. 基于麻雀算法优化的Tsallis相对熵图像多阈值分割 -附代码
  6. html站点地图怎么做,sitemap网站地图(站点地图)如何制作以及作用
  7. 设计院中心所工作流程 CAD广播电视工程工艺绘图
  8. 路由交换实验一——CISCO路由器的基本配置
  9. 2019第四次新生周赛——YZJ的牛肉干
  10. Python高级编程第2版_张亮 阿信(译)_人民邮电出版社_2017-10_v2_完整版
  11. 基于 ZNS 模式搭建并运行 FEMU
  12. 如何快速制作gif图片
  13. 这五本人气火爆的有声小说,能成为网络文学20年优秀有声作品吗?
  14. Web前端Lec12 - HTTP协议
  15. 快来开建春晚红包信息群吧!
  16. vb雅西高速计算机考试,2016年高中信息技术学业水平考试--VB程序复习题.doc
  17. Python输入一个字符串,输出其中每个字符的出现次数。要求使用标准库collotections中的Counter类...
  18. matlab弹钢琴卡农,【matlab】寂寞的理科生用matlab演奏卡农
  19. linux下在终端打开文件夹
  20. 520礼物清单、送男友实用礼物排行榜

热门文章

  1. linux 系统邮件 查看清空
  2. linux 用户空间文件系统 filesystem in userspace fuse 简介
  3. 线程同步----递归锁
  4. Android--一个好玩的应用程序/开机自启动
  5. OpenStack在dashboard界面点击管理员网络,服务器页面出错
  6. SSL/TLS 协议简介与实例分析
  7. matlab识别不出linux链接,在Ubuntu上,从matlab调用外部脚本失败_linux_开发99编程知识库...
  8. java 线程组和线程_Java多线程 线程组原理及实例详解
  9. 初中计算机实践研究计划,初中信息技术个人研修计划
  10. php+实现群发微信模板消息_php实现发送微信模板消息的方法,php信模板消息_PHP教程...