Spring Security 实战干货:从零手写一个验证码登录

Spring Security 实战干货:从零手写一个验证码登录

1. 前言

前面关于Spring Security写了两篇文章,一篇是介绍UsernamePasswordAuthenticationFilter,另一篇是介绍 AuthenticationManager。很多同学表示无法理解这两个东西有什么用,能解决哪些实际问题?所以今天就对这两篇理论进行实战运用,我们从零写一个短信验证码登录并适配到Spring Security体系中。如果你在阅读中有什么疑问可以回头看看这两篇文章,能解决很多疑惑。

当然你可以修改成邮箱或者其它通讯设备的验证码登录。

2. 验证码生命周期

验证码存在有效期,一般5分钟。 一般逻辑是用户输入手机号后去获取验证码,服务端对验证码进行缓存。在最大有效期内用户只能使用验证码验证成功一次(避免验证码浪费);超过最大时间后失效。

验证码的缓存生命周期:

public interface CaptchaCacheStorage { /** * 验证码放入缓存. * * @param phone the phone * @return the string */ String put(String phone); /** * 从缓存取验证码. * * @param phone the phone * @return the string */ String get(String phone); /** * 验证码手动过期. * * @param phone the phone */ void expire(String phone); }

我们一般会借助于缓存中间件,比如RedisEhcacheMemcached等等来做这个事情。为了方便收看该教程的同学们所使用的不同的中间件。这里我结合Spring Cache特意抽象了验证码的缓存处理。

private static final String SMS_CAPTCHA_CACHE = "captcha"; @Bean CaptchaCacheStorage captchaCacheStorage() { return new CaptchaCacheStorage() { @CachePut(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone") @Override public String put(String phone) { return RandomUtil.randomNumbers(5); } @Cacheable(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone") @Override public String get(String phone) { return null; } @CacheEvict(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone") @Override public void expire(String phone) { } }; }

务必保证缓存的可靠性,这与用户的体验息息相关。

接着我们就来编写验证码服务了,验证码服务的核心功能有两个:发送验证码验证码校验。其它的诸如统计、黑名单、历史记录可根据实际业务定制。这里只实现核心功能。

/** * 验证码服务. * 两个功能: 发送和校验. * * @param captchaCacheStorage the captcha cache storage * @return the captcha service */ @Bean public CaptchaService captchaService(CaptchaCacheStorage captchaCacheStorage) { return new CaptchaService() { @Override public boolean sendCaptcha(String phone) { String existed = captchaCacheStorage.get(phone); if (StringUtils.hasText(existed)) { // 节约成本的话如果缓存中有可用的验证码 不再发新的验证码 log.warn("captcha code 【 {} 】 is available now", existed); return false; } // 生成验证码并放入缓存 String captchaCode = captchaCacheStorage.put(phone); log.info("captcha: {}", captchaCode); //todo 这里自行完善调用第三方短信服务发送验证码 return true; } @Override public boolean verifyCaptcha(String phone, String code) { String cacheCode = captchaCacheStorage.get(phone); if (Objects.equals(cacheCode, code)) { // 验证通过手动过期 captchaCacheStorage.expire(phone); return true; } return false; } }; }

接下来就可以根据CaptchaService编写短信发送接口/captcha/{phone}了。

@RestController @RequestMapping("/captcha") public class CaptchaController { @Resource CaptchaService captchaService; /** * 模拟手机号发送验证码. * * @param phone the mobile * @return the rest */ @GetMapping("/{phone}") public Rest<?> captchaByMobile(@PathVariable String phone) { //todo 手机号 正则自行验证 if (captchaService.sendCaptcha(phone)){ return RestBody.ok("验证码发送成功"); } return RestBody.failure(-999,"验证码发送失败"); } } 3. 集成到Spring Security

下面的教程就必须用到前两篇介绍的知识了。我们要实现验证码登录就必须定义一个Servlet Filter进行处理。它的作用这里再重复一下:

拦截短信登录接口。

获取登录参数并封装为Authentication凭据。

交给AuthenticationManager认证。

我们需要先定制Authentication和AuthenticationManager

3.1 验证码凭据

Authentication在我看来就是一个载体,在未得到认证之前它用来携带登录的关键参数,比如用户名和密码、验证码;在认证成功后它携带用户的信息和角色集。所以模仿UsernamePasswordAuthenticationToken 来实现一个CaptchaAuthenticationToken,去掉不必要的功能,抄就完事儿了:

package cn.felord.spring.security.captcha; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.SpringSecurityCoreVersion; import java.util.Collection; /** * 验证码认证凭据. * @author felord.cn */ public class CaptchaAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; private String captcha; /** * 此构造函数用来初始化未授信凭据. * * @param principal the principal * @param captcha the captcha * @see CaptchaAuthenticationToken#CaptchaAuthenticationToken(Object, String, Collection) */ public CaptchaAuthenticationToken(Object principal, String captcha) { super(null); this.principal = principal; this.captcha = captcha; setAuthenticated(false); } /** * 此构造函数用来初始化授信凭据. * * @param principal the principal * @param captcha the captcha * @param authorities the authorities * @see CaptchaAuthenticationToken#CaptchaAuthenticationToken(Object, String) */ public CaptchaAuthenticationToken(Object principal, String captcha, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.captcha = captcha; super.setAuthenticated(true); // must use super, as we override } public Object getCredentials() { return this.captcha; } 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); } @Override public void eraseCredentials() { super.eraseCredentials(); captcha = null; } 3.2 验证码认证管理器

我们还需要定制一个AuthenticationManager来对上面定义的凭据CaptchaAuthenticationToken进行认证处理。下面这张图有必要再拿出来看一下:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgjdwf.html