spring-security-oauth2(八) 短信密码登录重构

短信密码登录重构

  • 抽取重复的代码
  • 将一些变量用常量或枚举类管理起来
  • 对一些流程进行抽象封装,方便扩展

比如前面我们的短信和验证码过滤器流程很相似,还有系统置不够清晰糅杂在一起了等。

关于用户名密码登录和短信登录表单提交的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 说明我们的配置通过了,其它请自行测试。

总结: 

猜你喜欢

转载自blog.csdn.net/ahcr1026212/article/details/85244082