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

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<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. 总结

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

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

(0)

相关推荐

  • SpringSecurity 自定义表单登录的实现

    本篇主要讲解 在SpringSecurity中 如何 自定义表单登录 , SpringSecurity默认提供了一个表单登录,但是实际项目里肯定无法使用的,本篇就主要讲解如何自定义表单登录 1.创建SpringSecurity项目 1.1 使用IDEA 先通过IDEA 创建一个SpringBoot项目 并且依赖SpringSecurity,Web依赖 此时pom.xml会自动添加 <dependency> <groupId>org.springframework.boot</

  • Spring Security保护用户密码常用方法详解

    1. 前言 本节将对 Spring Security 中的密码编码进行一些探讨. 2. 不推荐使用md5 首先md5 不是加密算法,是哈希摘要.以前通常使用其作为密码哈希来保护密码.由于彩虹表的出现,md5 和sha1之类的摘要算法都已经不安全了.如果有不相信的同学 可以到一些解密网站 如 cmd5 网站尝试解密 你会发现 md5 和 sha1 是真的非常容易被破解. 3. Spring Security中的密码算法 ObjectProvider<PasswordEncoder>参数.这里的P

  • spring security自定义登录页面

    在项目中我们肯定不能使用Spring自己生成的登录页面,而要用我们自己的登录页面,下面讲一下如何自定义登录页面,先看下配置 <sec:http auto-config="true"> <sec:intercept-url pattern="/app.jsp" access="ROLE_SERVICE"/> <sec:intercept-url pattern="/**" access="

  • Spring Security UserDetails实现原理详解

    1. 前言 今天开始我们来一步步窥探它是如何工作的.我们又该如何驾驭它.本篇将通过 Spring Boot 2.x 来讲解 Spring Security 中的用户主体UserDetails.以及从中找点乐子. 2. Spring Boot 集成 Spring Security 这个简直老生常谈了.不过为了照顾大多数还是说一下.集成 Spring Security 只需要引入其对应的 Starter 组件.Spring Security 不仅仅能保护Servlet Web 应用,也可以保护Rea

  • SpringSecurity自定义成功失败处理器的示例代码

    1. 新建SpringBoot工程 2. 项目依赖 <dependencies> <!-- security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- thymeleaf -->

  • spring security自定义认证登录的全过程记录

    spring security使用分类: 如何使用spring security,相信百度过的都知道,总共有四种用法,从简到深为: 1.不用数据库,全部数据写在配置文件,这个也是官方文档里面的demo: 2.使用数据库,根据spring security默认实现代码设计数据库,也就是说数据库已经固定了,这种方法不灵活,而且那个数据库设计得很简陋,实用性差: 3.spring security和Acegi不同,它不能修改默认filter了,但支持插入filter,所以根据这个,我们可以插入自己的f

  • 解析SpringSecurity自定义登录验证成功与失败的结果处理问题

    一.需要自定义登录结果的场景 在我之前的文章中,做过登录验证流程的源码解析.其中比较重要的就是 当我们登录成功的时候,是由AuthenticationSuccessHandler进行登录结果处理,默认跳转到defaultSuccessUrl配置的路径对应的资源页面(一般是首页index.html). 当我们登录失败的时候,是由AuthenticationfailureHandler进行登录结果处理,默认跳转到failureUrl配置的路径对应的资源页面(一般是登录页login.html). 但是

  • Spring Security实现不同接口安全策略方法详解

    1. 前言 欢迎阅读 Spring Security 实战干货 系列文章 .最近有开发小伙伴提了一个有趣的问题.他正在做一个项目,涉及两种风格,一种是给小程序出接口,安全上使用无状态的JWT Token:另一种是管理后台使用的是Freemarker,也就是前后端不分离的Session机制.用Spring Security该怎么办? 2. 解决方案 我们可以通过多次继承WebSecurityConfigurerAdapter构建多个HttpSecurity.HttpSecurity 对象会告诉我们

  • Spring Security如何优雅的增加OAuth2协议授权模式

    一.什么是OAuth2协议? OAuth 2.0 是一个关于授权的开放的网络协议,是目前最流行的授权机制. 数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据.系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用. 由于授权的场景众多,OAuth 2.0 协议定义了获取令牌的四种授权方式,分别是: 授权码模式:授权码模式(authorization code)是功能最完整.流程最严密的授权模式.它的特点就是通过客户端的后台服务器,与"服务提供商"的认

  • Spring Security自定义登录原理及实现详解

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

  • Spring Security自定义登录页面认证过程常用配置

    目录 一.自定义登录页面 1.编写登录页面 2.修改配置类 3.编写控制器 二. 认证过程其他常用配置 1.失败跳转 1.1编写页面 1.2修改表单配置 1.3添加控制器方法 1.4设置fail.html不需要认证 2.设置请求账户和密码的参数名 2.1源码简介 2.2修改配置 2.3修改页面 3.自定义登录成功处理器 3.1源码分析 3.2代码实现 4.自定义登录失败处理器 4.1源码分析 4.2代码实现 一.自定义登录页面 虽然Spring Security给我们提供了登录页面,但是对于实际

  • Spring Security动态权限的实现方法详解

    目录 1. 动态管理权限规则 1.1 数据库设计 1.2 实战 2. 测试 最近在做 TienChin 项目,用的是 RuoYi-Vue 脚手架,在这个脚手架中,访问某个接口需要什么权限,这个是在代码中硬编码的,具体怎么实现的,松哥下篇文章来和大家分析,有的小伙伴可能希望能让这个东西像 vhr 一样,可以在数据库中动态配置,因此这篇文章和小伙伴们简单介绍下 Spring Security 中的动态权限方案,以便于小伙伴们更好的理解 TienChin 项目中的权限方案. 1. 动态管理权限规则 通

  • Spring Security 多过滤链的使用详解

    目录 一.背景 二.需求 1.给客户端使用的api 2.给网站使用的api 三.实现方案 方案一: 方案二 四.实现 1.app 端 Spring Security 的配置 五.实现效果 1.app 有权限访问 api 2.app 无权限访问 api 3.admin 用户有权限访问 网站 api 4.dev 用户无权限访问 网站 api 六.完整代码 一.背景 在我们实际的开发过程中,有些时候可能存在这么一些情况,某些api 比如: /api/** 这些是给App端使用的,数据的返回都是以JSO

  • Spring MVC自定义日期类型转换器实例详解

    Spring MVC自定义日期类型转换器实例详解 WEB层采用Spring MVC框架,将查询到的数据传递给APP端或客户端,这没啥,但是坑的是实体类中有日期类型的属性,但是你必须提前格式化好之后返回给它们.说真的,以前真没这样做过,之前都是一口气查询到数据,然后在jsp页面上格式化,最后展示给用户.但是这次不同,这次我纯属操作数据,没有页面.直接从数据库拿数据给它们返数据.它们给我传数据我持久化数据,说到这里一个小问题就默默的来了. 首先把问题还原一下吧(这是一个数据导出功能),下图中用红框圈

  • Spring Boot自定义错误视图的方法详解

    Spring Boot缺省错误视图解析器 Web应用在处理请求的过程中发生错误是非常常见的情况,SpringBoot中为我们实现了一个错误视图解析器(DefaultErrorViewResolver).它基于一些常见的约定,尝试根据HTTP错误状态码解析出错误处理视图.它会在目录/error下针对提供的HTTP错误状态码搜索模板或者静态资源,比如,给定了HTTP状态码404,它会尝试搜索如下模板或者静态资源: /<templates>/error/404.<ext> - 这里<

  • Springboot集成Spring Security实现JWT认证的步骤详解

    1 简介 Spring Security作为成熟且强大的安全框架,得到许多大厂的青睐.而作为前后端分离的SSO方案,JWT也在许多项目中应用.本文将介绍如何通过Spring Security实现JWT认证. 用户与服务器交互大概如下: 客户端获取JWT,一般通过POST方法把用户名/密码传给server: 服务端接收到客户端的请求后,会检验用户名/密码是否正确,如果正确则生成JWT并返回:不正确则返回错误: 客户端拿到JWT后,在有效期内都可以通过JWT来访问资源了,一般把JWT放在请求头:一次

  • Spring中自定义数据类型转换的方法详解

    目录 类型转换服务 实现Converter接口 实现ConverterFactory接口 实现GenericConverter接口 环境:Spring5.3.12.RELEASE. Spring 3引入了一个core.onvert包,提供一个通用类型转换系统.系统定义了一个SPI来实现类型转换逻辑,以及一个API来在运行时执行类型转换.在Spring容器中,可以使用这个系统作为PropertyEditor实现的替代,将外部化的bean属性值字符串转换为所需的属性类型.还可以在应用程序中需要类型转

  • Spring Security过滤器链体系的实例详解

    以下摘自胖哥分享的 2022开工福利教程. 在学习Spring Security的时候你有没有下面这两个疑问: Spring Security的登录是怎么配置的? Spring Security的访问控制是什么机制? SpringBootWebSecurityConfiguration 上面两个疑问的答案就在配置类SpringBootWebSecurityConfiguration中.你可以按照下面这个思维导图去理解这个自动配置: SpringBootWebSecurityConfigurati

随机推荐