热门标签 | HotTags
当前位置:  开发笔记 > 前端 > 正文

SpringSecurity自定义登录原理及实现详解

这篇文章主要介绍了SpringSecurity自定义登录原理及实现详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

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 映射表。 通过前端来判定登录方式进行策略上的预处理,最终还是会交给

 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 processors = new HashMap<>();

   public PreLoginFilter(String loginProcessingUrl, Collection 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&#63;username=Felordcn&password=12345&login_type=0 可以请求成功。或者以下列方式也可以提交成功:

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

7. 总结

今天我们通过各种技术的运用实现了从简单登录到可动态扩展的多种方式并存的实战运用。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。


推荐阅读
  • 如何在服务器主机上实现文件共享的方法和工具
    本文介绍了在服务器主机上实现文件共享的方法和工具,包括Linux主机和Windows主机的文件传输方式,Web运维和FTP/SFTP客户端运维两种方式,以及使用WinSCP工具将文件上传至Linux云服务器的操作方法。此外,还介绍了在迁移过程中需要安装迁移Agent并输入目的端服务器所在华为云的AK/SK,以及主机迁移服务会收集的源端服务器信息。 ... [详细]
  • 前端跨域访问后端数据的方法
    参考链接:https:mp.weixin.qq.coms4G_27oRLSMMYBFvtYZgqcg一、什么是跨域当两个域名的协议、子域名、主域名、端口号中有任意一个不 ... [详细]
  • 深入浅出JWT
    JWT(JSONWEBTOKEN)的组成https:jwt.ioheader(头部)承载两部分信息:声明 ... [详细]
  • HDIV简介一个简单又强大的安全框架
    为什么80%的码农都做不了架构师?惯例官方纯英文档:https:hdivsecurity.comtechnical-documentationdo ... [详细]
  • t-io 2.0.0发布-法网天眼第一版的回顾和更新说明
    本文回顾了t-io 1.x版本的工程结构和性能数据,并介绍了t-io在码云上的成绩和用户反馈。同时,还提到了@openSeLi同学发布的t-io 30W长连接并发压力测试报告。最后,详细介绍了t-io 2.0.0版本的更新内容,包括更简洁的使用方式和内置的httpsession功能。 ... [详细]
  • SpringBoot整合SpringSecurity+JWT实现单点登录
    SpringBoot整合SpringSecurity+JWT实现单点登录,Go语言社区,Golang程序员人脉社 ... [详细]
  • GSIOpenSSH PAM_USER 安全绕过漏洞
    漏洞名称:GSI-OpenSSHPAM_USER安全绕过漏洞CNNVD编号:CNNVD-201304-097发布时间:2013-04-09 ... [详细]
  • Django + Ansible 主机管理(有源码)
    本文给大家介绍如何利用DjangoAnsible进行Web项目管理。Django介绍一个可以使Web开发工作愉快并且高效的Web开发框架,能够以最小的代价构建和维护高 ... [详细]
  • 本文由编程笔记#小编为大家整理,主要介绍了markdown[软件代理设置]相关的知识,希望对你有一定的参考价值。 ... [详细]
  • DockerDataCenter系列(四)-离线安装UCP和DTR,Go语言社区,Golang程序员人脉社 ... [详细]
  • 背景后端使用Nginx并更改本地host文件,起本地服务。将aaa.bbbb.com代理至本地IP地址(10.26.36.156)。使用$.ajax调用后端restful接口,要求 ... [详细]
  • 这座城市多了十只伤心的鸽
    这个作业属于哪个课程2021春软件工程实践|W班(福州大学)这个作业要求在哪里团队第四次作业这个作业的目标设计项目原型、制作项目需求规格说明书团队名称这座城市多了十只伤心的鸽其他参 ... [详细]
  • 很多同学对热备,冷备,云备了解不深,我科普一下IT行业各种备份术语。以后别闹笑话了。假设你是一位女性,你有一位男朋友&#x ... [详细]
  • Python爬取小姐姐内衣信息,寻找妹纸们的偏好
    今天继续来分析爬虫数据分析文章,一起来看看网易严选商品评论的获取和分析。警告:本教程仅用作学习交流,请勿用作商业盈利,违者后果自负!如本文有侵犯任何组织集团 ... [详细]
  • 白帽子讲Web安全读书笔记
    Part1:安全的发展,或者说,黑客的发展黑客是什么?互联网本来是安全的,自从有了研究安全的人之后,互联网就变得不安全了。“root”对黑客的吸引,就像大米对老鼠,美女对色狼的吸引。不想拿到“root ... [详细]
author-avatar
Sunshine丶米粉_499
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有