SpringSecurity短信验证码接口发送
in JavaDevelop with 0 comment

SpringSecurity短信验证码接口发送

in JavaDevelop with 0 comment

短信验证码接口发送

实现短信验证码登录

这里的套路与之前图形验证码的套路类似

开发短信验证码接口

@RestController
public class ValidateCodeController {
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ValidateCodeGenerate imageCodeGenerate;

    @Autowired
    private ValidateCodeGenerate smsCodeGenerate;

    @Autowired
    private SmsCodeSender smsCodeSender;


    @GetMapping("/code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
        ValidateCode validateCode = smsCodeGenerate.generate(request);
        String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, validateCode.getCode());
        smsCodeSender.send(mobile, validateCode.getCode());
    }

配置类

@Configuration
public class ValidateCodeConfig {
    @Autowired
    private SecurityProperties securityProperties;

    // spring 容器中如果存在imageCodeGenerate的bean就不会再初始化该bean了
    // 条件注解
    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerate")
    public ValidateCodeGenerate imageCodeGenerate() {
        ImageCodeGenerate imageCodeGenerate = new ImageCodeGenerate(securityProperties.getCode().getImage());
        return imageCodeGenerate;
    }

    // 这里由于产生了多个ValidateCodeGenerate的实现类
    // 所以需要使用name来区分
    // 在注入的时候也需要用其他手段与该name相同的id注入才可以
    // 当然还有其他的方式。可能可以使用:不同的子接口来分离短信和图形接口
    // 比如 @Qualifier("imageCodeGenerate") 或则什么的参数名和这个相同
    @Bean
    @ConditionalOnMissingBean(name = "smsCodeGenerate")
    public ValidateCodeGenerate smsCodeGenerate() {
        SmsCodeGenerate smsCodeGenerate = new SmsCodeGenerate(securityProperties.getCode().getSms());
        return smsCodeGenerate;
    }

    @Bean
    @ConditionalOnMissingBean(DefaultSmsCodeSender.class)
    public SmsCodeSender defaultSmsCodeSender() {
        return new DefaultSmsCodeSender();
    }
}

短信验证码生成类

package cn.mrcode.imooc.springsecurity.securitycore.validate.code.sms;

import cn.mrcode.imooc.springsecurity.securitycore.properties.SmsCodeProperties;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCode;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeGenerate;
import org.apache.commons.lang3.RandomStringUtils;

import javax.servlet.http.HttpServletRequest;

public class SmsCodeGenerate implements ValidateCodeGenerate {
    private SmsCodeProperties smsCodeProperties;

    public SmsCodeGenerate(SmsCodeProperties smsCodeProperties) {
        this.smsCodeProperties = smsCodeProperties;
    }

    @Override
    public ValidateCode generate(HttpServletRequest request) {
        int count = smsCodeProperties.getLength();
        int expireIn = smsCodeProperties.getExpireIn();
        String smsCode = RandomStringUtils.randomNumeric(count);
        return new ValidateCode(smsCode, expireIn);
    }

这里目前没有什么特别的,都是伪代码,提供一种思路。
还有就是贴出来的代码与之前图形验证码的部分代码重合了,就重构了;
由于发现有好多逻辑也是重复的。进行深度重构抽象。验证码处理器结构如下:

重构验证码逻辑

虚线是调用

上图逻辑清晰,看着么多类,实际上是把变化的部分抽象成接口了,公共的逻辑使用模版方法模式封装起来了;
以后可以应对不同的变化,比如:

  1. 图形验证码或则短信验证码的 生成逻辑变了,提供ValidateCodeGenerator实现类即可
  2. 图形或则短信验证码的响应/发送逻辑变了,提供AbstractValidateCodeProcessor的子类实现 abstract void send发送方法

经过上面的思路分析,就是把变化的流程单独拿出来了。

这里涉及到一个spring中的开发技巧:依赖查找

/**
 * <pre>
 * 收集系统中所有 {@link ValidateCodeGenerate} 接口的实现
 * spring开发技巧-依赖查找:
 *  spring会查找所有ValidateCodeGenerate的实现
 *  beanName做为key,实现作为value注入这里
 * </pre>
 */
@Autowired
private Map<String, ValidateCodeGenerate> validateCodeGenerates;

在 AbstractValidateCodeProcessor 中声明的该参数;再来看下在其他地方是怎么初始化的

@Configuration
public class ValidateCodeConfig {

  @Bean
  @ConditionalOnMissingBean(name = "imageCodeGenerate")
  public ValidateCodeGenerate imageCodeGenerate() {
      ImageCodeGenerate imageCodeGenerate = new ImageCodeGenerate(securityProperties.getCode().getImage());
      return imageCodeGenerate;
  }

  @Bean
  @ConditionalOnMissingBean(name = "smsCodeGenerate")
  public ValidateCodeGenerate smsCodeGenerate() {
      SmsCodeGenerate smsCodeGenerate = new SmsCodeGenerate(securityProperties.getCode().getSms());
      return smsCodeGenerate;
  }
}

上面使用了 条件排除,可以看出来这里有一个限制,就是短信和图形验证码的生成接口都使用的同一个;

那么这里在排除的时候就只能写上beanName来限制了;

对于上面的依赖查找技巧不会产生任何问题,但是对于使用处想替换该实现的时候。

对于bean的name只能是覆盖这里的同名name。否则就会出现配置不成功的问题

注意1:这里重构之后,会对 之前过滤器中的一部分代码有问题。
比如过滤器中图片验证码验证的地方SESSION_KEY的获取,目前处理是写死key的名称

注意2:这里只是针对发送接口做调整;不是验证功能;
仔细体会,这个接口的开发思路和使用的技巧超越自己好多年了

短信登录开发

短信登录和 用户名密码登录的逻辑不同(现在也不知道为什么不同,跟着走吧),不能和之前的写在一起

这里模仿它的原理进行另外一条线,加入短信登录的认证(注意不是验证);不是验证发送的短信验证码;

之前写的图形验证码是在 UsernamePasswordAuthenticationFilter前增加了我们自己的图形验证过滤器,
验证成功之后再交给用户名和密码进行认证,调用userDetailsService进行匹配验证;

最后通过的话,会进入Authentication已认证流程;

短信认证的思路和上面一样:

  1. SmsCodeAuthenticationFilter 短信登录请求

  2. SmsCodeAuthenticationProvider 提供短信登录处理的实现类

  3. SmsCodeAuthenticationToken 存放认证信息(包括未认证前的参数信息传递)

  4. 最后开发一个过滤器放在 短信登录请求之前,进行短信验证码的验证,

    因为这个过滤器只关心提交的验证码是否正常就行了。所以可以应用到任意业务中,对任意业务提交进行短信的验证

SmsCodeAuthenticationToken

package cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * 编写思路:直接复制参考 UsernamePasswordAuthenticationToken 的写法
 * 分析哪些需要哪些是不需要的。包括功能
 * @author zhuqiang
 * @version 1.0.1 2018/8/4 17:54
 * @date 2018/8/4 17:54
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    // ~ Instance fields
    // ================================================================================================

    /** 存放用户名 : credentials 字段去掉,因为短信认证在授权认证前已经过滤了 */
    private final Object principal;

    // ~ Constructors
    // ===================================================================================================

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     * @param principal
     * @param authorities
     */
    public SmsCodeAuthenticationToken(Object principal,
                                      Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }


    // ~ Methods
    // ========================================================================================================
    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    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();
    }
}

SmsCodeAuthenticationFilter

package cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile;

import org.springframework.security.authentication.AuthenticationServiceException;
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;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 短信验证码验证: 直接仿照UsernamePasswordAuthenticationFilter
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/5 9:29
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    // ~ Static fields/initializers
    // =====================================================================================

    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    private boolean postOnly = true;

    // ~ Constructors
    // ===================================================================================================

    public SmsCodeAuthenticationFilter() {
        // 拦截该路径,如果是访问该路径,则标识是需要短信登录
        super(new AntPathRequestMatcher("/authentication/sms", "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
        // 把request里面的一些信息copy近token里面
        // 后面认证成功的时候还需要copy这信息到新的token
        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

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

SmsCodeAuthenticationProvider

这个没有找到仿照的地方。没有发现和usernamePassword类型的提供provider

package cn.mrcode.imooc.springsecurity.securitycore.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;

/**
 * 短信处理器
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/5 9:20
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
        UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal());
        if(user == null){
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
        // 需要把未认证中的一些信息copy到已认证的token中
        authenticationResult.setDetails(token);
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

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

SmsCodeAuthenticationSecurityConfig

需要的几个东西已经准备好了。这里要进行配置把这些加入到 security的认证流程中去;

package cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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;

/**
 * app和浏览器都需要使用,短信验证配置
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/5 9:59
 */
@Component
public class SmsCodeAuthenticationSecurityConfig
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
        // 这两个设置参数 不知道从哪里来的
        // 难道是要去看一个源码?
        // 把该过滤器交给管理器
        // 图上流程,因为最先走的 短信认证的过滤器(不是验证码,只是认证)
        // 要使用管理器来获取provider,所以把管理器注册进去
        filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        filter.setAuthenticationFailureHandler(authenticationFailureHandler);
        filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        http
                // 注册到AuthenticationManager中去
                .authenticationProvider(smsCodeAuthenticationProvider)
                // 添加到 UsernamePasswordAuthenticationFilter 之后
                // 貌似所有的入口都是 UsernamePasswordAuthenticationFilter
                // 然后UsernamePasswordAuthenticationFilter的provider不支持这个地址的请求
                // 所以就会落在我们自己的认证过滤器上。完成接下来的认证
                .addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
    }
}

BrowserSecurityConfig 应用方配置

变化的配置用注释标出来了。无变化的把注释去掉了;

package cn.mrcode.imooc.springsecurity.securitybrowser;

import cn.mrcode.imooc.springsecurity.securitycore.authentication.mobile.SmsCodeAuthenticationSecurityConfig;
import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.SmsCodeFilter;
import cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeFilter;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/3 0:05
 */

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private DataSource dataSource;
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;
    @Autowired
    private UserDetailsService userDetailsService;

    // 由下面的  .apply(smsCodeAuthenticationSecurityConfigs)方法添加这个配置
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();

        // 短信的是copy图形的过滤器,这里直接copy初始化
        SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
        smsCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
        smsCodeFilter.setSecurityProperties(securityProperties);
        smsCodeFilter.afterPropertiesSet();
        http
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                // 在这里不能注册到我们自己的短信认证过滤器上,会报错
                .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                .rememberMe()
                .tokenRepository(persistentTokenRepository)
                .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())
                .userDetailsService(userDetailsService)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require",
                        securityProperties.getBrowser().getLoginPage(),
                        "/code/*",
                        "/error"
                )
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()
                // 这里应用短信认证配置
                .apply(smsCodeAuthenticationSecurityConfigs)
        ;
    }
}

登录页面再回顾下表单的提交代码

/authentication/sms 登录地址,就是我们认证过滤器里面的支持地址;

<h3>短信验证码</h3>
<form action="/authentication/sms" method="post">
    <table>
        <tr>
            <td>手机号:</td>
            <td><input type="text" name="mobile"></td>
        </tr>
        <tr>
            <td>短信验证码:</td>
            <td>
                <input type="text" name="smsCode">
                <a href="/code/sms?mobile=13012345678">发送验证码</a>
            </td>
        </tr>
        <tr>
            <td>
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>

测试

  1. 登录页面
  2. 点击发送短信验证码
  3. 返回到登录页面
  4. 后台复制真正发送的验证码添加
  5. 提交短信登录

总结

自定义认证逻辑的配置大致步骤:

  1. 入口配置 应用方使用该配置 .apply(smsCodeAuthenticationSecurityConfigs)
  2. 提供处理过滤器 ProcessingFilter 并限制该过滤器支持拦截的url
  3. 提供AuthenticationProvider 进行认证的处理支持
  4. 把ProviderManager 赋值给 ProcessingFilter
  5. 把AuthenticationProvider注册到AuthenticationManager中去

(这里完成ProcessingFilter调用管理器查找Provider,完成认证这个过程)
6. 把 ProcessingFilter 添加到 认证处理链中 ,之后

(也就是UsernamePasswordAuthenticationFilter)

自定义验证码验证逻辑的配置大致步骤

  1. 入口配置 应用方把验证码(验证是否有效,是否过期)的过滤器添加到认证处理链中 之前

(也就是UsernamePasswordAuthenticationFilter),就是在进入认证之前线把验证码是否有效先验证了
这里发现 认证和部分业务逻辑是不一致的。无关的直接分离;
比如这里的验证过滤器,只管提交的短信验证码在当前的session中是否有效;
而后面的短信认证登录,只是根据用户名去获取用户详细信息,并返回;
而默认的用户名密码过认证登录的逻辑是:根据用户名获取用户详细信息,然后再比对密码是否ok;
从这里可以看出来,短信认证的其实也可以写在认证逻辑里面,这样分离出去是不是就做成公用的了?

  1. 提供验证码过的过滤器 里面的逻辑要配合 验证码发送服务中的存储方式进行获取发送的验证码

从这里可以看出来。认证中涉及到业务逻辑了:配置发送验证服务中的逻辑进行获取验证码相关信息

短信登录配置及重构

重构思路:

  1. 重构不是更改已有的功能

  2. 重构是不影响已有功能的情况下,对已有代码进行抽象封装

  3. 多处使用相同代码的地方,需要抽出来

  4. 比如上章节的很多代码,

    如:图形验证码过滤器和短信验证码过滤器重复代码太多
    服务接口的url地址和过滤器中的过滤器地址重复
    等…

系统配置相关的代码结构

core项目中的重构如下:

browser项目:

app:

通过配置apply功能进行配置的引用

感受

花了6个小时看老师重构之后的代码,然后完成了自己跟练的项目代码;

太厉害!!这个重构技巧太牛逼了;

总之:当有两处重复代码的时候 就要抽取代码了。这个需要大量的经验才能不分类好,不至于越抽越乱

这里再啰嗦下:
关于用户名密码登录和短信登录表单提交的url地址,不需要真实存在,
因为这个是提供这两个特定过滤器框架特定的拦截点。只有提交到指定的拦截点,
才会进入认证功能服务

此次重构一些知识点

善用 HttpSecurity.apply 应用分离之后的配置类

 HttpSecurity.apply 方法跟踪进来是父类的;这里是一个泛型,所有需要看HttpSecurity对应传递的是什么类型
public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>>
		extends AbstractSecurityBuilder<O> {
      public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer)
        throws Exception {
        configurer.addObjectPostProcessor(objectPostProcessor);
        configurer.setBuilder((B) this);
        add(configurer);
        return configurer;
      }

--------- HttpSecurity 声明
public final class HttpSecurity extends
		AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
		implements SecurityBuilder<DefaultSecurityFilterChain>,
		HttpSecurityBuilder<HttpSecurity> {

------------ 注意看对比
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer

AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>

这里的c对应的泛型就是 <O, B> ,而O,B对应到HttpSecurity的声明就是<DefaultSecurityFilterChain, HttpSecurity>

HttpSecurity.apply 返回一个SecurityConfigurerAdapter<O, B>,所以这里只要继承该类,就是apply需要的对象了

------------- 如下示例
/**
 * 验证码配置
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/5 20:05
 */
@Component
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    /**
     * @see ValidateCodeFilter  目前融合了短信和图形验证码的验证功能
     */
    @Autowired
    private Filter validateCodeFilter;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 由源码得知,在最前面的是UsernamePasswordAuthenticationFilter
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

SecurityConstants 常量接口类

在重构中就已经发现这样做的好处了。因为这个代码被分离写和配置的,老是忘记在哪些地方用过
在修改的时候经常忘记修改,或则找不到,所以需要抽成常量类

public interface SecurityConstants {

	/**
	 * 默认的处理验证码的url前缀
	 */
	public static final String DEFAULT_VALIDATE_CODE_URL_PREFIX = "/code";

善用 Autowired注解提供的 依赖查找功能

把多个实现类统一管理,特别是使用模板方法抽取公用逻辑的时候,就拍上用处了
如下代码,还提供了按beanName查找指定的子类实现;
还提供了按自定义类型,下面会讲到善用命名会在某些地方起到奇效

/**
 * 处理器持有者,用来管理所有验证码类型的处理器
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/5 20:40
 */
@Component
public class ValidateCodeProcessorHolder {
    @Autowired
    private Map<String, ValidateCodeProcessor> validateCodeProcessors;

    public ValidateCodeProcessor findValidateCodeProcessor(ValidateCodeType type) {
        return findValidateCodeProcessor(type.toString().toLowerCase());
    }

    public ValidateCodeProcessor findValidateCodeProcessor(String type) {
        String beanName = type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();
        ValidateCodeProcessor processor = validateCodeProcessors.get(beanName);
        if (processor == null) {
            throw new ValidateCodeException("验证码处理器 " + beanName + " 不存在");
        }
        return processor;
    }
}

善用 类名统一起名

如这里的几个类

这里的前缀,配合上面的技巧 善用 Autowired注解提供的 依赖查找功能,使用以下代码就能方便的获取到对应的处理器

他们都一个共同的父类,有公用的步骤,变化的部分由子类实现;
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {
  /**
   * 根据请求的url获取校验码的类型:
   * ValidateCodeProcessorHolder : 中持有所有本类的子类型,获取getClass能拿到具体的实例类名
   * @return
   * @see ValidateCodeProcessorHolder
   */
  private ValidateCodeType getValidateCodeType() {
      // 处理器 命名规则:ImageValidateCodeProcessor,拿到前缀即可
      // 返回 Image
      String type = StringUtils.substringBefore(getClass().getSimpleName(), ValidateCodeProcessor.class.getSimpleName());
      return ValidateCodeType.valueOf(type.toUpperCase());
  }
}

在外部使用 type + ValidateCodeProcessor.class.getSimpleName() 就能获取到完整的类名,
也就能使用ValidateCodeProcessorHolder动态的获取处理器了

善用枚举类提供相应的支持

枚举类的名称是 短信和图片验证功能的前缀。配合上面的几条。
在使用模板方法模式抽取公用逻辑的时候,可以使用前缀获取不同功能支持的动态常量等类容
在外部要动态使用服务的时候,也能用前缀+具体的的父类命名获取到

public enum ValidateCodeType {
    /**
     * 短信验证码
     */
    SMS {
        @Override
        public String getParamNameOnValidate() {
            return SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS;
        }
    },
    /**
     * 图片验证码
     */
    IMAGE {
        @Override
        public String getParamNameOnValidate() {
            return SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_IMAGE;
        }
    };

    /**
     * 校验时从请求中获取的参数的名字
     * @return
     */
    public abstract String getParamNameOnValidate();
}