在Spring Security添加图形验证码中,我们已经实现了基于Spring Boot + Spring Security的账号密码登录,并集成了图形验证码功能。时下另一种非常常见的网站登录方式为手机短信验证码登录,但Spring Security默认只提供了账号密码的登录认证逻辑,所以要实现手机短信验证码登录认证功能,我们需要模仿Spring Security账号密码登录逻辑代码来实现一套自己的认证逻辑。

1. 短信验证码生成

我们在Spring Security添加图形验证码的基础上来集成短信验证码登录的功能。

和图形验证码类似,我们先定义一个短信验证码对象SmsCode:

@Data
public class SmsCode {private String code;private LocalDateTime expireTime;public SmsCode(String code, int expireIn) {this.code = code;this.expireTime = LocalDateTime.now().plusSeconds(expireIn);}public SmsCode(String code, LocalDateTime expireTime) {this.code = code;this.expireTime = expireTime;}public boolean isExpire() {return LocalDateTime.now().isAfter(expireTime);}
}

SmsCode对象包含了两个属性:code验证码和expireTime过期时间。isExpire方法用于判断短信验证码是否已过期。

接着在ValidateCodeController中加入生成短信验证码相关请求对应的方法:

@RestController
public class ValidateController {public final static String SESSION_KEY_SMS_CODE = "SESSION_KEY_SMS_CODE";private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@GetMapping("/code/sms")public void createSmsCode(HttpServletRequest request, HttpServletResponse response, String mobile) throws IOException {SmsCode smsCode = createSMSCode();sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_SMS_CODE + mobile, smsCode);// 输出验证码到控制台代替短信发送服务System.out.println("您的登录验证码为:" + smsCode.getCode() + ",有效时间为60秒");}private SmsCode createSMSCode() {String code = RandomStringUtils.randomNumeric(6);return new SmsCode(code, 60);}}

这里我们使用createSMSCode方法生成了一个6位的纯数字随机数,有效时间为60秒。然后通过SessionStrategy对象的setAttribute方法将短信验证码保存到了Session中,对应的key为SESSION_KEY_SMS_CODE

至此,短信验证码生成模块编写完毕,下面开始改造登录页面。

2. 改造登录页

我们在登录页面中加入一个与手机短信验证码认证相关的Form表单:

<form class="login-page" action="/login/mobile" method="post"><div class="form"><h3>短信验证码登录</h3><input type="text" placeholder="手机号" name="mobile" value="17777777777" required="required"/><span style="display: inline"><input type="text" name="smsCode" placeholder="短信验证码" style="width: 50%;"/><a href="/code/sms?mobile=17777777777">发送验证码</a></span><button type="submit">登录</button></div>
</form>

其中a标签的href属性值对应我们的短信验证码生成方法的请求URL。Form的action对应处理短信验证码登录方法的请求URL,这个方法下面在进行具体实现。同时,我们需要在Spring Security中配置/code/sms路径免验证:

@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器.formLogin() // 表单方式//http.httpBasic() // HTTP Basic方式
//                .loginPage("/login.html").loginPage("/authentication/require") // 登录跳转 URL.loginProcessingUrl("/login").successHandler(authenticationSuccessHandler) // 处理登录成功.failureHandler(authenticationFailureHandler) // 处理登录失败.and().rememberMe().tokenRepository(persistentTokenRepository()) // 配置 token 持久化仓库.tokenValiditySeconds(3600) // remember 过期时间,单为秒.userDetailsService(userDetailService) // 处理自动登录逻辑.and().authorizeRequests() // 授权配置.antMatchers("/authentication/require", "/login.html", "/code/image", "/code/sms").permitAll().anyRequest()  // 所有请求.authenticated() // 都需要认证.and().csrf().disable();}

重启项目,访问http://localhost:8080/login.html:

点击发送验证码,控制台输出如下:

接下来开始实现使用短信验证码登录认证逻辑。

3. 添加短信验证码认证

在Spring Security中,使用用户名密码认证的过程大致如下图所示:

Spring Security使用UsernamePasswordAuthenticationFilter过滤器来拦截用户名密码认证请求,将用户名和密码封装成一个UsernamePasswordToken对象交给AuthenticationManager处理。AuthenticationManager将挑出一个支持处理该类型Token的AuthenticationProvider(这里为DaoAuthenticationProviderAuthenticationProvider的其中一个实现类)来进行认证,认证过程中DaoAuthenticationProvider将调用UserDetailServiceloadUserByUsername方法来获取UserDetails对象,如果UserDetails不为空并且密码和用户输入的密码匹配一致的话,则将认证信息保存到Session中,认证后我们便可以通过Authentication对象获取到认证的信息了。

由于Spring Security并没用提供短信验证码认证的流程,所以我们需要仿照上面这个流程来实现:

在这个流程中,我们自定义了一个名为SmsAuthenticationFitler的过滤器来拦截短信验证码登录请求,并将手机号码封装到一个叫SmsAuthenticationToken的对象中。在Spring Security中,认证处理都需要通过AuthenticationManager来代理,所以这里我们依旧将SmsAuthenticationToken交由AuthenticationManager处理。接着我们需要定义一个支持处理SmsAuthenticationToken对象的SmsAuthenticationProviderSmsAuthenticationProvider调用UserDetailServiceloadUserByUsername方法来处理认证。与用户名密码认证不一样的是,这里是通过SmsAuthenticationToken中的手机号去数据库中查询是否有与之对应的用户,如果有,则将该用户信息封装到UserDetails对象中返回并将认证后的信息保存到Authentication对象中。

为了实现这个流程,我们需要定义SmsAuthenticationFitlerSmsAuthenticationTokenSmsAuthenticationProvider,并将这些组建组合起来添加到Spring Security中。下面我们来逐步实现这个过程。

3.1 定义SmsAuthenticationToken

查看UsernamePasswordAuthenticationToken的源码,将其复制出来重命名为SmsAuthenticationToken,并稍作修改,修改后的代码如下所示:

public class SmsAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;private final Object principal;public SmsAuthenticationToken(String mobile) {super(null);this.principal = mobile;setAuthenticated(false);}public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {super(authorities);this.principal = principal;super.setAuthenticated(true); // must use super, as we override}@Overridepublic Object getCredentials() {return null;}public Object getPrincipal() {return this.principal;}public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}
}

SmsAuthenticationToken包含一个principal属性,从它的两个构造函数可以看出,在认证之前principal存的是手机号,认证之后存的是用户信息。UsernamePasswordAuthenticationToken原来还包含一个credentials属性用于存放密码,这里不需要就去掉了。

3.2 定义SmsAuthenticationFilter

定义完SmsAuthenticationToken后,我们接着定义用于处理短信验证码登录请求的过滤器SmsAuthenticationFilter,同样的复制UsernamePasswordAuthenticationFilter源码并稍作修改:

public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {public static final String MOBILE_KEY = "mobile";private String mobileParameter = MOBILE_KEY;private boolean postOnly = true;public SmsAuthenticationFilter() {super(new AntPathRequestMatcher("/login/mobile", "POST"));}public Authentication attemptAuthentication(HttpServletRequest request,HttpServletResponse response) throws AuthenticationException {if (postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String mobile = obtainMobile(request);if (mobile == null) {mobile = "";}mobile = mobile.trim();SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}protected String obtainMobile(HttpServletRequest request) {return request.getParameter(mobileParameter);}protected void setDetails(HttpServletRequest request,SmsAuthenticationToken authRequest) {authRequest.setDetails(authenticationDetailsSource.buildDetails(request));}public void setMobileParameter(String mobileParameter) {Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");this.mobileParameter = mobileParameter;}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}public final String getMobileParameter() {return mobileParameter;}
}

构造函数中指定了当请求为/login/mobile,请求方法为POST的时候该过滤器生效。mobileParameter属性值为mobile,对应登录页面手机号输入框的name属性。attemptAuthentication方法从请求中获取到mobile参数值,并调用SmsAuthenticationTokenSmsAuthenticationToken(String mobile)构造方法创建了一个SmsAuthenticationToken。下一步就如流程图中所示的那样,SmsAuthenticationFilterSmsAuthenticationToken交给AuthenticationManager处理。

3.3 定义SmsAuthenticationProvider

在创建完SmsAuthenticationFilter后,我们需要创建一个支持处理该类型Token的类,即SmsAuthenticationProvider,该类需要实现AuthenticationProvider的两个抽象方法:

public class SmsAuthenticationProvider implements AuthenticationProvider {private UserDetailService userDetailService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;UserDetails userDetails = userDetailService.loadUserByUsername((String) authenticationToken.getPrincipal());if (userDetails == null)throw new InternalAuthenticationServiceException("未找到与该手机号对应的用户");SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}@Overridepublic boolean supports(Class<?> aClass) {return SmsAuthenticationToken.class.isAssignableFrom(aClass);}public UserDetailService getUserDetailService() {return userDetailService;}public void setUserDetailService(UserDetailService userDetailService) {this.userDetailService = userDetailService;}
}

其中supports方法指定了支持处理的Token类型为SmsAuthenticationTokenauthenticate方法用于编写具体的身份认证逻辑。在authenticate方法中,我们从SmsAuthenticationToken中取出了手机号信息,并调用了UserDetailServiceloadUserByUsername方法。该方法在用户名密码类型的认证中,主要逻辑是通过用户名查询用户信息,如果存在该用户并且密码一致则认证成功;而在短信验证码认证的过程中,该方法需要通过手机号去查询用户,如果存在该用户则认证通过。认证通过后接着调用SmsAuthenticationTokenSmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities)构造函数构造一个认证通过的Token,包含了用户信息和用户权限。

你可能会问,为什么这一步没有进行短信验证码的校验呢?实际上短信验证码的校验是在SmsAuthenticationFilter之前完成的,即只有当短信验证码正确以后才开始走认证的流程。所以接下来我们需要定一个过滤器来校验短信验证码的正确性。

3.4 定义SmsCodeFilter

短信验证码的校验逻辑其实和图形验证码的校验逻辑基本一致,所以我们在图形验证码过滤器的基础上稍作修改,代码如下所示:

@Component
public class SmsCodeFilter extends OncePerRequestFilter {@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler;private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,FilterChain filterChain) throws ServletException, IOException {if (StringUtils.equalsIgnoreCase("/login/mobile", httpServletRequest.getRequestURI())&& StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {try {validateSmsCode(new ServletWebRequest(httpServletRequest));} catch (ValidateCodeException e) {authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);return;}}filterChain.doFilter(httpServletRequest, httpServletResponse);}private void validateSmsCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {String smsCodeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode");String mobile = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "mobile");SmsCode smsCode = (SmsCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);if (StringUtils.isBlank(smsCodeInRequest)) {throw new ValidateCodeException("验证码不能为空!");}if (smsCode == null) {throw new ValidateCodeException("验证码不存在,请重新发送!");}if (smsCode.isExpire()) {sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);throw new ValidateCodeException("验证码已过期,请重新发送!");}if (!StringUtils.equalsIgnoreCase(smsCode.getCode(), smsCodeInRequest)) {throw new ValidateCodeException("验证码不正确!");}sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile);}
}

方法的基本逻辑和之前定义的ValidateCodeFilter一致,这里不再赘述。

3.5 配置生效

在定义完所需的组件后,我们需要进行一些配置,将这些组件组合起来形成一个和上面流程图对应的流程。创建一个配置类SmsAuthenticationConfig

@Component
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {@Autowiredprivate AuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate AuthenticationFailureHandler authenticationFailureHandler;@Autowiredprivate UserDetailService userDetailService;@Overridepublic void configure(HttpSecurity http) throws Exception {SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();smsAuthenticationProvider.setUserDetailService(userDetailService);http.authenticationProvider(smsAuthenticationProvider).addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);}
}

在流程中第一步需要配置SmsAuthenticationFilter,分别设置了AuthenticationManagerAuthenticationSuccessHandlerAuthenticationFailureHandler属性。这些属性都是来自SmsAuthenticationFilter继承的AbstractAuthenticationProcessingFilter类中。

第二步配置SmsAuthenticationProvider,这一步只需要将我们自个的UserDetailService注入进来即可。

最后调用HttpSecurityauthenticationProvider方法指定了AuthenticationProviderSmsAuthenticationProvider,并将SmsAuthenticationFilter过滤器添加到了UsernamePasswordAuthenticationFilter后面。

到这里我们已经将短信验证码认证的各个组件组合起来了,最后一步需要做的是配置短信验证码校验过滤器,并且将短信验证码认证流程加入到Spring Security中。在SecurityConfigconfigure方法中添加如下配置:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate MyAuthenticationSuccessHandler authenticationSuccessHandler;@Autowiredprivate MyAuthenticationFailureHandler authenticationFailureHandler;@Autowiredprivate ValidateCodeFilter validateCodeFilter;@Autowiredprivate DataSource dataSource;@Autowiredprivate UserDetailService userDetailService;@Autowiredprivate SmsCodeFilter smsCodeFilter;@Autowiredprivate SmsAuthenticationConfig smsAuthenticationConfig;/*** 处理自动登录** @return*/@Beanpublic PersistentTokenRepository persistentTokenRepository() {JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();jdbcTokenRepository.setDataSource(dataSource);jdbcTokenRepository.setCreateTableOnStartup(false);return jdbcTokenRepository;}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加短信验证码校验过滤器.formLogin() // 表单方式//http.httpBasic() // HTTP Basic方式
//                .loginPage("/login.html").loginPage("/authentication/require") // 登录跳转 URL.loginProcessingUrl("/login").successHandler(authenticationSuccessHandler) // 处理登录成功.failureHandler(authenticationFailureHandler) // 处理登录失败.and().rememberMe().tokenRepository(persistentTokenRepository()) // 配置 token 持久化仓库.tokenValiditySeconds(3600) // remember 过期时间,单为秒.userDetailsService(userDetailService) // 处理自动登录逻辑.and().authorizeRequests() // 授权配置.antMatchers("/authentication/require", "/login.html", "/code/image", "/code/sms").permitAll().anyRequest()  // 所有请求.authenticated() // 都需要认证.and().csrf().disable().apply(smsAuthenticationConfig); // 将短信验证码认证配置加到 Spring Security 中}}

4. 测试

重启项目,访问http://localhost:8080/login.html,点击发送验证码,控制台输出如下:这个时候界面会跳转到其他界面,返回一下就可以了,然后输入验证码。

输入该验证码,点击登录后页面如下所示:

5. 项目地址

短信验证登录

Spring Security 短信验证码登录(5)相关推荐

  1. 5.Spring Security 短信验证码登录

    Spring Security 短信验证码登录 在 Spring Security 添加图形验证码一节中,我们已经实现了基于 Spring Boot + Spring Security 的账号密码登录 ...

  2. spring security 短信验证码登录

    短信登录过滤器  SmsAuthenticationFilter  import org.springframework.lang.Nullable; import org.springframewo ...

  3. Spring Security简单增加短信验证码登录

    查网上资料增加短信验证码登录都要增加一大推,要重头写Spring Security的实现,我呢,只想在原来的密码登录基础上简单实现一下短信验证码登录. 1.首先得先一个认证类,来认证验证码是否正确,这 ...

  4. Spring Security OAuth2 优雅的集成短信验证码登录以及第三方登录

    基于SpringCloud做微服务架构分布式系统时,OAuth2.0作为认证的业内标准,Spring Security OAuth2也提供了全套的解决方案来支持在Spring Cloud/Spring ...

  5. security模仿密码登录实现短信验证码登录

    security模仿密码登录实现短信验证码登录 模仿UsernamePasswordAuthenticationToken创建短信验证码的token类SmsAuthenticationToken /* ...

  6. SpringSecurity短信验证码登录

    短信验证码登录 时下另一种非常常见的网站登录方式为手机短信验证码登录,但Spring Security默认只提供了账号密码的登录认证逻辑,所以要实现手机短信验证码登录认证功能,我们需要模仿Spring ...

  7. 手把手带你在集成SpringSecurity的SpringBoot应用中添加短信验证码登录认证功能

    本文目录 前言 1 自定义AuthenticationToken类 2 自定义AuthenticationProvider类 3 自定义MobilePhoneAuthenticationFilter ...

  8. SpringBoot + SpringSecurity 短信验证码登录功能实现

    实现原理 在之前的文章中,我们介绍了普通的帐号密码登录的方式:SpringBoot + Spring Security 基本使用及个性化登录配置(http://www.deiniu.com/artic ...

  9. cas5.3.9自定义手机短信验证码登录

    cas自定义多种登录方式 cas添加手机短信验证码登录 cas添加手机短信验证码登录 全部基于SpringBoot,以及SpringWebflow开发,请在有此基础再进行学习! 添加Maven依赖 & ...

最新文章

  1. 模拟真实环境之内网漫游
  2. SpringCloud创建项目父工程
  3. iPhone 12包装盒设计曝光,没充电器没耳机实锤?
  4. 关于修改CentOS7(64位)环境变量
  5. 【破解】Flash Decompiler Trillix.4.1.1.720 破解补丁
  6. MYsql隔离级别问题和解决方案,Spring的七大传播机制
  7. 《MPLS在Cisco IOS上的配置》一2.3 配置命令参考
  8. 181220每日一句
  9. 将java对象转换成json字符串_将java对象转换成json字符串
  10. 基于UP30飞控系统的无人机地面站航线规划方法
  11. 一个理工女宝妈和西牧乳业奶粉的故事
  12. 剑指Offe面试题:用两个栈实现队列
  13. AE模板 正文00:39 / 00:51抽象视差优雅大气科幻时尚视频幻灯片AE模板
  14. 学习笔记(05):【中国PHP教育大牛高洛峰】亲授php教程-PHP常量的介绍
  15. 什么软件测试iphone性能,5款iPhone性能测试比拼:A9虽然垫底,与A13的差距并不大...
  16. Mysql配置ssl证书
  17. DCQCN+QCN+DCTCP
  18. 中国计算机学会推荐国际学术期刊
  19. 五子棋爱好者必看!专业五子棋级位段位资格评定标准
  20. 【前端修炼场】 — 认识前端了解HTML(筑基期)

热门文章

  1. Python实现Plugin
  2. iview table数据直接导出_(excel表格数据导入导出)iView如何实现excel导入导出
  3. 微信小程序批量提交后台审核
  4. 计算机领域中的牛人,计算机视觉领域的牛人
  5. c语言i10表达式的值是什么,C10和I10的换算关系是()。A.C10=10×I10B.I10=10×C10C.I10×C10=10...
  6. 抖音用户扫码一键转发视频至抖音php
  7. 标准h5的定位_HTML5定位(浏览器定位技)术探讨
  8. 2008服务器做微信退款,Net微信退款开发流程
  9. 【云和恩墨招聘】这是一份靠谱的招聘需求
  10. 赛门铁克Symantec 软件界面,备份还原设置操作讲解说明