Spring Security parse (four) - messages Log in development

Spring Security parse (four) - messages Log in development

  When learning Spring Cloud, met oauth related content authorization service, always scanty, it was decided to first Spring Security, Spring Security Oauth2 and other rights related to certification content, learning theory and design and organize it again. This series of articles is to strengthen the impression that in the process of learning and understanding written, if infringement, please inform.

Project environment:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

First, how to achieve on the basis of Security on the SMS login feature?

  Security under review the implementation process login form:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190902225110478-797444218.jpg

  From the process, we found that there is special treatment or that have other sisters subclass implemented during logon
:

  • AuthenticationFilter: for intercepting login request;
  • Unauthenticated Authentication object, as the authentication method of the reference;
  • AuthenticationProvider authentication process.

  So we can fully customize by a SmsAuthenticationFilter to intercept a SmsAuthenticationToken to transmit authentication data, a SmsAuthenticationProvider authentication business processes. Since we know UsernamePasswordAuthenticationFilter of doFilter by AbstractAuthenticationProcessingFilter to achieve, but UsernamePasswordAuthenticationFilter itself only implements attemptAuthentication () method. According to this arrangement, our SmsAuthenticationFilter only achieve attemptAuthentication () method, how to verify code it? Then we need to call a verification code to achieve filtration filter before SmsAuthenticationFilter: ValidateCodeFilter . After finishing the process to achieve the following figure:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190902225110744-1309305475.jpg

Second, SMS Login Certified Developer

(A) SmsAuthenticationFilter achieve

  After analog UsernamePasswordAuthenticationFilter SmsAuthenticationFilter achieve its code is as follows:

@EqualsAndHashCode(callSuper = true)
@Data
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 获取request中传递手机号的参数名
    private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;

    private boolean postOnly = true;

    // 构造函数,主要配置其拦截器要拦截的请求地址url
    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "POST"));
    }


    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        // 判断请求是否为 POST 方式
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 调用 obtainMobile 方法从request中获取手机号
        String mobile = obtainMobile(request);

        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        // 创建 未认证的  SmsCodeAuthenticationToken  对象
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        setDetails(request, authRequest);
        
        // 调用 认证方法
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * 获取手机号
     */
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
    
    /**
     * 原封不动照搬UsernamePasswordAuthenticationFilter 的实现 (注意这里是 SmsCodeAuthenticationToken  )
     */
    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    /**
     * 开放设置 RemmemberMeServices 的set方法
     */
    @Override
    public void setRememberMeServices(RememberMeServices rememberMeServices) {
        super.setRememberMeServices(rememberMeServices);
    }
}

Its internal implementation there are several attention points:

  • Set parameters property transfer phone numbers
  • Constructor methods have parameters call the parent class, which is provided to be mainly used to intercept url
  • UsernamePasswordAuthenticationFilter achieve copy of attemptAuthentication (), the internal need of rehabilitation have 2 points: 1, obtainMobile get the phone number information 2, create an object SmsCodeAuthenticationToken
  • In order to achieve messages Log also have to remember my function here open setRememberMeServices () method is used to set rememberMeServices.

(B) SmsAuthenticationToken achieve

  We simulated the same UsernamePasswordAuthenticationToken achieve SmsAuthenticationToken:

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    private final Object principal;

    /**
     * 未认证时,内容为手机号
     * @param mobile
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    /**
     *
     * 认证成功后,其中为用户信息
     *
     * @param principal
     * @param authorities
     */
    public SmsCodeAuthenticationToken(Object principal,
                                      Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    }

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

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

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

  Contrast UsernamePasswordAuthenticationToken, we have reduced the credentials (can be understood as a password), the other is basically intact.

(C) SmsAuthenticationProvider achieve

  Since SmsCodeAuthenticationProvider is a whole new different authentication entrusted to achieve, so that we write according to their own ideas, without having to refer DaoAuthenticationProvider. Look at the code we own realization:

@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

        if (user == null) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }

        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

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

  Which interface method implemented by the direct successor AuthenticationProvider authenticate () and supports (). the Supports () we write directly to the Provider reference to another, this is mainly determined whether the currently processed Authentication SmsCodeAuthenticationToken or subclass. authenticate () we directly call the loadUserByUsername userDetailsService () method is simple to implement because the code has been verified by the ValidateCodeFilter, so here we just pass the phone number and find information the user would be directly sentenced to top the success of the current user authentication, and generates certified SmsCodeAuthenticationToken return.

(D) ValidateCodeFilter achieve

   Just as ValidateCodeFilter verification codes only our previously described, where we set the acquisition codes generated verification code to compare the user input through redis:

@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    /**
     * 验证码校验失败处理器
     */
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    /**
     * 系统配置信息
     */
    @Autowired
    private SecurityProperties securityProperties;

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 存放所有需要校验验证码的url
     */
    private Map<String, String> urlMap = new HashMap<>();
    /**
     * 验证请求url与配置的url是否匹配的工具类
     */
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * 初始化要拦截的url配置信息
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();

        urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS);
        addUrlToMap(securityProperties.getSms().getSendSmsUrl(), SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS);
    }

    /**
     * 讲系统中配置的需要校验验证码的URL根据校验的类型放入map
     *
     * @param urlString
     * @param smsParam
     */
    protected void addUrlToMap(String urlString, String smsParam) {
        if (StringUtils.isNotBlank(urlString)) {
            String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
            for (String url : urls) {
                urlMap.put(url, smsParam);
            }
        }
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {

        String code = request.getParameter(getValidateCode(request));
        if (code != null) {
            try {
                String oldCode = stringRedisTemplate.opsForValue().get(request.getParameter(SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE));
                if (StringUtils.equalsIgnoreCase(oldCode,code)) {
                    logger.info("验证码校验通过");
                } else {
                    throw new ValidateCodeException("验证码失效或错误!");
                }
            } catch (AuthenticationException e) {
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * 获取校验码
     *
     * @param request
     * @return
     */
    private String getValidateCode(HttpServletRequest request) {
        String result = null;
        if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
            Set<String> urls = urlMap.keySet();
            for (String url : urls) {
                if (pathMatcher.match(url, request.getRequestURI())) {
                    result = urlMap.get(url);
                }
            }
        }
        return result;
    }
}

Here the main look doFilterInternal codes validation logic can be implemented.

Third, how to set up an SMS Filter added to the entry into force of FilterChain it?

Here we need to introduce new configuration class SmsCodeAuthenticationSecurityConfig, its codes are as follows:

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler ;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        // 设置 AuthenticationManager
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // 分别设置成功和失败处理器
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        // 设置 RememberMeServices
        smsCodeAuthenticationFilter.setRememberMeServices(http
                .getSharedObject(RememberMeServices.class));

        // 创建 SmsCodeAuthenticationProvider 并设置 userDetailsService
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        // 将Provider添加到其中
        http.authenticationProvider(smsCodeAuthenticationProvider)
                // 将过滤器添加到UsernamePasswordAuthenticationFilter后面
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }

Finally, we need to refer to the SpringSecurityConfig configuration class SmsCodeAuthenticationSecurityConfig:

http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class)
                .apply(smsCodeAuthenticationSecurityConfig)
                . ...

Fourth, the new interface to send a verification code and verification code login form

   Send new verification code Interface (primarily set to no access):

    @GetMapping("/send/sms/{mobile}")
    public void sendSms(@PathVariable String mobile) {
        // 随机生成 6 位的数字串
        String code = RandomStringUtils.randomNumeric(6);
        // 通过 stringRedisTemplate 缓存到redis中 
        stringRedisTemplate.opsForValue().set(mobile, code, 60 * 5, TimeUnit.SECONDS);
        // 模拟发送短信验证码
        log.info("向手机: " + mobile + " 发送短信验证码是: " + code);
    }

   Add code to sign in form:

// 注意这里的请求接口要与 SmsAuthenticationFilter的构造函数 设置的一致
<form action="/loginByMobile" method="post">
    <table>
        <tr>
            <td>手机号:</td>
            <td><input type="text" name="mobile" value="15680659123"></td>
        </tr>
        <tr>
            <td>短信验证码:</td>
            <td>
                <input type="text" name="smsCode">
                <a href="/send/sms/15680659123">发送验证码</a>
            </td>
        </tr>
        <tr>
            <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>记住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登录</button>
            </td>
        </tr>
    </table>
</form>

Fifth, personal summary

  In fact, to achieve another login, the key point in the filter, AuthenticationToken, AuthenticationProvider these three points. Is sorted out: a custom SmsAuthenticationFilter , intercepting a AuthenticationToken to transmit authentication data, a AuthenticationProvider authentication business processes. Since we know UsernamePasswordAuthenticationFilter of doFilter by AbstractAuthenticationProcessingFilter to achieve, but UsernamePasswordAuthenticationFilter itself only implements attemptAuthentication () method. According to this arrangement, our AuthenticationFilter only achieve attemptAuthentication () method, but at the same time need to call a filter to achieve validation filter before AuthenticationFilter: ValidatFilter . As same as the following flowchart, any login may be added in this way:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190902225110744-1309305475.jpg

   This article describes the development of the code SMS login code repository can be accessed in the security module, github address projects: https://github.com/BUG9/spring-security

         If you are interested in these, welcomed the star, follow, bookmarking, forwarding support!

Guess you like

Origin www.cnblogs.com/bug9/p/11449573.html