在Spring Security添加图形验证码一节中,我们已经实现了基于Spring Boot + Spring Security的账号密码登录,并集成了图形验证码功能。时下另一种非常常见的网站登录方式为手机短信验证码登录,但Spring Security默认只提供了账号密码的登录认证逻辑,所以要实现手机短信验证码登录认证功能,我们需要模仿Spring Security账号密码登录逻辑代码来实现一套自己的认证逻辑。
短信验证码生成
我们在上一节Spring Security添加图形验证码的基础上来集成短信验证码登录的功能。
和图形验证码类似,我们先定义一个短信验证码对象SmsCode:
1 | public class SmsCode { |
SmsCode对象包含了两个属性:code验证码和expireTime过期时间。isExpire方法用于判断短信验证码是否已过期。
接着在ValidateCodeController中加入生成短信验证码相关请求对应的方法:
1 |
|
这里我们使用createSMSCode
方法生成了一个6位的纯数字随机数,有效时间为60秒。然后通过SessionStrategy
对象的setAttribute
方法将短信验证码保存到了Session中,对应的key为SESSION_KEY_SMS_CODE
。
至此,短信验证码生成模块编写完毕,下面开始改造登录页面。
改造登录页
我们在登录页面中加入一个与手机短信验证码认证相关的Form表单:
1 | <form class="login-page" action="/login/mobile" method="post"> |
其中a标签的href
属性值对应我们的短信验证码生成方法的请求URL。Form的action对应处理短信验证码登录方法的请求URL,这个方法下面在进行具体实现。同时,我们需要在Spring Security中配置/code/sms
路径免验证:
1 |
|
重启项目,访问http://localhost:8080/login.html:
点击发送验证码,控制台输出如下:
1 | 您的登录验证码为:693583,有效时间为60秒 |
接下来开始实现使用短信验证码登录认证逻辑。
添加短信验证码认证
在Spring Security中,使用用户名密码认证的过程大致如下图所示:
Spring Security使用UsernamePasswordAuthenticationFilter
过滤器来拦截用户名密码认证请求,将用户名和密码封装成一个UsernamePasswordToken
对象交给AuthenticationManager
处理。AuthenticationManager
将挑出一个支持处理该类型Token的AuthenticationProvider
(这里为DaoAuthenticationProvider
,AuthenticationProvider
的其中一个实现类)来进行认证,认证过程中DaoAuthenticationProvider
将调用UserDetailService
的loadUserByUsername
方法来获取UserDetails对象,如果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中。下面我们来逐步实现这个过程。
定义SmsAuthenticationToken
查看UsernamePasswordAuthenticationToken
的源码,将其复制出来重命名为SmsAuthenticationToken
,并稍作修改,修改后的代码如下所示:
1 | public class SmsAuthenticationToken extends AbstractAuthenticationToken { |
SmsAuthenticationToken
包含一个principal
属性,从它的两个构造函数可以看出,在认证之前principal
存的是手机号,认证之后存的是用户信息。UsernamePasswordAuthenticationToken
原来还包含一个credentials
属性用于存放密码,这里不需要就去掉了。
定义SmsAuthenticationFilter
定义完SmsAuthenticationToken
后,我们接着定义用于处理短信验证码登录请求的过滤器SmsAuthenticationFilter
,同样的复制UsernamePasswordAuthenticationFilter
源码并稍作修改:
1 | public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { |
构造函数中指定了当请求为/login/mobile
,请求方法为POST的时候该过滤器生效。mobileParameter
属性值为mobile,对应登录页面手机号输入框的name属性。attemptAuthentication
方法从请求中获取到mobile参数值,并调用SmsAuthenticationToken
的SmsAuthenticationToken(String mobile)
构造方法创建了一个SmsAuthenticationToken
。下一步就如流程图中所示的那样,SmsAuthenticationFilter
将SmsAuthenticationToken
交给AuthenticationManager
处理。
定义SmsAuthenticationProvider
在创建完SmsAuthenticationFilter
后,我们需要创建一个支持处理该类型Token的类,即SmsAuthenticationProvider
,该类需要实现AuthenticationProvider
的两个抽象方法:
1 | public class SmsAuthenticationProvider implements AuthenticationProvider { |
其中supports
方法指定了支持处理的Token类型为SmsAuthenticationToken
,authenticate
方法用于编写具体的身份认证逻辑。在authenticate
方法中,我们从SmsAuthenticationToken
中取出了手机号信息,并调用了UserDetailService
的loadUserByUsername
方法。该方法在用户名密码类型的认证中,主要逻辑是通过用户名查询用户信息,如果存在该用户并且密码一致则认证成功;而在短信验证码认证的过程中,该方法需要通过手机号去查询用户,如果存在该用户则认证通过。认证通过后接着调用SmsAuthenticationToken
的SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities)
构造函数构造一个认证通过的Token,包含了用户信息和用户权限。
你可能会问,为什么这一步没有进行短信验证码的校验呢?实际上短信验证码的校验是在SmsAuthenticationFilter
之前完成的,即只有当短信验证码正确以后才开始走认证的流程。所以接下来我们需要定一个过滤器来校验短信验证码的正确性。
定义SmsCodeFilter
短信验证码的校验逻辑其实和图形验证码的校验逻辑基本一致,所以我们在图形验证码过滤器的基础上稍作修改,代码如下所示:
1 |
|
方法的基本逻辑和之前定义的ValidateCodeFilter
一致,这里不再赘述。
配置生效
在定义完所需的组件后,我们需要进行一些配置,将这些组件组合起来形成一个和上面流程图对应的流程。创建一个配置类SmsAuthenticationConfig
:
1 |
|
在流程中第一步需要配置SmsAuthenticationFilter
,分别设置了AuthenticationManager
、AuthenticationSuccessHandler
和AuthenticationFailureHandler
属性。这些属性都是来自SmsAuthenticationFilter
继承的AbstractAuthenticationProcessingFilter
类中。
第二步配置SmsAuthenticationProvider
,这一步只需要将我们自个的UserDetailService
注入进来即可。
最后调用HttpSecurity
的authenticationProvider
方法指定了AuthenticationProvider
为SmsAuthenticationProvider
,并将SmsAuthenticationFilter
过滤器添加到了UsernamePasswordAuthenticationFilter
后面。
到这里我们已经将短信验证码认证的各个组件组合起来了,最后一步需要做的是配置短信验证码校验过滤器,并且将短信验证码认证流程加入到Spring Security中。在BrowserSecurityConfig
的configure
方法中添加如下配置:
1 |
|
具体含义见注释,这里不再赘述。
测试
重启项目,访问http://localhost:8080/login.html,点击发送验证码,控制台输出如下:
1 | 您的登录验证码为:169638,有效时间为60秒 |
输入该验证码,点击登录后页面如下所示:
认证成功。
源码链接 https://github.com/wuyouzhuguli/SpringAll/tree/master/38.Spring-Security-SmsCode