2023-06-18
原文作者:代码有毒 mrcode 原文地址:https://mrcode.blog.csdn.net/article/details/81942345

短信登录开发

202306181902569641.png

上图,可见要自己扩展的明白原理流程;

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

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

之前写的图形验证码是在 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">
        | 手机号:||
| :-----: | :-----: | 
| 手机号: |  | 
| 短信验证码: | 发送验证码 | 
| 登录 | 

    </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;
    从这里可以看出来,短信认证的其实也可以写在认证逻辑里面,这样分离出去是不是就做成公用的了?

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

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

阅读全文