源码下载
摘要
前面做的用户名和密码登陆是SpringSecurity框架内部集成好的功能,但是手机短信验证码登陆SpringSecurity框架并没有内部实现这个功能,所以需要使用者自己去实现手机短信验证码的功能,最后在把自己的实现嫁接到SpringSecurity框架上。
一、手机短信验证码实现原理
通过前面对SpringSecurity的用户名密码登陆源码的分析,主要流程就是:
- 1、通过过滤器(UsernamePasswordAuthenticationFilter)来拦截指定的url接口
- 2、通过用户名和密码来生成一个用于认证的Token(UsernamePasswordAuthenticationToken)
- 3、然后通过AuthenticationManager管理器(ProviderManager)来找到一个合适的处理器(DaoAuthenticationProvider)处理UsernamePasswordAuthenticationToken
- 4、通过UserDetailsService接口中loadUserByUsername方法把用户信息传给SpringSecurity框架,然后与UsernamePasswordAuthenticationToken中的用户名和密码进行比对校验,如果校验成功,那就认证通过
- 5、认证通过后把带有权限的UserDetails存储到session
-
6、加入自定义图形验证码时,需要在UsernamePasswordAuthenticationFilter过滤器之前再加上一个验证码过滤器,图片验证码验证通过后才可以继续执行后面的步骤
用户名密码登陆流程.png
通过上面对用户名密码登陆的分析,可以推出手机验证码登陆流程如下
- 1、自定义手机短信验证过滤器(RavenMobileCodeValidateFilter)
- 2、拦截指定url的过滤器(RavenMobileCodeAuthenticationFilter)
- 3、封装手机号为Token(RavenMobileCodeAuthenticationToken)
- 4、用户处理Token的Provider(RavenMobileCodeAuthenticationProvider)
- 5、通过UserDetailsService加载用户信息
-
6、认证通过后把带有权限的UserDetails存储到session
手机短信验证登陆.png
二、编写代码
- RavenMobileCodeAuthenticationFilter 过滤器
/**
* 手机短信验证码过滤器
*/
public class RavenMobileCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
/**
* 指定拦截的请求url 和 请求方法
*/
public RavenMobileCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/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();
RavenMobileCodeAuthenticationToken authRequest = new RavenMobileCodeAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
// protected String obtainPassword(HttpServletRequest request) {
// return request.getParameter(passwordParameter);
// }
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected void setDetails(HttpServletRequest request,
RavenMobileCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Username parameter must not be empty or null");
this.mobileParameter = mobileParameter;
}
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
public final String getPasswordParameter() {
return null;
}
}
- RavenMobileCodeAuthenticationToken
public class RavenMobileCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 认证主题
* 认证前:principal存储的是手机号
* 认证后:principal存储的是认证信息
*/
private final Object principal;
/**
* 通过手机号来构造
*/
public RavenMobileCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* 认证成功后
* principal:认证信息
* 设置被已认证
*/
public RavenMobileCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
public 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);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
- RavenMobileCodeAuthenticationProvider
@Setter
@Getter
public class RavenMobileCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RavenMobileCodeAuthenticationToken mobileToken = (RavenMobileCodeAuthenticationToken) authentication;
// 通过手机号来验证
UserDetails userDetails = this.userDetailsService.loadUserByUsername((String) mobileToken.getPrincipal());
if (userDetails == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
/**
* 到这里认证通过
* 通过重新创建RavenMobileCodeAuthenticationToken,使用2个参数的构造器
* 这样认证标识会被设置为true
*/
RavenMobileCodeAuthenticationToken mobileAuthenticationToken = new RavenMobileCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
// 重新赋值
mobileAuthenticationToken.setDetails(mobileToken.getDetails());
return mobileAuthenticationToken;
}
/**
* AuthenticationManager 选择要执行的Provider
* 这里就决定了不同的 Provider 执行不同的 Token
* RavenMobileCodeAuthenticationToken
*/
@Override
public boolean supports(Class<?> authentication) {
return (RavenMobileCodeAuthenticationToken.class
.isAssignableFrom(authentication));
}
}
- 添加配置,把自定义的Provider和Filter添加到SpringSecurity的过滤器链上
@Component
public class RavenMobileCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler successHandler;
@Autowired
private AuthenticationFailureHandler failureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity http) throws Exception {
// 手机短信登录过滤器
RavenMobileCodeAuthenticationFilter mobileFilter = new RavenMobileCodeAuthenticationFilter();
mobileFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
mobileFilter.setAuthenticationSuccessHandler(this.successHandler);
mobileFilter.setAuthenticationFailureHandler(this.failureHandler);
//Provider
RavenMobileCodeAuthenticationProvider mobileProvider = new RavenMobileCodeAuthenticationProvider();
mobileProvider.setUserDetailsService(this.userDetailsService);
// 把自定义Provider 和 Filter 加入到SpringSecurity的过滤器链上
http.authenticationProvider(mobileProvider)
.addFilterAfter(mobileFilter, UsernamePasswordAuthenticationFilter.class);
}
}
三、手机短信登录验证
- bw-login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h3>短信登录</h3>
<form action="/authentication/mobile" method="post">
<table>
<tr>
<td>手机号:</td>
<td><input type="text" name="mobile" value="18212345678"></td>
</tr>
<tr>
<td>短信验证码:</td>
<td>
<input type="text" name="smsCode">
<a href="/code/sms?mobile=18212345678">发送验证码</a>
</td>
</tr>
<tr>
<td colspan='2'><input name="remember-me" type="checkbox" value="true" />记住我</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
</body>
</html>
- 处理验证码请求
@RestController
public class RavenValidateCodeController {
/**
* 操作session的工具类
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private IRavenValidateCodeGenerator imageValidateCodeGenerator;
@Autowired
private IRavenMobileValidateCodeGenerator mobileValidateCodeGenerator;
@Autowired
private IRavenMobileCodeSendService mobileCodeSendService;
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 生成图片
RavenImageCode imageCode = this.imageValidateCodeGenerator.generator(new ServletWebRequest(request, response));
// 保存
sessionStrategy.setAttribute(new ServletWebRequest(request, response), RavenSecurityConstants.SESSION_KEY_PREFIX, imageCode);
// 发送
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
@GetMapping("/code/sms")
public void createMobileCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 获取手机号
String mobile = ServletRequestUtils.getStringParameter(request, "mobile");
// 生成验证码
RavenValidateCode validateCode = this.mobileValidateCodeGenerator.generator(new ServletWebRequest(request, response));
// 保存
sessionStrategy.setAttribute(new ServletWebRequest(request, response), RavenSecurityConstants.SESSION_KEY_PREFIX, validateCode);
// 发送
this.mobileCodeSendService.send(mobile, validateCode.getCode());
}
}
- 验证码RavenValidateCode
@Setter
@Getter
public class RavenValidateCode {
private String code;
private LocalDateTime expireTime;
public RavenValidateCode(String code, int expireIn) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(this.expireTime);
}
}
- 短信生成器IRavenMobileValidateCodeGenerator
public interface IRavenMobileValidateCodeGenerator {
/**
* 生成验证码
*/
RavenValidateCode generator(ServletWebRequest request);
}
- 默认手机验证码生成器
/**
* 默认的手机验证码生成器
*/
@Setter
public class DefaultRavenMobileValidateCodeGenerator implements IRavenMobileValidateCodeGenerator {
private RavenSecurityProperties securityProperties;
@Override
public RavenValidateCode generator(ServletWebRequest request) {
int expireIn = securityProperties.getValidate().getMobile().getExpireIn();
int count = securityProperties.getValidate().getMobile().getCount();
String code = RandomStringUtils.randomNumeric(count);
return new RavenValidateCode(code, expireIn);
}
}
- 发送验证码到手机 IRavenMobileCodeSendService
public interface IRavenMobileCodeSendService {
/**
* 给手机发送短信
* 默认实现使用服务器随机生成验证码
*/
void send(String mobile, String code);
}
- 默认发送验证码到手机实现 DefaultRavenMobileCodeSendServiceImpl
/**
* 由于 IRavenMobileCodeSendService 需要外接来实现发送短信验证码到手机
* 所以这里就不能直接入住的spring容器中,需要通过Bean的方式来配置注入
*/
public class DefaultRavenMobileCodeSendServiceImpl implements IRavenMobileCodeSendService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void send(String mobile, String code) {
logger.info("发送验证码:" + code + " 到手机:" + mobile + " 请注意查收");
}
}
- 短信验证码生成器、发送短信验证码Bean配置。因为需要外接自己实现,所以就以配置Bean的方式来注入容器
@Configuration
public class RavenBeanConfig {
@Autowired
private RavenSecurityProperties securityProperties;
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 图形验证码生成器
* 由于IRavenValidateCodeGenerator这个接口是为了让外界实现,所以不能在它默认的实现类
* DefaultRavenValidateCodeGenerator上直接使用@Component,防止会造成2个Bean,
* 所以需要使用@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
* 来判断Bean imageValidateCodeGenerator是否存在,如果存在就不会执行下面的Bean
*/
@Bean
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
IRavenValidateCodeGenerator imageValidateCodeGenerator() {
DefaultRavenValidateCodeGenerator generator = new DefaultRavenValidateCodeGenerator();
generator.setSecurityProperties(this.securityProperties);
return generator;
}
/**
* 手机短信验证码生成器
*/
@Bean
@ConditionalOnMissingBean(name = "mobileValidateCodeGenerator")
IRavenMobileValidateCodeGenerator mobileValidateCodeGenerator() {
DefaultRavenMobileValidateCodeGenerator generator = new DefaultRavenMobileValidateCodeGenerator();
generator.setSecurityProperties(this.securityProperties);
return generator;
}
/**
* 发送手机短信
*/
@Bean
@ConditionalOnMissingBean(name = "mobileCodeSendService")
IRavenMobileCodeSendService mobileCodeSendService() {
DefaultRavenMobileCodeSendServiceImpl mobileCodeSendImpl = new DefaultRavenMobileCodeSendServiceImpl();
return mobileCodeSendImpl;
}
}
- 自己实现手机短信的生成
@Component("mobileValidateCodeGenerator")
public class DemoMobileCodeGenerator implements IRavenMobileValidateCodeGenerator {
@Override
public RavenValidateCode generator(ServletWebRequest request) {
return new RavenValidateCode("111", 100);
}
}
- 自己实现手机短信发送
@Component("mobileCodeSend")
public class DemoMobileCodeSend implements IRavenMobileCodeSendService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void send(String mobile, String code) {
logger.info(mobile);
logger.info(code);
}
}
- 配置
/**
* Web端security配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private RavenSecurityProperties securityProperties;
@Autowired
private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
@Autowired
private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
@Autowired
private RavenValidateCodeFilter validateCodeFilter;
@Autowired
private RavenMobileCodeAuthenticationSecurityConfig mobileCodeConfig;
@Autowired
private HikariDataSource hikariDataSource;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(this.hikariDataSource);
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置登录界面
String loginPage = this.securityProperties.getBrowser().getLoginPage();
int tokenTime = this.securityProperties.getBrowser().getTokenTime();
RavenMobileValidateCodeFilter mobileFilter = new RavenMobileValidateCodeFilter();
mobileFilter.setFailureHandler(this.browserAuthenticationFailureHandler);
mobileFilter.setSecurityProperties(this.securityProperties);
mobileFilter.afterPropertiesSet();
http.csrf().disable()
.apply(this.mobileCodeConfig)
.and()
.addFilterBefore(this.validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(mobileFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require") // 当需要身份认证时,跳转到这里
.loginProcessingUrl("/authentication/form") // 默认处理的/login,自定义登录界面需要指定请求路径
.successHandler(this.browserAuthenticationSuccessHandler)
.failureHandler(this.browserAuthenticationFailureHandler)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(tokenTime)
.userDetailsService(this.userDetailsService)
.and()
.authorizeRequests()
.antMatchers("/authentication/require",
loginPage,
"/code/*"
).permitAll()
.anyRequest()
.authenticated();
}
}
网友评论