在Spring Security -- 添加图形验证码一节中,我们已经实现了基于Spring Boot + Spring Security的账号密码登录,并集成了图形验证码功能。当前另一种非常常见的网站登录方式为手机短信验证码登录,但Spring Security默认只提供了账号密码的登录认证逻辑,所以要实现手机短信验证码登录认证功能,我们需要模仿Spring Security账号密码登录逻辑代码来实现一套自己的认证逻辑。
一、短信验证码生成
我们在Spring Security -- 添加图形验证码的基础上来集成短信验证码登录的功能。
1、SmsCode实体类
和图形验证码类似,我们先定义一个短信验证码对象SmsCode:
package com.goldwind.entity; import lombok.Data; import java.time.LocalDateTime; /** * @Author: zy * @Description: 手机验证码实体类 * @Date: 2020-2-9 */ @Data public class SmsCode { /** * code验证码 */ private String code; /** * 过期时间 单位秒 */ private LocalDateTime expireTime; /** * 判断验证码是否过期 * @return */ public boolean isExpire() { return LocalDateTime.now().isAfter(expireTime); } /** * 构造函数 * @param code * @param expireIn */ public SmsCode(String code, int expireIn) { this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } /** * 构造函数 * @param code * @param expireTime */ public SmsCode(String code, LocalDateTime expireTime) { this.code = code; this.expireTime = expireTime; } }
SmsCode对象包含了两个属性:code验证码和expireTime过期时间。isExpire方法用于判断短信验证码是否已过期。
2、ValidateCodeController
接着在ValidateCodeController中加入生成短信验证码相关请求对应的方法:
/** * 手机验证码 */ public final static String SESSION_KEY_SMS_CODE = "SESSION_KEY_SMS_CODE"; /** * 用于生成手机验证码 * @param request:请求 * @param response:响应 * @param mobile:手机号码 * @throws IOException:异常 */ @RequestMapping("/sms") public void createSmsCode(HttpServletRequest request, HttpServletResponse response,@RequestParam String mobile) throws IOException { //生成手机验证码对象 SmsCode smsCode = createSMSCode(); //生成的验证码对象存储到Session中 sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_SMS_CODE + mobile, smsCode); // 输出验证码到控制台代替短信发送服务 System.out.println("您的登录验证码为:" + smsCode.getCode() + ",有效时间为60秒"); } /** * 用于生成手机验证码对象 * @return */ private SmsCode createSMSCode() { String code = RandomStringUtils.randomNumeric(6); return new SmsCode(code, 60); }
这里我们使用createSMSCode方法生成了一个6位的纯数字随机数,有效时间为60秒。然后通过SessionStrategy对象的setAttribute方法将短信验证码保存到了Session中,对应的key为SESSION_KEY_SMS_CODE。
至此,短信验证码生成模块编写完毕,下面开始改造登录页面。
二、登录页
我们在登录页面中加入一个与手机短信验证码认证相关的Form表单:
<div id="content"> <div id="box"> <div class="title">短信验证码登录</div> <div class="input"> <form name="f" action="/login" method="post"> <input type="text" placeholder="手机号" name="mobile" value="17777777777" required="required"/> <br> <input type="text" name="smsCode" placeholder="短信验证码" style="width: 50%;"/> <a href="/code/sms?mobile=17777777777">发送验证码</a> <br> <input type="submit" value="登录" /> </form> </div> </div> </div>
其中a标签的href属性值对应我们的短信验证码生成方法的请求URL。同时,我们需要在Spring Security中配置/code/sms路径免验证:
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器 .authorizeRequests() // 授权配置 .antMatchers("/code/image","/code/sms") .permitAll() // 无需认证的请求路径 .anyRequest() // 任何请求 .authenticated() //都需要身份认证 .and() .formLogin() // 或者httpBasic() .loginPage("/login") // 指定登录页的路径 .loginProcessingUrl("/login") // 指定自定义form表单请求的路径 .successHandler(authenticationSucessHandler) // 处理登录成功 .failureHandler(authenticationFailureHandler) // 处理登录失败 // 必须允许所有用户访问我们的登录页(例如未验证的用户,否则验证流程就会进入死循环) // 这个formLogin().permitAll()方法允许所有用户基于表单登录访问/login这个page。 .permitAll() .and() .rememberMe() .tokenRepository(persistentTokenRepository) // 配置 token 持久化仓库 .tokenValiditySeconds(3600) // remember 过期时间,单为秒 .userDetailsService(userDetailService) // 处理自动登录逻辑 .and() .logout() .permitAll() .and() //默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉 .csrf().disable(); }
重启项目,访问http://localhost:8080/login:
点击发送验证码,控制台输出如下:
您的登录验证码为:788974,有效时间为60秒
接下来开始实现使用短信验证码登录认证逻辑。
三、添加短信验证码认证
1、实现原理
在Spring Security中,使用用户名密码认证的过程大致如下图所示:
Spring Security使用UsernamePasswordAuthenticationFilter过滤器来拦截用户名密码认证请求,将用户名和密码封装成一个UsernamePasswordToken对象交给AuthenticationManager处理。AuthenticationManager将挑出一个支持处理该类型Token的AuthenticationProvider(这里为DaoAuthenticationProvider,AuthenticationProvider的其中一个实现类)来进行认证,认证过程中DaoAuthenticationProvider将调用UserDetailService的loadUserByUsername方法来处理认证,如果认证通过(即UsernamePasswordToken中的用户名和密码相符)则返回一个UserDetails类型对象,并将认证信息保存到Session中,认证后我们便可以通过Authentication对象获取到认证的信息了。
由于Spring Security并没用提供短信验证码认证的流程,所以我们需要仿照上面这个流程来实现:
在这个流程中,我们自定义了一个名为SmsAuthenticationFitler的过滤器来拦截短信验证码登录请求,并将手机号码封装到一个叫SmsAuthenticationToken的对象中。在Spring Security中,认证处理都需要通过AuthenticationManager来代理,所以这里我们依旧将SmsAuthenticationToken交由AuthenticationManager处理。接着我们需要定义一个支持处理SmsAuthenticationToken对象的SmsAuthenticationProvider,SmsAuthenticationProvider调用UserDetailService的loadUserByUsername方法来处理认证。与用户名密码认证不一样的是,这里是通过SmsAuthenticationToken中的手机号去数据库中查询是否有与之对应的用户,如果有,则将该用户信息封装到UserDetails对象中返回并将认证后的信息保存到Authentication对象中。
为了实现这个流程,我们需要定义SmsAuthenticationFitler、SmsAuthenticationToken和SmsAuthenticationProvider,并将这些组建组合起来添加到Spring Security中。下面我们来逐步实现这个过程。