zoukankan      html  css  js  c++  java
  • Spring Security构建Rest服务-0702-短信验证码登录

    先来看下 Spring Security密码登录大概流程,模拟这个流程,开发短信登录流程

    1,密码登录请求发送给过滤器 UsernamePasswordAuthenticationFilter 

    2,过滤器拿出用户名密码组装成 UsernamePasswordAuthenticationToken 对象传给AuthenticationManager

    3,AuthenticationManager 会从一堆 AuthenticationProvider 里选出一个Provider 处理认证请求。挑选的依据是AuthenticationProvider 里有个

    boolean supports(Class<?> authentication);方法,判断当前的provider是否支持传进的token,如果支持就用这个provider认证这个token,并调用authenticate() 方法 进行认证

    4,认证过程会调用UserDetailsService获取用户信息,跟传进来的登录信息做比对。认证通过会把UsernamePasswordAuthenticationToken做一个标识   标记已认证,放进session。

    做短信登录,不能在这个流程上改,这是两种不同的登录方式,混在一起代码质量不好,需要仿照这个流程写一套自己的流程:

    SmsAuthenticationFilter:拦截短信登录请求,从请求中获取手机号,封装成 SmsAuthenticationToken 也会传给AuthenticationManager,AuthenticationManager会找适合的provider,自定义SmsAuthenticationProvider校验SmsAuthenticationToken 里手机号信息。也会调UserDetailsService 看是否能登录,能的话标记为已登录。

    其中SmsAuthenticationFilter 参考UsernamePasswordAuthenticationFilter写,SmsCodeAuthenticationToken参考UsernamePasswordAuthenticationToken写,其实就是就是复制粘贴改改

    从上图可知,需要写三个类:

    1,SmsAuthenticationToken:复制UsernamePasswordAuthenticationToken,把没用的去掉

    package com.imooc.security.core.authentication.mobile;
    
    import java.util.Collection;
    
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.SpringSecurityCoreVersion;
    
    /**
     * 模仿UsernamePasswordAuthenticationToken写的短信登录token
     * ClassName: SmsCodeAuthenticationToken 
     * @Description: TODO
     * @author lihaoyang
     * @date 2018年3月7日
     */
    public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
    
        //没登陆,放手机号,登录成功,放用户信息
        private final Object principal;
    
    
    
        /**
         * 没登录放手机号
         * <p>Description: </p>
         * @param mobile
         */
        public SmsCodeAuthenticationToken(String mobile) {
            super(null);
            this.principal = mobile;//没登录放手机号
            setAuthenticated(false);//没登录
        }
    
        
        public SmsCodeAuthenticationToken(Object principal, Object credentials,
                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() {
            return null;
        }
    }

    2,SmsCodeAuthenticationFilter,参考UsernamePasswordAuthenticationFilter

    package com.imooc.security.core.authentication.mobile;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
    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;
    
    /**
     * 模仿UsernamePasswordAuthenticationFilter 写的短信验证码过滤器
     * ClassName: SmsCodeAuthenticationFilter 
     * @Description: TODO
     * @author lihaoyang
     * @date 2018年3月8日
     */
    public class SmsCodeAuthenticationFilter extends
    AbstractAuthenticationProcessingFilter{
        
        public static final String IMOOC_FORM_MOBILE_KEY = "mobile";
    
        private String mobilePatameter = IMOOC_FORM_MOBILE_KEY;
        private boolean postOnly = true;
    
        // ~ Constructors
        // ===================================================================================================
    
        public SmsCodeAuthenticationFilter() {
            //过滤的请求url,登录表单的url
            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);
    
            // Allow subclasses to set the "details" property
            setDetails(request, authRequest);
            //在这里把SmsCodeAuthenticationToken交给AuthenticationManager
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    
        /**
         * 获取手机号
         * @Description: TODO
         * @param @param request
         * @param @return   
         * @return String  
         * @throws
         * @author lihaoyang
         * @date 2018年3月7日
         */
        private String obtainMobile(HttpServletRequest request) {
            return request.getParameter(mobilePatameter);
        }
    
        protected void setDetails(HttpServletRequest request,
                SmsCodeAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
    
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        
    
    }

    3,SmsCodeAuthenticationProvider:

     在 SmsCodeAuthenticationFilter 里 attemptAuthentication方法的最后, return this.getAuthenticationManager().authenticate(authRequest);这句话就是进到 SmsCodeAuthenticationProvider 先调用 supports() 方法,通过后,再调用 authenticate()方法进行认证

    package com.imooc.security.core.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;
    
    /**
     * AuthenticationManager 认证时候需要用的一个Provider
     * ClassName: SmsCodeAuthenticationProvider 
     * @Description: TODO
     * @author lihaoyang
     * @date 2018年3月8日
     */
    public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    
        private UserDetailsService userDetailsService;
        
        /**
         * 认证
         */
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            //能进到这说明authentication是SmsCodeAuthenticationToken,转一下
            SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
            //token.getPrincipal()就是手机号 mobile
            UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
            
            //认证没通过
            if(user == null){
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            }
            //认证通过
            SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
            //把认证之前得token里存的用户信息赋值给认证后的token对象
            authenticationResult.setDetails(authenticationToken.getDetails());
            return authenticationResult;
        }
    
        /**
         * 告诉AuthenticationManager,如果是SmsCodeAuthenticationToken的话用这个类处理
         */
        @Override
        public boolean supports(Class<?> authentication) {
            //判断传进来的authentication是不是SmsCodeAuthenticationToken类型的
            return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
        }
    
        public UserDetailsService getUserDetailsService() {
            return userDetailsService;
        }
    
        public void setUserDetailsService(UserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    
    }

    短信 验证码过滤器,照着图片验证码过滤器写,其实可以重构,不会弄:

    package com.imooc.security.core.validate.code;
    
    import java.io.IOException;
    import java.util.HashSet;
    import java.util.Set;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.social.connect.web.HttpSessionSessionStrategy;
    import org.springframework.social.connect.web.SessionStrategy;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.web.bind.ServletRequestBindingException;
    import org.springframework.web.bind.ServletRequestUtils;
    import org.springframework.web.context.request.ServletWebRequest;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import com.imooc.security.core.properties.SecurityConstants;
    import com.imooc.security.core.properties.SecurityProperties;
    
    /**
     * 短信验证码过滤器
     * ClassName: ValidateCodeFilter 
     * @Description:
     *  继承OncePerRequestFilter:spring提供的工具,保证过滤器每次只会被调用一次
     *  实现 InitializingBean接口的目的:
     *      在其他参数都组装完毕的时候,初始化需要拦截的urls的值
     * @author lihaoyang
     * @date 2018年3月2日
     */
    public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean{
    
        private Logger logger = LoggerFactory.getLogger(getClass());
        
        //认证失败处理器
        private AuthenticationFailureHandler authenticationFailureHandler;
    
        //获取session工具类
        private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
        
        //需要拦截的url集合
        private Set<String> urls = new HashSet<>();
        //读取配置
        private SecurityProperties securityProperties;
        //spring工具类
        private AntPathMatcher antPathMatcher = new AntPathMatcher();
        
        /**
         * 重写InitializingBean的方法,设置需要拦截的urls
         */
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            //读取配置的拦截的urls
            String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
            //如果配置了需要验证码拦截的url,不判断,如果没有配置会空指针
            if(configUrls != null && configUrls.length > 0){
                for (String configUrl : configUrls) {
                    logger.info("ValidateCodeFilter.afterPropertiesSet()--->配置了验证码拦截接口:"+configUrl);
                    urls.add(configUrl);
                }
            }else{
                logger.info("----->没有配置拦验证码拦截接口<-------");
            }
            //短信验证码登录一定拦截
            urls.add(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE);
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            //如果是 登录请求 则执行
    //        if(StringUtils.equals("/authentication/form", request.getRequestURI())
    //                &&StringUtils.equalsIgnoreCase(request.getMethod(), "post")){
    //            try {
    //                validate(new ServletWebRequest(request));
    //            } catch (ValidateCodeException e) {
    //                //调用错误处理器,最终调用自己的
    //                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
    //                return ;//结束方法,不再调用过滤器链
    //            }
    //        }
            
            
            /**
             * 可配置的验证码校验
             * 判断请求的url和配置的是否有匹配的,匹配上了就过滤
             */
            boolean action = false;
            for(String url:urls){
                if(antPathMatcher.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);
        }
    
        /**
         * 校验验证码
         * @Description: 校验验证码
         * @param @param request
         * @param @throws ServletRequestBindingException   
         * @return void  
         * @throws ValidateCodeException
         * @author lihaoyang
         * @date 2018年3月2日
         */
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
            //拿出session中的ImageCode对象
            ValidateCode smsCodeInSession = (ValidateCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
            //拿出请求中的验证码
            String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
            //校验
            if(StringUtils.isBlank(imageCodeInRequest)){
                throw new ValidateCodeException("验证码不能为空");
            }
            if(smsCodeInSession == null){
                throw new ValidateCodeException("验证码不存在,请刷新验证码");
            } 
            if(smsCodeInSession.isExpired()){
                //从session移除过期的验证码
                sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
                throw new ValidateCodeException("验证码已过期,请刷新验证码");
            }
            if(!StringUtils.equalsIgnoreCase(smsCodeInSession.getCode(), imageCodeInRequest)){
                throw new ValidateCodeException("验证码错误");
            }
            //验证通过,移除session中验证码
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY_SMS);
        }
    
        public AuthenticationFailureHandler getAuthenticationFailureHandler() {
            return authenticationFailureHandler;
        }
    
        public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
            this.authenticationFailureHandler = authenticationFailureHandler;
        }
    
        public SecurityProperties getSecurityProperties() {
            return securityProperties;
        }
    
        public void setSecurityProperties(SecurityProperties securityProperties) {
            this.securityProperties = securityProperties;
        }
        
        
    }

    把新建的这三个类做下配置,让spring security知道

    SmsCodeAuthenticationSecurityConfig:

    package com.imooc.security.core.authentication.mobile;
    
    import org.springframework.beans.factory.annotation.Autowired;
    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;
    
    /**
     * 短信验证码配置
     * ClassName: SmsCodeAuthenticationSecurityConfig 
     * @Description: TODO
     * @author lihaoyang
     * @date 2018年3月8日
     */
    @Component
    public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
        @Autowired
        private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
        
        @Autowired
        private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
        
        @Autowired
        private UserDetailsService userDetailsService;
        
        @Override
        public void configure(HttpSecurity http) throws Exception {
            //1,配置短信验证码过滤器
            SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
            smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
            //设置认证失败成功处理器
            smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
            smsCodeAuthenticationFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
            
            //配置pprovider
            SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
            smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        
            http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        }
    
        
    }

    最后在BrowserSecurityConfig里配置短信验证码

    @Configuration //这是一个配置
    public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
        
        //读取用户配置的登录页配置
        @Autowired
        private SecurityProperties securityProperties;
        
        //自定义的登录成功后的处理器
        @Autowired
        private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
        
        //自定义的认证失败后的处理器
        @Autowired
        private AuthenticationFailureHandler imoocAuthenticationFailureHandler;
        
        //数据源
        @Autowired
        private DataSource dataSource;
        
        
        @Autowired
        private UserDetailsService userDetailsService;
        
        @Autowired
        private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
        
        @Autowired
        private SpringSocialConfigurer imoocSocialSecurityConfig;
    
        //注意是org.springframework.security.crypto.password.PasswordEncoder
        @Bean
        public PasswordEncoder passwordencoder(){
            //BCryptPasswordEncoder implements PasswordEncoder
            return new BCryptPasswordEncoder();
        }
        
        
        /**
         * 记住我TokenRepository配置,在登录成功后执行
         * 登录成功后往数据库存token的
         * @Description: 记住我TokenRepository配置
         * @param @return   JdbcTokenRepositoryImpl
         * @return PersistentTokenRepository  
         * @throws
         * @author lihaoyang
         * @date 2018年3月5日
         */
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            jdbcTokenRepository.setDataSource(dataSource);
            //启动时自动生成相应表,可以在JdbcTokenRepositoryImpl里自己执行CREATE_TABLE_SQL脚本生成表
            //第二次启动表已存在,需要注释
    //        jdbcTokenRepository.setCreateTableOnStartup(true);
            return jdbcTokenRepository;
        }
        
      //版本二:可配置的登录页
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //~~~-------------> 图片验证码过滤器 <------------------
            ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
            //验证码过滤器中使用自己的错误处理
            validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
            //配置的验证码过滤url
            validateCodeFilter.setSecurityProperties(securityProperties);
            validateCodeFilter.afterPropertiesSet();
            
            //~~~-------------> 短信验证码过滤器 <------------------
            SmsCodeFilter smsCodeFilter = new SmsCodeFilter();
            //验证码过滤器中使用自己的错误处理
            smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenticationFailureHandler);
            //配置的验证码过滤url
            smsCodeFilter.setSecurityProperties(securityProperties);
            smsCodeFilter.afterPropertiesSet();
            
            
            
            //实现需要认证的接口跳转表单登录,安全=认证+授权
            //http.httpBasic() //这个就是默认的弹框认证
            //
            http 
                .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
    //            .apply(imoocSocialSecurityConfig)//社交登录
    //            .and()
                //把验证码过滤器加载登录过滤器前边
                .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                //表单认证相关配置
                .formLogin() 
                    .loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL) //处理用户认证BrowserSecurityController
                    //登录过滤器UsernamePasswordAuthenticationFilter默认登录的url是"/login",在这能改
                    .loginProcessingUrl(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_FORM) 
                    .successHandler(imoocAuthenticationSuccessHandler)//自定义的认证后处理器
                    .failureHandler(imoocAuthenticationFailureHandler) //登录失败后的处理
                    .and()
                //记住我相关配置    
                .rememberMe()
                    .tokenRepository(persistentTokenRepository())//TokenRepository,登录成功后往数据库存token的
                    .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds())//记住我秒数
                    .userDetailsService(userDetailsService) //记住我成功后,调用userDetailsService查询用户信息
                .and()
                //授权相关的配置 
                .authorizeRequests() 
                    // /authentication/require:处理登录,securityProperties.getBrowser().getLoginPage():用户配置的登录页
                    .antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
                    securityProperties.getBrowser().getLoginPage(),//放过登录页不过滤,否则报错
                    SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/*").permitAll() //验证码
                    .anyRequest()        //任何请求
                    .authenticated()    //都需要身份认证
                .and()
                    .csrf().disable() //关闭csrf防护
                .apply(smsCodeAuthenticationSecurityConfig);//把短信验证码配置应用上
        }
    }

    访问登陆页,点击发送验证码模拟发送验证码

    输入后台打印的验证码

    登录成功:

    完整代码在github:https://github.com/lhy1234/spring-security

  • 相关阅读:
    TestNG
    K近邻算法
    Python解决乱码问题
    Log4J 配置
    多线程死锁
    hadooplzo安装出错的解决方法
    B树
    设计模式
    整数序列化
    Maven
  • 原文地址:https://www.cnblogs.com/lihaoyang/p/8523279.html
Copyright © 2011-2022 走看看