文章目录

  • 原理
  • 1.实现短信登录
  • 1.1编写短信验证码的过滤器。
  • 1.2编写用来封装短信的Token
  • 1.3编写处理短信Token,所需要用到的Provider,可以仿照(用户名和密码)的provider的逻辑来写。
  • 1.4编写短信验证码的校验
  • 2.将上述的逻辑加入到一起,放在http.中去。

原理

spring secrity 短信验证码 spring security短信验证码登陆_验证码


逻辑:先将(用户名和密码)或者(手机号)组装成未认证的Token。传给AuthenticationManager,然后从一堆的AuthenticationProvider中挑选适合的Provider。来处理认证请求。挑选的依据:根据provider中的support的方法是否支持传递进来的Token。在认证的过程中会调用UserdetailService来获取用户(存在数据库中的)的信息,然后与传递进来的用户信息进行比对。如果正确的话,就将Authentication表示为已认证。

而验证短信验证码和图形验证码是否匹配都需要在传递请求之前,通过添加过滤器进行处理。这种处理方式可以重用,在多处使用。

1.实现短信登录

1.1编写短信验证码的过滤器。

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	public static final String WU_FORM_MOBILE_KEY = "mobile";
	private String mobileParameter = "mobile";
	private boolean postOnly = true;  //只处理Post请求。

	//请求的匹配器。
	public SmsCodeAuthenticationFilter() {
		super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
	}

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
		if (this.postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		} else {
			//取出手机号
			String mobile = this.obtainMobile(request);
			if (mobile == null) {
				mobile = "";
			}
			//去除空格
			mobile = mobile.trim();
			//这里封装未认证的Token
			SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
			//将请求信息也放入到Token中。
			this.setDetails(request, authRequest);
			/首先进入方法这里会找到我们自己写的SmsCodeAuthenticationProvider.
			/最后将结果放回到这里之后,经过AbstractAuthenticationProcessingFilter,这个抽象类的doFilter,然后调用处理器。成功调用成功处理器,失败调用失败处理器。
			return this.getAuthenticationManager().authenticate(authRequest);
		}
	}

	/**
	 * 获取手机号的方法
	 * @param request
	 * @return
	 */
	protected String obtainMobile(HttpServletRequest request) {
		return request.getParameter(this.mobileParameter);
	}
	//将请求信息也放入到Token中。
	protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
		authRequest.setDetails(this.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 mobileParameter() {
		return this.mobileParameter;
	}

}

1.2编写用来封装短信的Token

**封装登录信息,身份认证之前传递的是手机号,认证成功之后:放的是用户信息。
 * @author zhailiang
 *
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

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

	//mobile:表示手机号。
	public SmsCodeAuthenticationToken(String 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
	}


	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");
		}

		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
	}
}

1.3编写处理短信Token,所需要用到的Provider,可以仿照(用户名和密码)的provider的逻辑来写。

@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

	private UserDetailsService userDetailsService;

	//身份认证的逻辑
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
		//根据手机号(Principal)去查用户信息
		UserDetails userDetails = userDetailsService.loadUserByUsername((String) authentication.getPrincipal());
		if (userDetails == null){
			throw new InternalAuthenticationServiceException("无法获取用户信息");
		}
		//将认证信息传入进去。
		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,userDetails.getAuthorities());
		//将请求的信息传递Token中。
		authenticationResult.setDetails(authenticationToken.getDetails());
		return authenticationResult;
	}
	//用于选去AuthenticationProvider的主要方法。
	@Override
	public boolean supports(Class<?> authentication) {
		return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
	}
}

1.4编写短信验证码的校验

@Data
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
    private AuthenticationFailureHandler failureHandler;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    private Set<String> urls = new HashSet<>();
    private SecurityProperties securityProperties;
    private AuthenticationFailureHandler authenticationFailureHandler;
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * 初始化过滤器要拦截的路径:urls
     * @throws ServletException
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(),",");
        if (configUrls != null && configUrls.length > 0){
            for (String configUrl : configUrls) {
                urls.add(configUrl);
            }
        }
        urls.add("/mobile/login");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        boolean flag = false;
        for (String url : urls) {
            //如果请求的路径在此过滤器要拦截的路径里面,则进行验证
            if (pathMatcher.match(url,request.getRequestURI())){
                flag = true;
            }
        }
        if (flag){
            try {
                validate(new ServletWebRequest(request));
            } catch (ImageCodeException e) {
                //验证出现异常,使用自定义的失败处理器来处理,并且直接return,不执行后面的过滤器
                failureHandler.onAuthenticationFailure(request,response,e);
                return;
            }
        }
        filterChain.doFilter(request,response);
    }

    /**
     * 短信验证码的验证逻辑
     * @param request
     */
    private void validate(ServletWebRequest request) {
        ValidateCode codeInSession = (ValidateCode)sessionStrategy.getAttribute(request, ValidateCodeController.SMS_SESSION_KEY);
        if (codeInSession != null){
            System.out.println("session中的验证码:"+codeInSession.getCode());
            String codeInRequest = null;
            try {
                codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
                System.out.println("请求中的验证码:"+codeInRequest);
            } catch (ServletRequestBindingException e) {
                System.out.println(e.getMessage());
            }
            if (codeInSession.isExpried()){
                sessionStrategy.removeAttribute(request, ValidateCodeController.SMS_SESSION_KEY);
                throw new ImageCodeException("验证码已过期");
            }
            if (StringUtils.isBlank(codeInRequest)){
                throw new ImageCodeException("验证码不能为空");
            }
            if (!StringUtils.equals(codeInSession.getCode(),codeInRequest)){
                throw new ImageCodeException("验证错误");
            }
            //不抛出异常,验证码正确,删除保存的验证码
            sessionStrategy.removeAttribute(request, ValidateCodeController.SMS_SESSION_KEY);
        } else {
            throw new ImageCodeException("请先获取验证码");
        }
    }
}

2.将上述的逻辑加入到一起,放在http.中去。

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
	
	@Autowired
	private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
	
	@Autowired
	private AuthenticationFailureHandler myAuthenticationFailureHandler;
	
	@Autowired
	private SmsLoginService smsLoginService;
	
	@Override
	public void configure(HttpSecurity http) throws Exception {
		
		SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
		//1.因为这个过滤器需要manager进行认证,所以先设置该Mannager,在Manager中查找适合的Provider
		smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		//设置失败处理器,成功处理器
		smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
		smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
		
		SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
		//用userLoginService来读取用户信息。
		smsCodeAuthenticationProvider.setUserDetailsService(smsLoginService);

		//将我们自己写的Provider加入到AuthenticationManager管的这个集合里面去。
		http.authenticationProvider(smsCodeAuthenticationProvider)
			.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
	}

}

先将上面写的配置类,自动注入之后。然后在public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter这个配置类的最后写上.apply(smsCodeAuthenticationSecurityConfig)将上面的配置类也加到配置中来。

@Autowired
  private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

spring secrity 短信验证码 spring security短信验证码登陆_ci_02