Spring Security构建Rest服务-0702-短信验证码登录

时间:2022-11-24 13:03:50

先来看下 Spring Security密码登录大概流程,模拟这个流程,开发短信登录流程

Spring Security构建Rest服务-0702-短信验证码登录

1,密码登录请求发送给过滤器 UsernamePasswordAuthenticationFilter

2,过滤器拿出用户名密码组装成 UsernamePasswordAuthenticationToken 对象传给AuthenticationManager

3,AuthenticationManager 会从一堆 AuthenticationProvider 里选出一个Provider 处理认证请求。挑选的依据是AuthenticationProvider 里有个

boolean supports(Class<?> authentication);方法,判断当前的provider是否支持传进的token,如果支持就用这个provider认证这个token,并调用authenticate() 方法 进行认证

4,认证过程会调用UserDetailsService获取用户信息,跟传进来的登录信息做比对。认证通过会把UsernamePasswordAuthenticationToken做一个标识   标记已认证,放进session。

做短信登录,不能在这个流程上改,这是两种不同的登录方式,混在一起代码质量不好,需要仿照这个流程写一套自己的流程:

Spring Security构建Rest服务-0702-短信验证码登录

SmsAuthenticationFilter:拦截短信登录请求,从请求中获取手机号,封装成 SmsAuthenticationToken 也会传给AuthenticationManager,AuthenticationManager会找适合的provider,自定义SmsAuthenticationProvider校验SmsAuthenticationToken 里手机号信息。也会调UserDetailsService 看是否能登录,能的话标记为已登录。

其中SmsAuthenticationFilter 参考UsernamePasswordAuthenticationFilter写,SmsCodeAuthenticationToken参考UsernamePasswordAuthenticationToken写,其实就是就是复制粘贴改改

从上图可知,需要写三个类:

1,SmsAuthenticationToken:复制UsernamePasswordAuthenticationToken,把没用的去掉

package com.imooc.security.core.authentication.mobile;

import java.util.Collection;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion; /**
* 模仿UsernamePasswordAuthenticationToken写的短信登录token
* ClassName: SmsCodeAuthenticationToken
* @Description: TODO
* @author lihaoyang
* @date 2018年3月7日
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; //没登陆,放手机号,登录成功,放用户信息
private final Object principal; /**
* 没登录放手机号
* <p>Description: </p>
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;//没登录放手机号
setAuthenticated(false);//没登录
} public SmsCodeAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
} // ~ Methods
// ======================================================================================================== 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();
} @Override
public Object getCredentials() {
return null;
}
}

2,SmsCodeAuthenticationFilter,参考UsernamePasswordAuthenticationFilter

package com.imooc.security.core.authentication.mobile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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; /**
* 模仿UsernamePasswordAuthenticationFilter 写的短信验证码过滤器
* ClassName: SmsCodeAuthenticationFilter
* @Description: TODO
* @author lihaoyang
* @date 2018年3月8日
*/
public class SmsCodeAuthenticationFilter extends
AbstractAuthenticationProcessingFilter{ public static final String IMOOC_FORM_MOBILE_KEY = "mobile"; private String mobilePatameter = IMOOC_FORM_MOBILE_KEY;
private boolean postOnly = true; // ~ Constructors
// =================================================================================================== public SmsCodeAuthenticationFilter() {
//过滤的请求url,登录表单的url
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(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // Allow subclasses to set the "details" property
setDetails(request, authRequest);
//在这里把SmsCodeAuthenticationToken交给AuthenticationManager
return this.getAuthenticationManager().authenticate(authRequest);
} /**
* 获取手机号
* @Description: TODO
* @param @param request
* @param @return
* @return String
* @throws
* @author lihaoyang
* @date 2018年3月7日
*/
private String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobilePatameter);
} protected void setDetails(HttpServletRequest request,
SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
} public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
} }

3,SmsCodeAuthenticationProvider:

在 SmsCodeAuthenticationFilter 里 attemptAuthentication方法的最后, return this.getAuthenticationManager().authenticate(authRequest);这句话就是进到 SmsCodeAuthenticationProvider 先调用 supports() 方法,通过后,再调用 authenticate()方法进行认证

package com.imooc.security.core.authentication.mobile;

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; /**
* AuthenticationManager 认证时候需要用的一个Provider
* ClassName: SmsCodeAuthenticationProvider
* @Description: TODO
* @author lihaoyang
* @date 2018年3月8日
*/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; /**
* 认证
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//能进到这说明authentication是SmsCodeAuthenticationToken,转一下
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
//token.getPrincipal()就是手机号 mobile
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); //认证没通过
if(user == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
//认证通过
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
//把认证之前得token里存的用户信息赋值给认证后的token对象
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
} /**
* 告诉AuthenticationManager,如果是SmsCodeAuthenticationToken的话用这个类处理
*/
@Override
public boolean supports(Class<?> authentication) {
//判断传进来的authentication是不是SmsCodeAuthenticationToken类型的
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
} public UserDetailsService getUserDetailsService() {
return userDetailsService;
} public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
} }

短信 验证码过滤器,照着图片验证码过滤器写,其实可以重构,不会弄:

package com.imooc.security.core.validate.code;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set; import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 com.imooc.security.core.properties.SecurityConstants;
import com.imooc.security.core.properties.SecurityProperties; /**
* 短信验证码过滤器
* ClassName: ValidateCodeFilter
* @Description:
* 继承OncePerRequestFilter:spring提供的工具,保证过滤器每次只会被调用一次
* 实现 InitializingBean接口的目的:
* 在其他参数都组装完毕的时候,初始化需要拦截的urls的值
* @author lihaoyang
* @date 2018年3月2日
*/
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean{ private Logger logger = LoggerFactory.getLogger(getClass()); //认证失败处理器
private AuthenticationFailureHandler authenticationFailureHandler; //获取session工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); //需要拦截的url集合
private Set<String> urls = new HashSet<>();
//读取配置
private SecurityProperties securityProperties;
//spring工具类
private AntPathMatcher antPathMatcher = new AntPathMatcher(); /**
* 重写InitializingBean的方法,设置需要拦截的urls
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//读取配置的拦截的urls
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
//如果配置了需要验证码拦截的url,不判断,如果没有配置会空指针
if(configUrls != null && configUrls.length > 0){
for (String configUrl : configUrls) {
logger.info("ValidateCodeFilter.afterPropertiesSet()--->配置了验证码拦截接口:"+configUrl);
urls.add(configUrl);
}
}else{
logger.info("----->没有配置拦验证码拦截接口<-------");
}
//短信验证码登录一定拦截
urls.add(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE);
} @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//如果是 登录请求 则执行
// if(StringUtils.equals("/authentication/form", request.getRequestURI())
// &&StringUtils.equalsIgnoreCase(request.getMethod(), "post")){
// try {
// validate(new ServletWebRequest(request));
// } catch (ValidateCodeException e) {
// //调用错误处理器,最终调用自己的
// authenticationFailureHandler.onAuthenticationFailure(request, response, e);
// return ;//结束方法,不再调用过滤器链
// }
// } /**
* 可配置的验证码校验
* 判断请求的url和配置的是否有匹配的,匹配上了就过滤
*/
boolean action = false;
for(String url:urls){
if(antPathMatcher.match(url, request.getRequestURI())){
action = true;
}
}
if(action){
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
//调用错误处理器,最终调用自己的
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return ;//结束方法,不再调用过滤器链
}
} //不是登录请求,调用其它过滤器链
filterChain.doFilter(request, response);
} /**
* 校验验证码
* @Description: 校验验证码
* @param @param request
* @param @throws ServletRequestBindingException
* @return void
* @throws ValidateCodeException
* @author lihaoyang
* @date 2018年3月2日
*/
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
//拿出session中的ImageCode对象
ValidateCode smsCodeInSession = (ValidateCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
//拿出请求中的验证码
String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
//校验
if(StringUtils.isBlank(imageCodeInRequest)){
throw new ValidateCodeException("验证码不能为空");
}
if(smsCodeInSession == null){
throw new ValidateCodeException("验证码不存在,请刷新验证码");
}
if(smsCodeInSession.isExpired()){
//从session移除过期的验证码
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
throw new ValidateCodeException("验证码已过期,请刷新验证码");
}
if(!StringUtils.equalsIgnoreCase(smsCodeInSession.getCode(), imageCodeInRequest)){
throw new ValidateCodeException("验证码错误");
}
//验证通过,移除session中验证码
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
} public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
} public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
} public SecurityProperties getSecurityProperties() {
return securityProperties;
} public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
} }

Spring Security构建Rest服务-0702-短信验证码登录

把新建的这三个类做下配置,让spring security知道

SmsCodeAuthenticationSecurityConfig:

package com.imooc.security.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; /**
* 短信验证码配置
* ClassName: SmsCodeAuthenticationSecurityConfig
* @Description: TODO
* @author lihaoyang
* @date 2018年3月8日
*/
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler; @Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler; @Autowired
private UserDetailsService userDetailsService; @Override
public void configure(HttpSecurity http) throws Exception {
//1,配置短信验证码过滤器
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
//设置认证失败成功处理器
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler); //配置pprovider
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
} }

最后在BrowserSecurityConfig里配置短信验证码

@Configuration //这是一个配置
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{ //读取用户配置的登录页配置
@Autowired
private SecurityProperties securityProperties; //自定义的登录成功后的处理器
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler; //自定义的认证失败后的处理器
@Autowired
private AuthenticationFailureHandler imoocAuthenticationFailureHandler; //数据源
@Autowired
private DataSource dataSource; @Autowired
private UserDetailsService userDetailsService; @Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Autowired
private SpringSocialConfigurer imoocSocialSecurityConfig; //注意是org.springframework.security.crypto.password.PasswordEncoder
@Bean
public PasswordEncoder passwordencoder(){
//BCryptPasswordEncoder implements PasswordEncoder
return new BCryptPasswordEncoder();
} /**
* 记住我TokenRepository配置,在登录成功后执行
* 登录成功后往数据库存token的
* @Description: 记住我TokenRepository配置
* @param @return JdbcTokenRepositoryImpl
* @return PersistentTokenRepository
* @throws
* @author lihaoyang
* @date 2018年3月5日
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
//启动时自动生成相应表,可以在JdbcTokenRepositoryImpl里自己执行CREATE_TABLE_SQL脚本生成表
//第二次启动表已存在,需要注释
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
} //版本二:可配置的登录页
@Override
protected void configure(HttpSecurity http) throws Exception {
//~~~-------------> 图片验证码过滤器 <------------------
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
//验证码过滤器中使用自己的错误处理
validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
//配置的验证码过滤url
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet(); //~~~-------------> 短信验证码过滤器 <------------------
SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
//验证码过滤器中使用自己的错误处理
smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
//配置的验证码过滤url
smsCodeFilter.setSecurityProperties(securityProperties);
smsCodeFilter.afterPropertiesSet(); //实现需要认证的接口跳转表单登录,安全=认证+授权
//http.httpBasic() //这个就是默认的弹框认证
//
http
.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
// .apply(imoocSocialSecurityConfig)//社交登录
// .and()
//把验证码过滤器加载登录过滤器前边
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
//表单认证相关配置
.formLogin()
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL) //处理用户认证BrowserSecurityController
//登录过滤器UsernamePasswordAuthenticationFilter默认登录的url是"/login",在这能改
.loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)
.successHandler(imoocAuthenticationSuccessHandler)//自定义的认证后处理器
.failureHandler(imoocAuthenticationFailureHandler) //登录失败后的处理
.and()
//记住我相关配置
.rememberMe()
.tokenRepository(persistentTokenRepository())//TokenRepository,登录成功后往数据库存token的
.tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())//记住我秒数
.userDetailsService(userDetailsService) //记住我成功后,调用userDetailsService查询用户信息
.and()
//授权相关的配置
.authorizeRequests()
// /authentication/require:处理登录,securityProperties.getBrowser().getLoginPage():用户配置的登录页
.antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
securityProperties.getBrowser().getLoginPage(),//放过登录页不过滤,否则报错
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*").permitAll() //验证码
.anyRequest() //任何请求
.authenticated() //都需要身份认证
.and()
.csrf().disable() //关闭csrf防护
.apply(smsCodeAuthenticationSecurityConfig);//把短信验证码配置应用上
}
}

访问登陆页,点击发送验证码模拟发送验证码

Spring Security构建Rest服务-0702-短信验证码登录

输入后台打印的验证码

Spring Security构建Rest服务-0702-短信验证码登录

登录成功:

Spring Security构建Rest服务-0702-短信验证码登录

Spring Security构建Rest服务-0702-短信验证码登录

完整代码在github:https://github.com/lhy1234/spring-security