短信登陆开发
原理
基本原理:SmsAuthenticationFilter接受请求生成SmsAuthenticationToken,然后交给系统的AuthenticationManager进行管理,然后找到SmsAuthenticationProvider,然后再调用UserDetailsService进行短信验证,SmsAuthenticationSecurityConfig进行配置 SmsCaptchaFilter验证码过滤器 在请求之前进行验证验证
短信验证主要是复制参考用户名密码流程,参考前面的源码分析。
代码
SmsAuthenticationToken
-
package com.rui.tiger.auth.core.authentication.mobile;
-
-
import org.springframework.security.authentication.AbstractAuthenticationToken;
-
import org.springframework.security.core.GrantedAuthority;
-
-
import java.util.Collection;
-
-
/**
-
* 手机token
-
* 参照:UsernamePasswordAuthenticationToken
-
* @author CaiRui
-
* @Date 2018/12/15 22:28
-
*/
-
public
class SmsAuthenticationToken extends AbstractAuthenticationToken {
-
private
static
final
long serialVersionUID =
500L;
-
private
final Object principal;
//用户名
-
//private Object credentials; 密码 手机登录验证码在登录前已经验证 考虑手机验证码通用 没有放到这里
-
-
/**
-
* 没有认证成功
-
* @param mobile 手机号
-
*/
-
public SmsAuthenticationToken(Object mobile) {
-
super((Collection)
null);
-
this.principal = mobile;
-
this.setAuthenticated(
false);
-
}
-
-
/**
-
* 认证成功同时进行权限设置
-
* @param principal
-
* @param authorities
-
*/
-
public SmsAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
-
super(authorities);
-
this.principal = principal;
-
super.setAuthenticated(
true);
-
}
-
-
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");
-
}
else {
-
super.setAuthenticated(
false);
-
}
-
}
-
-
public void eraseCredentials() {
-
super.eraseCredentials();
-
}
-
}
SmsAuthenticationFilter
-
package com.rui.tiger.auth.core.authentication.mobile;
-
-
import org.springframework.security.authentication.AuthenticationServiceException;
-
import org.springframework.security.core.Authentication;
-
import org.springframework.security.core.AuthenticationException;
-
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
-
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
-
import org.springframework.util.Assert;
-
-
import javax.servlet.http.HttpServletRequest;
-
import javax.servlet.http.HttpServletResponse;
-
-
/**
-
* 手机登陆过滤器
-
* 参照:UsernamePasswordAuthenticationFilter
-
* @author CaiRui
-
* @Date 2018/12/16 10:39
-
*/
-
public
class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
-
-
// ~ Static fields/initializers
-
// =====================================================================================
-
-
public
static
final String TIGER_SECURITY_FORM_MOBILE_KEY =
"mobile";
-
private String mobileParameter = TIGER_SECURITY_FORM_MOBILE_KEY;
-
//public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
-
// private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
-
private
boolean postOnly =
true;
-
-
// ~ Constructors
-
// ===================================================================================================
-
-
//TODO /authentication/mobile 这些参数应该配置到字典中待优化
-
public SmsAuthenticationFilter() {
-
// 拦截该路径,如果是访问该路径,则标识是需要短信登录
-
super(
new AntPathRequestMatcher(
"/authentication/mobile",
"POST"));
-
}
-
-
// ~ Methods
-
// ========================================================================================================
-
-
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();
-
-
SmsAuthenticationToken authRequest =
new SmsAuthenticationToken(mobile);
-
-
// Allow subclasses to set the "details" property
-
setDetails(request, authRequest);
-
-
return
this.getAuthenticationManager().authenticate(authRequest);
-
}
-
-
-
/**
-
* Enables subclasses to override the composition of the username, such as by
-
* including additional values and a separator.
-
*
-
* @param request so that request attributes can be retrieved
-
*
-
* @return the username that will be presented in the <code>Authentication</code>
-
* request token to the <code>AuthenticationManager</code>
-
*/
-
protected String obtainMobile(HttpServletRequest request) {
-
return request.getParameter(mobileParameter);
-
}
-
-
/**
-
* Provided so that subclasses may configure what is put into the authentication
-
* request's details property.
-
*
-
* @param request that an authentication request is being created for
-
* @param authRequest the authentication request object that should have its details
-
* set
-
*/
-
protected void setDetails(HttpServletRequest request,
-
SmsAuthenticationToken authRequest) {
-
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
-
}
-
-
/**
-
* Sets the parameter name which will be used to obtain the mobile from the login
-
* request.
-
*
-
* @param mobileParameter the parameter name. Defaults to "mobile".
-
*/
-
public void setMobileParameter(String mobileParameter) {
-
Assert.hasText(mobileParameter,
"mobile parameter must not be empty or null");
-
this.mobileParameter = mobileParameter;
-
}
-
-
public String getMobileParameter() {
-
return mobileParameter;
-
}
-
-
/**
-
* Defines whether only HTTP POST requests will be allowed by this filter. If set to
-
* true, and an authentication request is received which is not a POST request, an
-
* exception will be raised immediately and authentication will not be attempted. The
-
* <tt>unsuccessfulAuthentication()</tt> method will be called as if handling a failed
-
* authentication.
-
* <p>
-
* Defaults to <tt>true</tt> but may be overridden by subclasses.
-
*/
-
public void setPostOnly(boolean postOnly) {
-
this.postOnly = postOnly;
-
}
-
-
}
SmsAuthenticationProvider
-
package com.rui.tiger.auth.core.authentication.mobile;
-
-
import lombok.Getter;
-
import lombok.Setter;
-
import org.springframework.security.authentication.AuthenticationProvider;
-
import org.springframework.security.authentication.InternalAuthenticationServiceException;
-
import org.springframework.security.core.Authentication;
-
import org.springframework.security.core.AuthenticationException;
-
import org.springframework.security.core.userdetails.UserDetails;
-
import org.springframework.security.core.userdetails.UserDetailsService;
-
-
/**
-
* @author CaiRui
-
* @Date 2018/12/16 10:38
-
*/
-
public
class SmsAuthenticationProvider implements AuthenticationProvider {
-
@Override
-
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
-
SmsAuthenticationToken smsCaptchaAuthenticationToken= (SmsAuthenticationToken) authentication;
-
UserDetails user=userDetailsService.loadUserByUsername((String) smsCaptchaAuthenticationToken.getPrincipal());
-
if(user==
null){
-
throw
new InternalAuthenticationServiceException(
"无法获取用户信息");
-
}
-
//认证通过
-
SmsAuthenticationToken authenticationTokenResult=
new SmsAuthenticationToken(user,user.getAuthorities());
-
//将之前未认证的请求放进认证后的Token中
-
authenticationTokenResult.setDetails(smsCaptchaAuthenticationToken.getDetails());
-
return authenticationTokenResult;
-
}
-
-
//@Autowired
-
@Getter
-
@Setter
-
private UserDetailsService userDetailsService;
//
-
-
/**
-
* AuthenticationManager 验证该Provider是否支持 认证
-
* @param aClass
-
* @return
-
*/
-
@Override
-
public boolean supports(Class<?> aClass) {
-
return aClass.isAssignableFrom(SmsAuthenticationToken.class);
-
}
-
-
}
SmsAuthenticationSecurityConfig
-
package com.rui.tiger.auth.core.authentication.mobile;
-
-
import org.springframework.beans.factory.annotation.Autowired;
-
import org.springframework.security.authentication.AuthenticationManager;
-
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
-
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-
import org.springframework.security.core.userdetails.UserDetailsService;
-
import org.springframework.security.web.DefaultSecurityFilterChain;
-
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
-
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
-
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-
import org.springframework.stereotype.Component;
-
-
/**
-
* 手机权限配置类
-
* @author CaiRui
-
* @Date 2018/12/16 13:42
-
*/
-
@Component
-
public
class SmsAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
-
-
@Autowired
-
private AuthenticationFailureHandler authenticationFailureHandler;
-
@Autowired
-
private AuthenticationSuccessHandler authenticationSuccessHandler;
-
//实现类怎么确定? 自定义的实现??
-
@Autowired
-
private UserDetailsService userDetailsService;
-
-
@Override
-
public void configure(HttpSecurity http) throws Exception {
-
SmsAuthenticationFilter filter =
new SmsAuthenticationFilter();
-
filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
-
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
-
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
-
-
SmsAuthenticationProvider smsAuthenticationProvider =
new SmsAuthenticationProvider();
-
smsAuthenticationProvider.setUserDetailsService(userDetailsService);
-
-
http
-
// 注册到AuthenticationManager中去
-
.authenticationProvider(smsAuthenticationProvider)
-
// 添加到 UsernamePasswordAuthenticationFilter 之后
-
// 貌似所有的入口都是 UsernamePasswordAuthenticationFilter
-
// 然后UsernamePasswordAuthenticationFilter的provider不支持这个地址的请求
-
// 所以就会落在我们自己的认证过滤器上。完成接下来的认证
-
.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
-
}
-
-
}
SmsCaptchaFilter
-
package com.rui.tiger.auth.core.captcha.sms;
-
-
import com.rui.tiger.auth.core.captcha.CaptchaException;
-
import com.rui.tiger.auth.core.captcha.CaptchaProcessor;
-
import com.rui.tiger.auth.core.captcha.CaptchaVo;
-
import com.rui.tiger.auth.core.captcha.ImageCaptchaVo;
-
import com.rui.tiger.auth.core.properties.SecurityProperties;
-
import lombok.Getter;
-
import lombok.Setter;
-
import lombok.extern.slf4j.Slf4j;
-
import org.apache.commons.lang.StringUtils;
-
import org.springframework.beans.factory.InitializingBean;
-
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
-
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
-
import org.springframework.social.connect.web.SessionStrategy;
-
import org.springframework.util.AntPathMatcher;
-
import org.springframework.web.bind.ServletRequestBindingException;
-
import org.springframework.web.bind.ServletRequestUtils;
-
import org.springframework.web.context.request.ServletWebRequest;
-
import org.springframework.web.filter.OncePerRequestFilter;
-
-
import javax.servlet.FilterChain;
-
import javax.servlet.ServletException;
-
import javax.servlet.http.HttpServletRequest;
-
import javax.servlet.http.HttpServletResponse;
-
import java.io.IOException;
-
import java.util.HashSet;
-
import java.util.Set;
-
import java.util.stream.Collectors;
-
import java.util.stream.Stream;
-
-
/**
-
* 手机验证码过滤器
-
* OncePerRequestFilter 过滤器只会调用一次
-
*
-
* @author CaiRui
-
* @date 2018-12-10 12:23
-
*/
-
@Setter
-
@Getter
-
@Slf4j
-
public
class SmsCaptchaFilter extends OncePerRequestFilter implements InitializingBean {
-
-
//一般在配置类中进行注入
-
-
private AuthenticationFailureHandler failureHandler;
-
-
private SecurityProperties securityProperties;
-
-
/**
-
* 验证码拦截的路径
-
*/
-
private Set<String> interceptUrlSet =
new HashSet<>();
-
-
//session工具类
-
private SessionStrategy sessionStrategy =
new HttpSessionSessionStrategy();
-
//路径匹配工具类
-
private AntPathMatcher antPathMatcher =
new AntPathMatcher();
-
-
/**
-
* @throws ServletException
-
*/
-
-
@Override
-
public void afterPropertiesSet() throws ServletException {
-
super.afterPropertiesSet();
-
//其它配置的需要验证码验证的路径
-
String configInterceptUrl = securityProperties.getCaptcha().getSms().getInterceptUrl();
-
if (StringUtils.isNotBlank(configInterceptUrl)) {
-
String[] configInterceptUrlArray = StringUtils.split(configInterceptUrl,
",");
-
interceptUrlSet = Stream.of(configInterceptUrlArray).collect(Collectors.toSet());
-
}
-
//短信登录请求验证
-
interceptUrlSet.add(
"/authentication/mobile");
-
}
-
-
@Override
-
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
-
-
log.info(
"验证码验证请求路径:[{}]", request.getRequestURI());
-
boolean action =
false;
// 默认不放行
-
for (String url : interceptUrlSet) {
-
if (antPathMatcher.match(url, request.getRequestURI())) {
-
action =
true;
-
}
-
}
-
if (action) {
-
try {
-
validate(request);
-
}
catch (CaptchaException captchaException) {
-
//失败调用我们的自定义失败处理器
-
failureHandler.onAuthenticationFailure(request, response, captchaException);
-
//后续流程终止
-
return;
-
}
-
-
}
-
//后续过滤器继续执行
-
filterChain.doFilter(request, response);
-
}
-
-
/**
-
* 图片验证码校验
-
*
-
* @param request
-
*/
-
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
-
String smsSessionKey=CaptchaProcessor.CAPTCHA_SESSION_KEY+
"sms";
-
// 拿到之前存储的imageCode信息
-
ServletWebRequest swr =
new ServletWebRequest(request);
-
CaptchaVo smsCaptchaInSession = (CaptchaVo) sessionStrategy.getAttribute(swr, smsSessionKey);
-
String codeInRequest = ServletRequestUtils.getStringParameter(request,
"smsCode");
-
-
if (StringUtils.isBlank(codeInRequest)) {
-
throw
new CaptchaException(
"验证码的值不能为空");
-
}
-
if (smsCaptchaInSession ==
null) {
-
throw
new CaptchaException(
"验证码不存在");
-
}
-
if (smsCaptchaInSession.isExpried()) {
-
sessionStrategy.removeAttribute(swr, smsSessionKey);
-
throw
new CaptchaException(
"验证码已过期");
-
}
-
if (!StringUtils.equals(smsCaptchaInSession.getCode(), codeInRequest)) {
-
throw
new CaptchaException(
"验证码不匹配");
-
}
-
//验证通过 移除缓存
-
sessionStrategy.removeAttribute(swr, smsSessionKey);
-
}
-
}
BrowserSecurityConfig 浏览器配置同步调整
-
package com.rui.tiger.auth.browser.config;
-
-
import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
-
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
-
import com.rui.tiger.auth.core.authentication.mobile.SmsAuthenticationSecurityConfig;
-
import com.rui.tiger.auth.core.captcha.CaptchaFilter;
-
import com.rui.tiger.auth.core.captcha.sms.SmsCaptchaFilter;
-
import com.rui.tiger.auth.core.properties.SecurityProperties;
-
import org.springframework.beans.factory.annotation.Autowired;
-
import org.springframework.context.annotation.Bean;
-
import org.springframework.context.annotation.Configuration;
-
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-
import org.springframework.security.core.userdetails.UserDetailsService;
-
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
-
import org.springframework.security.crypto.password.PasswordEncoder;
-
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
-
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
-
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
-
-
import javax.sql.DataSource;
-
-
/**
-
* 浏览器security配置类
-
*
-
* @author CaiRui
-
* @date 2018-12-4 8:41
-
*/
-
@Configuration
-
public
class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
-
-
@Autowired
-
private SecurityProperties securityProperties;
-
@Autowired
-
private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
-
@Autowired
-
private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;
-
@Autowired
-
private DataSource dataSource;
-
@Autowired
-
private UserDetailsService userDetailsService;
-
@Autowired
-
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;
//短信登陆配置
-
-
/**
-
* 密码加密解密
-
*
-
* @return
-
*/
-
@Bean
-
public PasswordEncoder passwordEncoder() {
-
return
new BCryptPasswordEncoder();
-
}
-
-
/**
-
* 记住我持久化数据源
-
* JdbcTokenRepositoryImpl CREATE_TABLE_SQL 建表语句可以先在数据库中执行
-
*
-
* @return
-
*/
-
@Bean
-
public PersistentTokenRepository persistentTokenRepository() {
-
JdbcTokenRepositoryImpl jdbcTokenRepository =
new JdbcTokenRepositoryImpl();
-
jdbcTokenRepository.setDataSource(dataSource);
-
//第一次会执行CREATE_TABLE_SQL建表语句 后续会报错 可以关掉
-
//jdbcTokenRepository.setCreateTableOnStartup(true);
-
return jdbcTokenRepository;
-
}
-
-
-
@Override
-
protected void configure(HttpSecurity http) throws Exception {
-
//加入图片验证码过滤器
-
CaptchaFilter captchaFilter =
new CaptchaFilter();
-
captchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
-
captchaFilter.setSecurityProperties(securityProperties);
-
captchaFilter.afterPropertiesSet();
-
//短信验证码的配置
-
SmsCaptchaFilter smsCaptchaFilter =
new SmsCaptchaFilter();
-
smsCaptchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
-
smsCaptchaFilter.setSecurityProperties(securityProperties);
-
smsCaptchaFilter.afterPropertiesSet();
-
-
//将验证码的过滤器放在登陆的前面
-
http.addFilterBefore(smsCaptchaFilter,UsernamePasswordAuthenticationFilter.class)
-
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
-
.formLogin()
-
.loginPage(
"/authentication/require")
//自定义登录请求
-
.loginProcessingUrl(
"/authentication/form")
//自定义登录表单请求
-
.successHandler(tigerAuthenticationSuccessHandler)
-
.failureHandler(tigerAuthenticationFailureHandler)
-
.and()
-
//记住我相关配置
-
.rememberMe()
-
.tokenRepository(persistentTokenRepository())
-
.tokenValiditySeconds(securityProperties.getBrowser().getRemberMeSeconds())
-
.userDetailsService(userDetailsService)
-
.and()
-
.authorizeRequests()
-
.antMatchers(securityProperties.getBrowser().getLoginPage(),
-
"/authentication/require",
"/captcha/*")
//此路径放行 否则会陷入死循环
-
.permitAll()
-
.anyRequest()
-
.authenticated()
-
.and()
-
.csrf().disable()
//跨域关闭
-
//短信登陆配置挂载
-
.apply(smsAuthenticationSecurityConfig)
-
;
-
}
-
-
}
ok 几个核心类都开发完毕下面我们进行测试下
测试
- 登录页面
- 点击发送短信验证码,后台日志查看验证码
- 返回到登录页面,输入验证码进行登陆确认
- 后台复制真正发送的验证码添加
- 提交短信登录
1.登录
2.发送验证码 后台日志获取
3.输入短信验证码 先输入错误的
4.返回提示
其它各种情况自行调试验证。
总结
文章转载至:https://blog.csdn.net/ahcr1026212/article/details/85028726