Spring Seurity系列(九)短信登录开发

1:原理图

 SmsCodeAuthenticationFilter接受请求生成SmsCodeAuthenticationToken,然后交给系统的AuthenticationManager进行管理,然后找到SmsCodeAuthenticationProvider,然后再调用UserDetailsService进行短信验证。验证码的验证是在请求之前进行验证码过滤验证的。

在这里主要的是SmsCodeAuthenticationToken,SmsCodeAuthenticationFilter,SmsCodeAuthenticationProvider。

2:SmsCodeAuthenticationToken参照UsernamePasswordAuthenticationToken

/**
 * SmsCodeAuthenticationToken
 * @author HZK
 * @date 2018年7月28日 下午10:49:06
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	// ~ Instance fields
	private final Object principal;//存放认证信息。

	// ~ Constructors
	/**
	 * 认证之前存放手机号
	 */
	public SmsCodeAuthenticationToken(Object mobile) {
		super(null);
		this.principal = mobile;
		setAuthenticated(false);
	}

	/**
	 * 认证成功存放用户
	 */
	public SmsCodeAuthenticationToken(Object principal,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() {
		// TODO Auto-generated method stub
		return null;
	}
}

3:SmsCodeAuthenticationFilter参照UsernamePasswordAuthenticationFilter

/**
 * 短信登录的过滤器
 * @author HZK
 * @date 2018年7月28日 下午10:22:39
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	//请求中携带的参数名
	public static final String IMOOC_FORM_MOBILE_KEY = "mobile";
	private String mobileParameter = IMOOC_FORM_MOBILE_KEY;// 请求中携带参数的名字是什么
	private boolean postOnly = true;//是否仅处理post请求
	// ~ Constructors
	//处理的短信登录的请求是什么
	public SmsCodeAuthenticationFilter() {
		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);
		// 将请求的信息设置在Token中
		setDetails(request, authRequest);
		//拿着Token调用AuthenticationManager
		return this.getAuthenticationManager().authenticate(authRequest);
	}
	
	/**
	 * 获取请求参数中的手机号
	 * @param request
	 * @return
	 */
	protected String obtainMobile(HttpServletRequest request) {
		return request.getParameter(mobileParameter);
	}

	/**
	 * 将请求的信息设置在Token中
	 * @param request
	 * @param authRequest
	 */
	protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}

	public void setMobileParameter(String mobileParameter) {
		Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
		this.mobileParameter = mobileParameter;
	}

	public void setPostOnly(boolean postOnly) {
		this.postOnly = postOnly;
	}

	public final String getMobileParameter() {
		return mobileParameter;
	}
}

4:SmsCodeAuthenticationProvider

/**
 * SmsCodeAuthenticationProvider
 * @author HZK
 * @date 2018年7月28日 下午11:47:14
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider{
	
	private UserDetailsService userDetailsService;
	/**
	 * 身份认证的逻辑
	 */
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken)authentication;
		UserDetails user = userDetailsService.loadUserByUsername((String) smsCodeAuthenticationToken.getPrincipal());
		if(user == null){
			throw new InternalAuthenticationServiceException("无法获取用户信息");
		}
		/**
		 * 1:用户认证信息
		 * 2:用户权限
		 */
		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
		
		//将之前未认证的请求放进认证后的Token中
		authenticationResult.setDetails(smsCodeAuthenticationToken);
		
		return authenticationResult;
	}
	
	/**
	 * AuthenticationManager带着Token调用Provider
	 * 判断传进来的Token最终调用的是哪个Provider
	 */
	@Override
	public boolean supports(Class<?> authentication) {
		return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
	}

	public UserDetailsService getUserDetailsService() {
		return userDetailsService;
	}

	public void setUserDetailsService(UserDetailsService userDetailsService) {
		this.userDetailsService = userDetailsService;
	}
}

5:SmsCodeFilter复制ValidateCodeFilte进行修改,后面会重构

public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
	
	private AuthenticationFailureHandler authenticationFailureHandler;
	
	private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
	
	private Set<String> urls = new HashSet<>();
	
	private SecurityProperties securityProperties;
	
	private AntPathMatcher pathMatcher = new AntPathMatcher();
	
	@Override
	public void afterPropertiesSet() throws ServletException {
		super.afterPropertiesSet();
		String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
		if(ArrayUtils.isNotEmpty(configUrls)){
			for (String configUrl : configUrls) {
				urls.add(configUrl);
			}
		}
		urls.add("/authentication/mobile");
	}

	/* (non-Javadoc)
	 * @see org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain)
	 */
	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {
		
		boolean action = false;
		for (String url : urls) {
			if(pathMatcher.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);
		
	}

	private void validate(ServletWebRequest request) throws ServletRequestBindingException {
		
		ValidateCode codeInSession = (ValidateCode) sessionStrategy.getAttribute(request,
				ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
		
		//请求中的短信验证码的值
		String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");

		if (StringUtils.isBlank(codeInRequest)) {
			throw new ValidateCodeException("验证码的值不能为空");
		}
		
		if(codeInSession == null){
			throw new ValidateCodeException("验证码不存在");
		}

		if(codeInSession.isExpried()){
			sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
			throw new ValidateCodeException("验证码已过期");
		}
		
		if(!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
			throw new ValidateCodeException("验证码不匹配");
		}
		
		sessionStrategy.removeAttribute(request, ValidateCodeProcessor.SESSION_KEY_PREFIX+"SMS");
	}

	public AuthenticationFailureHandler getAuthenticationFailureHandler() {
		return authenticationFailureHandler;
	}

	public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
		this.authenticationFailureHandler = authenticationFailureHandler;
	}

	public SessionStrategy getSessionStrategy() {
		return sessionStrategy;
	}

	public void setSessionStrategy(SessionStrategy sessionStrategy) {
		this.sessionStrategy = sessionStrategy;
	}

	public Set<String> getUrls() {
		return urls;
	}

	public void setUrls(Set<String> urls) {
		this.urls = urls;
	}

	public SecurityProperties getSecurityProperties() {
		return securityProperties;
	}

	public void setSecurityProperties(SecurityProperties securityProperties) {
		this.securityProperties = securityProperties;
	}
}

6:下面是将短信验证码和短信登录的相关类进行的配置类

/**
 * 将前面的三个类穿在一起
 * 在浏览器也要在app中使用
 * @author HZK
 * @date 2018年7月28日 下午11:33:07
 */
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
	
	@Autowired
	private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
	
	@Autowired
	private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
	
	@Autowired
	private UserDetailsService userDetailsService;
	
	@Override
	public void configure(HttpSecurity http) throws Exception {
		
		SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
		//AuthenticationManager
		smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		//AuthenticationSuccessHandler
		smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
		//AuthenticationFailureHandler
		smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
		
		SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
		smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
		
		//smsCodeAuthenticationProvider设置在AuthenticationManager管理的集合上面
		http.authenticationProvider(smsCodeAuthenticationProvider)
			.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
	}
}
/**
 * 安全配置类
 * @author zhailiang
 *
 */
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private SecurityProperties securityProperties;
	
	@Autowired
	private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
	
	@Autowired
	private AuthenticationFailureHandler imoocAuthenctiationFailureHandler;
	
	@Autowired
	private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
	
	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		
		//验证码的配置
		ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
		validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenctiationFailureHandler);
		validateCodeFilter.setSecurityProperties(securityProperties);
		validateCodeFilter.afterPropertiesSet();
		
		//短信验证码的配置
		SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
		smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenctiationFailureHandler);
		smsCodeFilter.setSecurityProperties(securityProperties);
		smsCodeFilter.afterPropertiesSet();

		//将验证码的过滤器放在登录验证过滤器之前
		http.addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
			.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
			.formLogin()
			.loginPage("/authentication/require")
			.loginProcessingUrl("/authentication/form")
			.successHandler(imoocAuthenticationSuccessHandler)//登录成功的处理
			.failureHandler(imoocAuthenctiationFailureHandler)//登录失败的处理
			.and()
			.authorizeRequests()
			.antMatchers("/authentication/require",
					securityProperties.getBrowser().getLoginPage(),
					"/code/image").permitAll()
			.anyRequest()
			.authenticated()
			.and()
			.csrf().disable()
			//将前面创建的配置类添加在安全配置类后面
			.apply(smsCodeAuthenticationSecurityConfig);
		
	}

}

至此短信登录开发完成,下一篇将对其进行代码的重构。

猜你喜欢

转载自blog.csdn.net/newhanzhe/article/details/81266010