zoukankan      html  css  js  c++  java
  • Spring boot整合Spring Security实现验证码登陆

    Spring boot整合Spring Security实现验证码登陆

     

    验证码登陆在日常使用软件中是很常见的,甚至可以说超过了密码登陆。

    如何通过Spring Security框架实现验证码登陆,并且登陆成功之后也同样返回和密码登陆类似的token?

    • 先看一张Spring Security拦截请求的流程图

     可以发现Spring Security默认有用户名密码登陆拦截器,查看 UsernamePasswordAuthenticationFilter 实现了 AbstractAuthenticationProcessingFilter类 。根据UsernamePasswordAuthenticationFilter的设计模式可以在Spring security的基础上拓展自己的拦截器,实现相应的功能。

    • 新增一个 SmsCodeAuthenticationToken 实体类,需要继承IrhAuthenticationToken,而IrhAuthenticationToken继承了 AbstractAuthenticationToken 用来封装验证码登陆时需要的信息,并且自定义一个AuthenticationToken的父类的作用就是用来在管理在具有多种登陆方式的系统中,能记录最核心的两个信息,一个是用户身份,一个是登陆口令;并且在后期能直接向上转型而不报错。
    复制代码
    package top.imuster.auth.config;
    
    import org.springframework.security.core.GrantedAuthority;
    
    import java.util.Collection;
    
    /**
     * @ClassName: SmsCodeAuthenticationToken
     * @Description: 验证码登录验证信息封装类
     * @author: hmr
     * @date: 2020/4/30 13:59
     */
    public class SmsCodeAuthenticationToken extends IrhAuthenticationToken {
        public SmsCodeAuthenticationToken(Object principal, Object credentials) {
            super(principal, credentials);
        }
    
        public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
            super(principal, credentials, authorities);
        }
    }
    复制代码
    复制代码
    package top.imuster.auth.config;
    
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    
    import java.util.Collection;
    
    /**
     * @Author hmr
     * @Description 自定义AbstractAuthenticationToken
     * @Date: 2020/5/1 13:51
     * @param irh平台的认证实体类   可以通过继承该类来实现不同的登录逻辑
     * @reture:
     **/
    public class IrhAuthenticationToken extends AbstractAuthenticationToken {
    
        private static final long serialVersionUID = 110L;
    
        //用户信息
        protected final Object principal;
    
        //密码或者邮箱验证码
        protected Object credentials;
    
        /**
         * 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 IrhAuthenticationToken(Object principal, Object credentials) {
            super(null);
            this.principal = principal;
            this.credentials = credentials;
            this.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>)
         * token token.
         *
         * @param principal
         * @param credentials
         * @param authorities
         */
        public IrhAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            this.credentials = credentials;
            super.setAuthenticated(true);
        }
    
    
        @Override
        public Object getCredentials() {
            return this.credentials;
        }
    
        @Override
        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");
            } else {
                super.setAuthenticated(false);
            }
        }
    
        public void eraseCredentials() {
            super.eraseCredentials();
            this.credentials = null;
        }
    }
    复制代码

    • 继承AbstractAuthenticationProcessingFilter之后就可以将自定义的Filter假如到过滤器链中。所以自定义一个 SmsAuthenticationFilter 类并且继承 AbstractAuthenticationProcessingFilter 类,并且该Filter中只校验输入参数是否正确,如果不完整,则抛出 AuthenticationServiceException异常,如果抛出自定义异常,会被Security框架处理。用原生异常可以设置异常信息,直接返回给前端。
    复制代码
    package top.imuster.auth.component.login;
    
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    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 top.imuster.auth.config.SecurityConstants;
    import top.imuster.auth.config.SmsCodeAuthenticationToken;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * @ClassName: SmsCodeAuthenticationFilter
     * @Description: 自定义拦截器,拦截登录请求中的登录类型
     * @author: hmr
     * @date: 2020/4/30 12:16
     */
    public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        private static final Logger log = LoggerFactory.getLogger(SmsCodeAuthenticationFilter.class);
    
        private static final String POST = "post";
    
        private boolean postOnly = true;
    
        public SmsCodeAuthenticationFilter(){
            super(new AntPathRequestMatcher("/emailCodeLogin", "POST"));
        }
    
        @Autowired
        AuthenticationManager authenticationManager;
    
        @Override
        @Autowired
        public void setAuthenticationManager(AuthenticationManager authenticationManager) {
            super.setAuthenticationManager(authenticationManager);
        }
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
            if(postOnly && !POST.equalsIgnoreCase(httpServletRequest.getMethod())){
                throw new AuthenticationServiceException("不允许{}这种的请求方式: " + httpServletRequest.getMethod());
            }
            //邮箱地址
            String loginName = obtainParameter(httpServletRequest, SecurityConstants.LOGIN_PARAM_NAME);
            //验证码
            String credentials = obtainParameter(httpServletRequest, SecurityConstants.EMAIL_VERIFY_CODE);
            loginName = loginName.trim();
    
            if(StringUtils.isBlank(loginName)) throw new AuthenticationServiceException("登录名不能为空");
            if(StringUtils.isBlank(credentials)) throw new AuthenticationServiceException("验证码不能为空");
    
            SmsCodeAuthenticationToken authenticationToken = new SmsCodeAuthenticationToken(loginName, credentials);  //将输入的信息封装成一个SmsCodeAuthenicationToken对象,并向后传递
            setDetails(httpServletRequest, authenticationToken);
            return authenticationManager.authenticate(authenticationToken);
        }
    
        private void setDetails(HttpServletRequest request,
                                SmsCodeAuthenticationToken authRequest) {
            authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        }
    
        /**
         * @Author hmr
         * @Description 从request中获得参数
         * @Date: 2020/4/30 12:22
         * @param request
         * @reture: java.lang.String
         **/
        protected String obtainParameter(HttpServletRequest request, String type){
            return request.getParameter(type);
        }
    }
    复制代码
    • 设置了Filter拦截到登陆请求之后,还需要一个具体的校验验证码是否正确的类  SmsAuthenticationProvider,该类才是具体的业务代码
    复制代码
    package top.imuster.auth.component.login;
    
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.AuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import top.imuster.auth.config.SmsCodeAuthenticationToken;
    import top.imuster.common.core.utils.RedisUtil;
    
    /**
     * @ClassName: IrhAuthenticationProvider
     * @Description: IrhAuthenticationProvider
     * @author: hmr
     * @date: 2020/4/30 14:36
     */
    public class SmsAuthenticationProvider implements AuthenticationProvider {
        private final Logger log = LoggerFactory.getLogger(this.getClass());
    
        @Autowired
        RedisTemplate redisTemplate;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;     //先将authentication强转成SmsCodeAuthenticationToken
            //登录名
            String loginName = authenticationToken.getPrincipal() == null?"NONE_PROVIDED":authentication.getName(); 
            //验证码
            String verify = (String)authenticationToken.getCredentials();
            String redisCode = (String)redisTemplate.opsForValue().get(RedisUtil.getConsumerLoginByEmail(loginName));   //从redis中获得申请到的验证码
            if(StringUtils.isEmpty(redisCode) || !verify.equalsIgnoreCase(redisCode)){
                throw new AuthenticationServiceException("验证码失效或者错误");
            }
            return authentication;
        }
    
        @Override
        public boolean supports(Class<?> aClass) {
            return (SmsCodeAuthenticationToken.class.isAssignableFrom(aClass));
        }
    
    }
    复制代码

     并且在一个继承了 WebSecurityConfigurerAdapter的类中将Filter和Provide声明成Bean

    复制代码
    package top.imuster.auth.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.annotation.Order;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.authentication.ProviderManager;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import top.imuster.auth.component.login.*;
    import top.imuster.auth.service.Impl.UsernameUserDetailsServiceImpl;
    
    import java.util.Arrays;
    
    @Configuration
    @EnableWebSecurity
    @Order(2147483636)
    class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public SmsCodeAuthenticationFilter smsCodeAuthenticationFilter(){
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
    try {
    smsCodeAuthenticationFilter.setAuthenticationManager(this.authenticationManager());
    } catch (Exception e) {
    e.printStackTrace();
    }
    smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(irhAuthenticationSuccessHandler());
    smsCodeAuthenticationFilter.setAuthenticationFailureHandler(irhAuthenticationFailHandler());
    return smsCodeAuthenticationFilter;
    }
    @Bean
    public SmsAuthenticationProvider smsAuthenticationProvider(){
    SmsAuthenticationProvider provider = new SmsAuthenticationProvider();
    // 设置userDetailsService
    // 禁止隐藏用户未找到异常
    return provider;
    }


    @Bean
    @Override
    public AuthenticationManager authenticationManager() throws Exception {
    ProviderManager authenticationManager = new ProviderManager(Arrays.asList(smsAuthenticationProvider()));
    return authenticationManager;
    }

    }
    复制代码

    说明:Filter是用来拦截Request请求,并且从request请求中将指定的信息提取出来,判断一些必要信息是否存在,不对信息进行校验,校验信息合法之后将其封装到token中,向后传递给provide如;provider则

    将filter中得到的信息进行校验,在provider中,主要要注意的就是需要重写 supports(Class<?> aClass) 方法,该方法的作用就是假如在系统中有多个filter,并且向后传递了多个不同的token,那么对应的token只能传递到对应的provider中,所以该方法返回到是一个boolean类型,用来给Spring Security决策是否需要进入该provider。

    • 获取验证码
    复制代码
        /**
         * @Author hmr
         * @Description 发送email验证码
         * @Date: 2020/4/30 10:12
         * @param email  接受code的邮箱
         * @param type   1-注册  2-登录  3-忘记密码
         * @reture: top.imuster.common.base.wrapper.Message<java.lang.String>
         **/
        @ApiOperation(value = "发送email验证码",httpMethod = "GET")
        @Idempotent(submitTotal = 5, timeTotal = 30, timeUnit = TimeUnit.MINUTES)
        @GetMapping("/sendCode/{type}/{email}")
        public Message<String> getCode(@ApiParam("邮箱地址") @PathVariable("email") String email, @PathVariable("type") Integer type) throws Exception {
            if(type != 1 && type != 2 && type != 3 && type != 4){
                return Message.createByError("参数异常,请刷新后重试");
            }
            userLoginService.getCode(email, type);
            return Message.createBySuccess();
        }
    复制代码

    具体代码我已经开源到GitHub上,仓库地址为https://github.com/HMingR/irh,该项目中还有利用Spring security实现微信小程序登陆,用户名密码登陆等。

  • 相关阅读:
    vue主动刷新页面及列表数据删除后的刷新实例
    一些VUE技巧收藏
    d2-admin中不错的技巧
    webSocket另一种封装
    基于token前后端分离认证
    node.js使用vue-native-websocket实现websocket通信 实测有效
    Vue 路由传递参数
    ES6中import {} 的括号
    Vue 参数传递
    简单工厂模式
  • 原文地址:https://www.cnblogs.com/duanweishi/p/14333601.html
Copyright © 2011-2022 走看看