在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的认证过滤器和认证处理器实现短信认证。
短信验证码认证
验证码对象类设计
和图片验证码一样,需要自己封装一个验证码对象,用来生成手机验证码并发送给手机。因为图片验证码和手机验证码对象的区别就在于前者多了个图片对象,所以两者共同部分抽象出来可以设计成一个ValidateCode
类,这个类里面只存放验证码和过期时间,短信验证码直接使用这个类即可:
1 | import java.time.LocalDateTime; |
图片验证码承继此类:
1 | import java.awt.image.BufferedImage; |
验证码生成类设计
由于图片和短信类均可以生成相应的验证码,所以直接设计一个验证码生成接口,具体实现类根据业务进行实现:
1 | import org.springframework.web.context.request.ServletWebRequest; |
这里的传参设计成了
ServletWebRequest
是能够根据前端请求中的参数进行不同的业务实现
目前实现累只有图片生成器和验证码生成器:
1 | // 图片验证码生成器 |
短信验证码发送接口设计
短信验证码生成之后,需要设计接口依赖短信服务提供商进行验证码发送,因此至少设计一个统一的接口,供短信服务提供商生成发送短信服务:
1 | public interface SmsCodeSender { |
为了演示,设计一个虚拟的默认短信发送器,只在日志文件中打印一行log:
1 | import org.springframework.stereotype.Service; |
短信验证码请求Controller
所有验证码的请求都在统一的ValidateCodeController
里,这里注入了两个验证码生成器ValidateCodeGenerator
,后期可以利用 spring 的依赖查找/搜索技巧来重构代码,另外所有的请求也是可以做成动态配置,这里临时全部 hardCode 在代码里:
1 | import java.io.IOException; |
从上述代码中可以看出图片验证码和短信验证码的生成请求逻辑是相似的:首先调用验证码生成接口生成验证码,然后将验证码放入 session 中,最后将验证码返回给前端或者用户。因此这个套路流程可以抽象成一个模板方法,以增强代码的可维护性和可扩展性。
用一张图来表述重构后的代码结构:
随机验证码过滤器设计
由于图片和手机都会产生验证码,后期还可以通过邮件发送随机验证码的方式进行随机验证码登录验证,因此将随机验证码的认证可以独立封装在一个随机验证码过滤器中,并且这个过滤器在整个 spring security 过滤器链的最前端(它是第一道认证墙)。
随机验证码过滤器只要继承 spring 框架中的OncePerRequestFilter
即可保证这个过滤器在请求来的时候只被调用一次,具体代码实现参见文末源码。
这里重点解释一下如何将随机验证码过滤器配置到 spring security 过滤器认证最前端,需要重写SecurityConfigurerAdapter
的configure()
方法,并将自定义的过滤器放到AbstractPreAuthenticatedProcessingFilter
过滤器之前即可:
1 | import org.springframework.beans.factory.annotation.Autowired; |
短信验证码认证
在自定义短信登录认证流程之前,建议可以移步到之前的文章:SpringBoot + Spring Security 学习笔记(二)安全认证流程源码详解,了解清除用户密码的认证流程才能更容易理解下面这张经典的流程图:
左侧是用户+密码的认证流程,整体的流程就是经过用户名+密码认证过滤器认证,将请求封装成 token 并注入到 AutheticationMananger 中,之后由默认的认证校验器进行校验,在校验的过程中会调用 UserDetailsService 接口进行 token 校验,当校验成功之后,就会将已经认证的 token 放到 SecurityContextHolder 中。
同理,由于短信登录方式只需要使用随机验证码进行校验而不需要密码登录功能,当校验成功之后就认为用户认证成功了,因此需要仿造左侧的流程开发自定义的短信登录认证 token,这个 token 只需要存放手机号即可,在token 校验的过程中,不能使用默认的校验器了,需要自己开发校验当前自定义 token 的校验器,最后将自定义的过滤器和校验器配置到 spring security 框架中即可。
注意:短信随机验证码的验证过程是在 SmsCodeAuthticationFIlter 之前就已经完成。
短信登录认证Token
仿造UsernamePasswordAuthenticationToken
设计一个属于短信验证的认证 token 对象,为什么要自定义一个短信验证的 token,spring security 框架不只提供了用户名+密码的验证方式,用户认证是否成功,最终看的就是SecurityContextHolder
对象中是否有对应的AuthenticationToken
,因此要设计一个认证对象,当认证成功之后,将其设置到SecurityContextHolder
即可。
1 | import java.util.Collection; |
从AuthenticationToken
接口可以看到,现在框架中有我们自己定义短信登录的 token 了:
短信登录认证过滤器
短信验证码的过滤器设计思路同理,仿造UsernamePasswordAuthenticationFilter
过滤器,这里再次提醒,短信随机验证码
1 | import java.util.Objects; |
短信验证码过滤器也成为了AbstractAuthenticationProcessingFilter
其中一个子类,后期需要注册到安全配置中,让它成为安全认证过滤链中的一环:
短信登录认证校验器
短信登录认证校验器的作用就是调用UserDetailsService
的loadUserByUsername()
方法对 authenticationToken 进行校验,所有校验器的根接口为:AuthenticationProvider
,因此自定义的短信登录认证校验器实现这个接口,重写authenticate()
即可:
1 | import java.util.Objects; |
注意,这里使用@Data
注解生成 setter 和 getter 方法。
短信登录认证安全配置设计
设计一个封装好的短信登录认证配置类,以供外部调用者直接调用:
1 | import org.springframework.beans.factory.annotation.Autowired; |
当外部想要引用这个封装好的配置,只需要在自定义的AbstractChannelSecurityConfig
安全认证配置中添加进去即可,注意这个配置对象使用了@Component
注解,注册到了spring 中,所以可以直接通过@Autowired
引用,如:
1 | import javax.sql.DataSource; |
这里的配置中有些代码出现了冗余配置,可以全部封装成抽象模板,完成一些基础的配置。