短信密码登录重构
- 抽取重复的代码
- 将一些变量用常量或枚举类管理起来
- 对一些流程进行抽象封装,方便扩展
比如前面我们的短信和验证码过滤器流程很相似,还有系统置不够清晰糅杂在一起了等。
关于用户名密码登录和短信登录表单提交的url地址,不需要真实存在,
因为这个是提供这两个特定过滤器框架特定的拦截点。只有提交到指定的拦截点,
才会进入认证功能服务。
重构
验证码过滤器重构
CaptchaUnionFilter 将短信和图形验证码的过滤器合并成一个 ,将原来的2个过滤器删除
package com.rui.tiger.auth.core.captcha;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.properties.SecurityConstants;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
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.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 重构的验证码拦截器 (图片验证+短信验证)
*
* @author CaiRui
* @date 2018-12-25 8:37
*/
@Component("captchaUnionFilter")
@Slf4j
public class CaptchaUnionFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
@Autowired
private CaptchaProcessorHolder captchaProcessorHolder;
/**
* 存放所有需要校验验证码的url
* key: 验证码类型
* value: 验证路径
*/
private Map<String, CaptchaTypeEnum> urlMap = new HashMap<>();
/**
* 验证请求url与配置的url是否匹配的工具类
*/
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* bean初始化后调用
*
* @throws ServletException
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
//短信验证码
urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, CaptchaTypeEnum.SMS);
addVaildateUrlToUrlMap(securityProperties.getCaptcha().getSms().getInterceptUrl(), CaptchaTypeEnum.SMS);
//图片验证码
urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM, CaptchaTypeEnum.IMAGE);
addVaildateUrlToUrlMap(securityProperties.getCaptcha().getImage().getInterceptImageUrl(), CaptchaTypeEnum.IMAGE);
}
/**
* 验证码拦截核心逻辑
*
* @param request
* @param response
* @param filterChain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
CaptchaTypeEnum captchaTypeEnum = getCaptchaTypeWithRequestUrl(request);
if (captchaTypeEnum != null) {
try {
log.info("校验请求【" + request.getRequestURI() + "】" + captchaTypeEnum.getDesc() + "验证码");
captchaProcessorHolder.findCaptchaProcessor(captchaTypeEnum)
.validate(new ServletWebRequest(request, response),captchaTypeEnum);
} catch (CaptchaException captchaException) {
log.info("验证码校验异常", captchaException.getMessage());
tigerAuthenticationFailureHandler.onAuthenticationFailure(request, response, captchaException);
return;
}
//filterChain.doFilter(request, response); 就是null后续都不执行 被坑了很久
}
filterChain.doFilter(request, response);
}
/**
* 根据请求路径返回验证码类型
*
* @param request
* @return
*/
private CaptchaTypeEnum getCaptchaTypeWithRequestUrl(HttpServletRequest request) {
String requestUrl = request.getRequestURI();//返回除去host(域名或者ip)部分的路径
if (!StringUtils.equalsIgnoreCase("get", request.getMethod())) {
Set<String> urlSet = urlMap.keySet();
for (String url : urlSet) {
if (pathMatcher.match(url, requestUrl)) {
return urlMap.get(url);
}
}
}
return null;
}
/**
* 不同类型拦截路径赋值
*
* @param interceptUrl
* @param captchaTypeEnum
*/
private void addVaildateUrlToUrlMap(String interceptUrl, CaptchaTypeEnum captchaTypeEnum) {
if (StringUtils.isNotBlank(interceptUrl)) {
String[] interceptUrlArray = StringUtils.split(interceptUrl, ",");
for (String url : interceptUrlArray) {
urlMap.put(url, captchaTypeEnum);
}
}
}
}
CaptchaProcessorHolder 验证码处理器持有者 根据枚举类型查找验证码处理器
package com.rui.tiger.auth.core.captcha;
import com.rui.tiger.auth.core.support.strategy.StrategyContainerImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* CaptchaProcessor接口 持有者
*
* @author CaiRui
* @date 2018-12-25 9:02
*/
@Component
@Slf4j
public class CaptchaProcessorHolder {
/**
* 获取CaptchaProcessor接口实现类
*
* @param name
* @return
*/
CaptchaProcessor findCaptchaProcessor(String name) {
CaptchaTypeEnum captchaTypeEnum = CaptchaTypeEnum.forCode(name);
if (captchaTypeEnum == null) {
log.error("验证码类型枚举" + name + "不存在");
throw new CaptchaException("验证码类型枚举类" + name + "不存在");
}
return findCaptchaProcessor(captchaTypeEnum);
}
/**
* 获取CaptchaProcessor 接口实现类
*
* @param captchaTypeEnum
* @return
*/
CaptchaProcessor findCaptchaProcessor(CaptchaTypeEnum captchaTypeEnum) {
if (captchaTypeEnum == null) {
throw new CaptchaException("验证码类型枚举类不存在");
}
CaptchaProcessor captchaProcessor = StrategyContainerImpl.getStrategy(CaptchaProcessor.class, captchaTypeEnum);
if (captchaProcessor == null) {
log.error("{}处理器不存在", captchaTypeEnum.getDesc());
throw new CaptchaException(captchaTypeEnum.getDesc() + "处理器不存在");
}
log.info("{}处理器获取", captchaTypeEnum.getDesc());
return captchaProcessor;
}
}
CaptchaProcessor 验证码处理器接口 新增校验逻辑
package com.rui.tiger.auth.core.captcha;
import com.rui.tiger.auth.core.support.strategy.IStrategy;
import org.springframework.web.context.request.ServletWebRequest;
/**
* 验证码处理器接口
* @author CaiRui
* @Date 2018/12/15 17:53
*/
public interface CaptchaProcessor extends IStrategy<CaptchaTypeEnum> {
/**
* 验证码缓存KEY值前缀
*/
String CAPTCHA_SESSION_KEY="captcha_session_key_";
/**
* 创建验证码
* @param request 封装请求和响应
* @throws Exception
*/
void create(ServletWebRequest request) throws Exception;
/**
* 校验验证码
* @param servletWebRequest
* @param captchaTypeEnum
*/
void validate(ServletWebRequest servletWebRequest, CaptchaTypeEnum captchaTypeEnum) throws CaptchaException ;
}
AbstractCaptchaProcessor 将验证逻辑放到抽象父类中进行统一验证
package com.rui.tiger.auth.core.captcha;
import org.apache.commons.lang.StringUtils;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import java.io.IOException;
/**
* 验证码处理器抽象父类
*
* @author CaiRui
* @Date 2018/12/15 18:21
*/
public abstract class AbstractCaptchaProcessor<C extends CaptchaVo> implements CaptchaProcessor {
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 创建验证码
*
* @param request 封装请求和响应
* @throws Exception
*/
@Override
public void create(ServletWebRequest request) throws Exception {
//生成
C captcha = generateCaptcha(request);
//保存
save(request, captcha);
//发送
send(request, captcha);
}
/**
* 短信和手机验证码的通用验证
*
* @param request
* @param captchaType 验证码
*/
@Override
public void validate(ServletWebRequest request, CaptchaTypeEnum captchaType) throws CaptchaException {
String sessionKey = getSessionKey(captchaType);
C captchaInSession = (C) sessionStrategy.getAttribute(request, sessionKey);
String captchaInRequest;
try {
captchaInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),
captchaType.getParamNameOnValidate());
} catch (ServletRequestBindingException e) {
throw new CaptchaException("获取验证码的值失败");
}
if (StringUtils.isBlank(captchaInRequest)) {
throw new CaptchaException(captchaType + "验证码的值不能为空");
}
if (captchaInSession == null) {
throw new CaptchaException(captchaType + "验证码不存在");
}
if (captchaInSession.isExpried()) {
sessionStrategy.removeAttribute(request, sessionKey);
throw new CaptchaException(captchaType + "验证码已过期");
}
if (!StringUtils.equals(captchaInSession.getCode(), captchaInRequest)) {
throw new CaptchaException(captchaType + "验证码不匹配");
}
//验证成功清除缓存中的key
sessionStrategy.removeAttribute(request, sessionKey);
}
/**
* 生成验证码
* @param request
* @return
*/
protected abstract C generateCaptcha(ServletWebRequest request);
/**
* 发送验证码
* @param request
* @param captcha
*/
protected abstract void send(ServletWebRequest request, C captcha) throws IOException, ServletRequestBindingException;
/**
* 保存验证码到session中
* @param request
* @param captcha
*/
private void save(ServletWebRequest request, C captcha) {
sessionStrategy.setAttribute(request, CAPTCHA_SESSION_KEY +getCondition().getCode(), captcha);
}
/**
* 获取验证码session key值
*
* @param captchaType
* @return
*/
private String getSessionKey(CaptchaTypeEnum captchaType) {
return CAPTCHA_SESSION_KEY + captchaType.getCode();
}
}
配置重构
AbstractChannelSecurityConfig 密码登录的抽取到core项目中 这个是浏览器和app项目共同的配置项
package com.rui.tiger.auth.core.config;
import com.rui.tiger.auth.core.properties.SecurityConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
/**
* 密码登录的通用安全配置
* @author CaiRui
* @date 2018-12-26 18:11
*/
public class AbstractChannelSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler tigerAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler tigerAuthenticationFailureHandler;
/**
* 密码登录配置
* @param http
* @throws Exception
*/
protected void applyPasswordAuthenticationConfig(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM)//
.successHandler(tigerAuthenticationSuccessHandler)
.failureHandler(tigerAuthenticationFailureHandler);
}
}
CaptchaSecurityConfig 验证码过滤器配置
package com.rui.tiger.auth.core.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
/**
* 验证码过滤器配置
* @author CaiRui
* @date 2018-12-26 18:22
*/
@Component("captchaSecurityConfig")
public class CaptchaSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
/**
* 重构的验证码拦截器 (图片验证+短信验证)
*/
@Autowired
private Filter captchaUnionFilter;
/**
* 验证码验证放在密码登录之前
* @param http
* @throws Exception
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(captchaUnionFilter, UsernamePasswordAuthenticationFilter.class);
}
}
BrowserSecurityConfig 浏览器配置只保留自己特有的如记住我等
package com.rui.tiger.auth.browser.config;
import com.rui.tiger.auth.core.config.AbstractChannelSecurityConfig;
import com.rui.tiger.auth.core.config.CaptchaSecurityConfig;
import com.rui.tiger.auth.core.config.SmsAuthenticationSecurityConfig;
import com.rui.tiger.auth.core.properties.SecurityConstants;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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 AbstractChannelSecurityConfig {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private SmsAuthenticationSecurityConfig smsAuthenticationSecurityConfig;//短信登陆配置
@Autowired
private CaptchaSecurityConfig captchaSecurityConfig;//验证码配置
/**
* 密码加密解密
*
* @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;
}
/**
* 核心配置
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* 表单密码配置
*/
applyPasswordAuthenticationConfig(http);
http
.apply(captchaSecurityConfig)
.and()
.apply(smsAuthenticationSecurityConfig)
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(securityProperties.getBrowser().getRemberMeSeconds())
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
.antMatchers(
SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,//权限认证
SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE,//手机
securityProperties.getBrowser().getLoginPage(),//登录页面
SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*")// /captcha/* 验证码放行
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable();
}
}
常量字典
SecurityConstants 权限常量配置类
package com.rui.tiger.auth.core.properties;
/**
* 权限常量配置类
* @author CaiRui
* @date 2018-12-25 8:52
*/
public class SecurityConstants {
/**
* 默认的处理验证码的url前缀
*/
public static final String DEFAULT_VALIDATE_CODE_URL_PREFIX = "/captcha";
/**
* 当请求需要身份认证时,默认跳转的url
*
*/
public static final String DEFAULT_UNAUTHENTICATION_URL = "/authentication/require";
/**
* 默认的用户名密码登录请求处理url
*/
public static final String DEFAULT_LOGIN_PROCESSING_URL_FORM = "/authentication/form";
/**
* 默认的手机验证码登录请求处理url
*/
public static final String DEFAULT_LOGIN_PROCESSING_URL_MOBILE = "/authentication/mobile";
/**
* 默认登录页面
*
*/
public static final String DEFAULT_LOGIN_PAGE_URL = "/tiger-login.html";
/**
* 验证图片验证码时,http请求中默认的携带图片验证码信息的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_CODE_IMAGE = "imageCode";
/**
* 验证短信验证码时,http请求中默认的携带短信验证码信息的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_CODE_SMS = "smsCode";
/**
* 发送短信验证码 或 验证短信验证码时,传递手机号的参数的名称
*/
public static final String DEFAULT_PARAMETER_NAME_MOBILE = "mobile";
}
ok 重构核心代码完成 我们来看下测试结果,首先打开登录界面,在没有发送验证码前先随便填个短信验证码试试
输入短信验证码看看
ok 说明我们的配置通过了,其它请自行测试。