spring security——短信验证码登录(四)

article/2025/9/15 16:18:08

一、导读

        短信登录和用户名密码登录的逻辑是不同的,Spring Security 框架中实现的是用户名密码的登录方式。现在我们就模仿它的原理来加入短信登录的认证(注意不是验证),实现右边的

之前写的图形验证码是在 UsernamePasswordAuthenticationFilter前增加了我们自己的图形验证过滤器,验证成功之后再交给用户名和密码进行认证,调用userDetailsService进行匹配验证。最后通过的话,会进入Authentication已认证流程。短信认证的思路和上面一样:

  • SmsCodeAuthenticationFilter 短信登录请求
  • SmsCodeAuthenticationProvider 提供短信登录处理的实现类
  • SmsCodeAuthenticationToken 存放认证信息(包括未认证前的参数信息传递)
  • 最后开发一个过滤器放在 短信登录请求之前,进行短信验证码的验证,

因为这个过滤器只关心提交的验证码是否正常就行了。所以可以应用到任意业务中,对任意业务提交进行短信的验证。

二、开发短信登录功能

1、流程开发

        我们首先创建一个SmsCodeAuthenticationToken ,用来产生身份验证令牌。直接复制参考 UsernamePasswordAuthenticationToken 的写法,分析哪些需要哪些是不需要的,稍微修改一下即可(代码都放在core中)。

/*** 类名称 : SmsCodeAuthenticationToken* 功能描述 :手机短信登陆认证令牌* 创建时间 : 2018/11/12 18:56* -----------------------------------*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
​private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
​//存放用户名 : credentials 字段去掉,因为短信认证在授权认证前已经过滤了private final Object principal;
​
​/** 功能描述:创建用户名密码身份验证令牌需要使用此构造函数* 返回值:通过身份验证的代码返回false*/public SmsCodeAuthenticationToken(String mobile) {super(null);this.principal = mobile;setAuthenticated(false);}
​/** 功能描述:产生身份验证令牌*/public SmsCodeAuthenticationToken(Object principal,Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true); // must use super, as we override}
​public Object getCredentials() {return null;}public Object getPrincipal() {return this.principal;}
​public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("无法将此令牌设置为可信使用构造函数,该构造函数将接受一个已授予的权限列表");}
​super.setAuthenticated(false);}
​@Overridepublic void eraseCredentials() {super.eraseCredentials();}
}

然后是手机短信认证登陆过滤器 SmsCodeAuthenticationFilter,仿照的是UsernamePasswordAuthenticationFilter

/*** 类名称 : SmsCodeAuthenticationFilter* 功能描述 :手机短信认证登陆过滤器* -----------------------------------*/
@Slf4j
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
​
​//发送短信验证码 或 验证短信验证码时,传递手机号的参数的名称[mobile]private String mobileParameter = SecurityConstant.DEFAULT_MOBILE_PARAMETER;
​private boolean postOnly = true;
​public SmsCodeAuthenticationFilter() {// 拦截该路径,如果是访问该路径,则标识是需要短信登录super(new AntPathRequestMatcher(SecurityConstant.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "POST"));}
​@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if (postOnly && !request.getMethod().equals("POST")){throw new AuthenticationServiceException("不支持该认证方法: " + request.getMethod());}
​String mobile = obtainMobile(request);if (mobile == null){mobile = "";}mobile = mobile.trim();
​SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
​// Allow subclasses to set the "details" property//把request里面的一些信息copy近token里面。后面认证成功的时候还需要copy这信息到新的tokensetDetails(request, authRequest);
​return this.getAuthenticationManager().authenticate(authRequest);}
​
​/** 功能描述:提供身份验证请求的详细属性* 入参:[request 为此创建身份验证请求, authRequest 详细信息集的身份验证请求对象]*/protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}
​
​/** 功能描述:设置用于获取用户名的参数名的登录请求。* 入参:[usernameParameter 默认为“用户名”。]*/public void setMobileParameter(String mobileParameter) {Assert.hasText(mobileParameter, "手机号参数不能为空");this.mobileParameter = mobileParameter;}
​
​/** 功能描述:获取手机号*/protected String obtainMobile(HttpServletRequest request) {return request.getParameter(this.mobileParameter);}
​/** 功能描述:定义此筛选器是否只允许HTTP POST请求。如果设置为true,则接收不到POST请求将立即引发异常并不再继续身份认证*/public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}
​public final String getMobileParameter() {return mobileParameter;}
}

接下来实现短信处理器SmsCodeAuthenticationProvider,用于匹配用户信息,如果认证成功加入到认证成功队列。这个没有找到仿照的地方。没有发现和usernamePassword类型的提供provider

/*** 类名称 : SmsCodeAuthenticationProvider* 功能描述 :短信处理器,查询用户信息,成功存放到已认证token* -----------------------------------*/
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
​private UserDetailsService userDetailsService;
​@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {
​SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
​//看下面UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
​if (user == null) {throw new InternalAuthenticationServiceException("无法获取用户信息");}
​SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());//需要把未认证中的一些信息copy到已认证的token中authenticationResult.setDetails(authenticationToken.getDetails());
​return authenticationResult;}
​@Overridepublic boolean supports(Class<?> authentication) {return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);}
​
}

但是我们看上面通过token获取用户信息部分

UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

我们查看userDetailService接口

public interface UserDetailsService {UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

只有根据username加载的接口,如果我们系统中只有一种登录实现方式的话是没问题的,比如portal系统中只支持用户名密码登录或者只支持短信验证码登录,我们只要在UserDetailsService实现中进行相应处理即可。但是两种方式都支持的话,就必须解决该问题了。我们对UserDetailsService进行扩展

public interface TinUserDetailsService extends UserDetailsService {UserDetails loadUserByMobile(String mobile) throws UsernameNotFoundException;
}

然后我们把上面的UserDetailsService替换成TinUserDetailsService即可

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
​SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
​if (userDetailsService instanceof TinUserDetailsService){
​TinUserDetailsService tinUserDetailsService = (TinUserDetailsService) userDetailsService;
​UserDetails user = tinUserDetailsService.loadUserByMobile((String) authenticationToken.getPrincipal());
​if (user == null) {throw new InternalAuthenticationServiceException("无法获取用户信息");}
​SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());//需要把未认证中的一些信息copy到已认证的token中authenticationResult.setDetails(authenticationToken.getDetails());
​return authenticationResult;}else {throw new InternalAuthenticationServiceException("请实现TinUserDetailsService#loadUserByMobile方法");}
}

这里之所以不直接将UserDetailsService替换成TinUserDetailsService,是为了不影想当不使用手机验证码登录时能正常实现UserDetailsService。这样我们在portal中的实现类就可以分开编写了。

public class UserDetailsServiceImpl implements TinUserDetailsService {
​@Autowiredprivate UserRepository userRepository;
​/** 功能描述:用户名密码登陆*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {log.info("表单登录用户名:" + username);}
​/** 功能描述:手机验证码登陆*/@Overridepublic UserDetails loadUserByMobile(String mobile) {log.info("表单登录手机号:" + mobile);}
}

2、加入到security的认证流程

        需要的几个东西已经准备好了,这里要进行配置把这些加入到 security的认证流程中去。创建SmsCodeAuthenticationSecurityConfig

@Component
public class SmsCodeAuthenticationSecurityConfigextends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler;@Autowiredprivate AuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate UserDetailsService userDetailsService;
​@Overridepublic void configure(HttpSecurity http) throws Exception {SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();// 把该过滤器交给管理器filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));filter.setAuthenticationFailureHandler(authenticationFailureHandler);filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
​SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
​http// 注册到AuthenticationManager中去.authenticationProvider(smsCodeAuthenticationProvider)// 添加到 UsernamePasswordAuthenticationFilter 之后// 貌似所有的入口都是 UsernamePasswordAuthenticationFilter// 然后UsernamePasswordAuthenticationFilter的provider不支持这个地址的请求// 所以就会落在我们自己的认证过滤器上。完成接下来的认证.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);}
}

这里我们要注意:图上流程,因为最先走的短信认证的过滤器(不是验证码,只是认证)。要使用管理器来获取provider,所以把管理器注册进去。

3、应用方配置

        这里是browser的BrowserSecurityConfig。变化的配置用注释标出来了,无变化的把注释去掉了。

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate SecurityProperties securityProperties;
​@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}
​@Autowiredprivate MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;@Autowiredprivate MyAuthenticationFailureHandler myAuthenticationFailureHandler;
​@Autowiredprivate DataSource dataSource;@Autowiredprivate PersistentTokenRepository persistentTokenRepository;@Autowiredprivate UserDetailsService userDetailsService;
​// 由下面的  .apply(smsCodeAuthenticationSecurityConfigs)方法添加这个配置@Autowiredprivate SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;
​@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);return jdbcTokenRepository;}
​@Overrideprotected void configure(HttpSecurity http) throws Exception {ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);validateCodeFilter.setSecurityProperties(securityProperties);validateCodeFilter.afterPropertiesSet();
​// 短信的是copy图形的过滤器,这里直接copy初始化SmsCodeFilter smsCodeFilter = new SmsCodeFilter();smsCodeFilter.setFailureHandler(myAuthenticationFailureHandler);smsCodeFilter.setSecurityProperties(securityProperties);smsCodeFilter.afterPropertiesSet();http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)// 在这里不能注册到我们自己的短信认证过滤器上,会报错,注意和验证码的顺序.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class).formLogin().loginPage("/authentication/require").loginProcessingUrl("/authentication/form").successHandler(myAuthenticationSuccessHandler).failureHandler(myAuthenticationFailureHandler).and().rememberMe().tokenRepository(persistentTokenRepository).tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()).userDetailsService(userDetailsService).and().authorizeRequests().antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage(),"/code/*","/error").permitAll().anyRequest().authenticated().and().csrf().disable()// 这里应用短信认证配置.apply(smsCodeAuthenticationSecurityConfigs);}
}

我们再看一下登录页面的表单的提交代码,/authentication/mobile登录地址,就是我们认证过滤器里面的支持地址(在browser中)。

<h3>短信验证码</h3>
<form action="/authentication/mobile" method="post"><table><tr><td>手机号:</td><td><input type="text" name="mobile"></td></tr><tr><td>短信验证码:</td><td><input type="text" name="smsCode"><a href="/code/sms?mobile=13012345678">发送验证码</a></td></tr><tr><td><button type="submit">登录</button></td></tr></table>
</form>

然后我们就可以访问登录页面,点击发送短信验证码,然后在后台复制真正发送的验证码添加,提交短信登录,进行测试。自定义认证逻辑就完成了,大致步骤就是:

  1. 入口配置 应用方使用该配置 .apply(smsCodeAuthenticationSecurityConfigs)
  2. 提供处理过滤器 ProcessingFilter 并限制该过滤器支持拦截的url
  3. 提供AuthenticationProvider 进行认证的处理支持
  4. 把ProviderManager 赋值给 ProcessingFilter
  5. 把AuthenticationProvider注册到AuthenticationManager中去(这里完成ProcessingFilter调用管理器查找Provider,完成认证这个过程)
  6. 把 ProcessingFilter 添加到 认证处理链中 ,之后(也就是UsernamePasswordAuthenticationFilter)

现在讲一下关于验证码,我们发现上面的处理流程其实和短信验证码没有关系,只是验证手机号信息。但事实上我们已经编写了短信验证码的逻辑,我们只需要在入口配置中( 应用方)把验证码(验证是否有效,是否过期)的过滤器添加到认证处理链之前(也就是UsernamePasswordAuthenticationFilter),就是在进入认证之前先把验证码是否有效验证了,那么在进行身份认证的过程中其实是无需关注验证码的。


http://chatgpt.dhexx.cn/article/bWk9W1eX.shtml

相关文章

雅克比矩阵求导推导

首先&#xff0c;引入雅克比矩阵公式&#xff1a; J [ u 1 u 2 ⋯ u n e 1 e 2 ⋯ e n ] \mathbf{J}\left[\begin{array}{c} \begin{array}{lll}\mathbf{u}_{1} & \mathbf{u}_{2} & \cdots & \mathbf{u}_{n}\end{array} \\ \begin{array}{lll}\mathbf{e}_{1} &…

矩阵求导公式

转自&#xff1a;http://blog.sina.com.cn/s/blog_4a033b090100pwjq.html 求导公式(撇号为转置&#xff09;&#xff1a; Y A * X --> DY/DX A Y X * A --> DY/DX A Y A * X * B --> DY/DX A * B Y A * X * B --> DY/DX B * A 乘积的导数 d(f*g)/dx(df/dx)…

矩阵求导法则的总结

最重要的写在前面&#xff1a; 进行更新&#xff1a;随着理解的加深&#xff0c;我发现之前写的有些问题&#xff0c;重新写一下吧 矩阵求导要分成两类&#xff0c;第一类是用在向量函数f(x)里&#xff0c;比如要在x0处展开&#xff0c;所以需要计算该点处的雅可比这些&#…

-矩阵求导-

一、为什么需要矩阵求导 参数的向量化&#xff1a; 向量化会使代码、式子更加简洁&#xff1b; 使用向量化代替for循环&#xff0c;向量化能够加速你的代码&#xff1b;求导在优化算法中的广泛应用&#xff1a; 优化算法需要反向传播&#xff0c;需要对参数矩阵进行求导 二、…

矩阵求导简析

大家好&#xff0c;这是我的第一篇博客。 矩阵求导&#xff08;Matrix Derivation&#xff0c;或者Matrix Differential&#xff09;&#xff0c;在机器学习、图像处理、最优化领域经常会遇到。其本质是多元变量的微积分&#xff0c;只是把求导应用在了矩阵上&#xff0c;不同在…

矩阵求导法则

转载自&#xff1a;https://blog.csdn.net/dinkwad/article/details/72819832 矩阵求导的技术&#xff0c;在统计学、控制论、机器学习等领域有广泛的应用。鉴于我看过的一些资料或言之不详、或繁乱无绪&#xff0c;本文来做个科普&#xff0c;分作两篇&#xff0c;上篇讲标量…

矩阵求导总结

文章目录 1.分子为标量1.1 ∂ 标量 ∂ 标量 \frac{\partial 标量}{\partial标量} ∂标量∂标量​1.2 ∂ 标量 ∂ 向量 \frac{\partial 标量}{\partial向量} ∂向量∂标量​1.3 ∂ 标量 ∂ 矩阵 \frac{\partial 标量}{\partial矩阵} ∂矩阵∂标量​ 2.分子为向量2.1 ∂ 向量…

矩阵求导方法

矩阵求导方法 在机器学习过程中&#xff0c;我们经常会对矩阵进行相关的操作&#xff0c;现对矩阵求导方法进行概况与推导。 首先总结矩阵求导的本质&#xff0c;即矩阵A中每个元素对矩阵B中每个元素求导。我们先从宏观上理解这个公式&#xff0c;即从求导后元素的个数来理解…

矩阵求导

一、矩阵和向量求导 参考地址&#xff1a;https://cloud.tencent.com/developer/article/1668818 求导公式大全&#xff1a;https://cloud.tencent.com/developer/article/1551901 机器学习中最常用的矩阵求导有&#xff1a;标量对矩阵的求导&#xff0c;矩阵对标量求导以…

矩阵求导、几种重要的矩阵及常用的矩阵求导公式

一、矩阵求导 一般来讲&#xff0c;我们约定x(x1,x2,...xN)Tx(x1,x2,...xN)T&#xff0c;这是分母布局。常见的矩阵求导方式有&#xff1a;向量对向量求导&#xff0c;标量对向量求导&#xff0c;向量对标量求导。 1、向量对向量求导 2、标量对向量求导 3、向量对标量求导 其他…

矩阵求导(本质、原理与推导)详解

矩阵求导是机器学习与深度学习的基础&#xff0c;它是高等数学、线性代数知识的综合&#xff0c;并推动了概率论与数理统计向多元统计的发展。在一般的线性代数的课程中&#xff0c;很少会提到矩阵导数的概念&#xff1b;而且在网上寻找矩阵求导的知识点&#xff0c;也是五花八…

矩阵求导常用公式

矩阵求导常用公式 1 引言2 向量的导数2.1 向量对标量求导 Vector-by-scalar2.2 标量对向量求导 Scalar-by-vector2.3 向量对向量求导 Vector-by-vector 3 矩阵的导数3.1 矩阵对标量求导 Matrix-by-scalar3.2 标量对矩阵求导 Scalar-by-matrix 4 常用求导公式4.1 向量对向量求导…

矩阵的求导

目录 1 布局(Layout) 1.1 矩阵向量求导引入 1.2 矩阵向量求导定义 1.3 矩阵向量求导布局 1.4 分母布局的另一种求解方法 1.5 总结 2 基本的求导规则 2.1 向量对标量求导&#xff08;相对于数量变量的微分&#xff0c;即自变量是数量变量&#xff09; 2.1.1 定义 2.1.…

JAVA—IO流详解

1. 流概述 1.1. 什么是IO IO:Input/Output即输入&输出&#xff0c;也称之为流&#xff08;河流&#xff0c;水流&#xff09;&#xff0c;指的是数据从一个地点到另一个地点过程&#xff1b;对于计算机来说文件的拷贝过程&#xff0c;文件的编写保存&#xff0c;显示功能…

Java高级特性-IO流

文章目录 前言一、什么是IO流二、字节流-输入输出三、字符流 - 输入输出四、复制文件五、总结 前言 在Java开发中我们经常会涉及到文件方面的操作&#xff0c;不论是网站的上传文件&#xff0c;还是服务器提供给客户端下载文件&#xff0c;这些都需要我们来处理&#xff0c;那J…

java IO流之BufferedReader和BufferedWriter

前言&#xff1a; &#x1f44f;作者简介&#xff1a;我是笑霸final&#xff0c;一名热爱技术的在校学生。 &#x1f4dd;个人主页&#xff1a;笑霸final的主页 &#x1f4d5;系列专栏&#xff1a;&#xff1a;本文写在java专栏 &#x1f4e7;如果文章知识点有错误的地方&#…

IO流详解

文章目录 IO流IO流分类FileInputStream输入流框架相对路径读取过程详解available()skip(long n) FileOutputStream输入流框架 FileReaderFileWriter文件复制FileInputStream 、FileOutputStreamFileReader 、FileWriter 缓冲流BufferedReader 数据专属流DataOutputStreamDataIn…

java中的io流

文章目录 1. IO流&#xff0c;什么是IO&#xff1f;2.IO流的分类&#xff1f;3.IO流都在java.io.*包下4.java IO流有四个家族5.java.io包下需要掌握的流有16个6.FileInputStream的1个字节读入法7.FileInputStream的byte[]读入法8.FileInputStream的其他方法9.FileOutputStream用…

Java读写IO流详解

一、IO流要明确的几点 &#xff08;1&#xff09;明确要操作的数据是数据源还是数据目的&#xff08;是读还是写&#xff09; 源/读 InputStream Reader 目的/写 OutputStream Writer &#xff08;2&#xff09;明确操作设备上的数据是字节还是文本 源/读 字节&…

python IO流

python io流 总的来说就说计算机输入和输出的操作&#xff0c;一般来说是内存与磁盘之间的输入输出 一种虚拟世界的物质。 IO流操作是一种持久化操作&#xff0c;是将数据持久化在磁盘上。 这里用了open函数 函数的第一个引号内放的是运行的python文件目录下的一个名为1.txt的…