zoukankan      html  css  js  c++  java
  • SpringSecurity(2)---记住我功能实现

    SpringSecurity(2)---记住我功能实现

    上一篇博客实现了认证+授权的基本功能,这里在这个基础上,添加一个 记住我的功能

    上一篇博客地址:SpringSecurity(1)---认证+授权代码实现

    说明:上一遍博客的 用户数据用户关联角色 的信息是在代码里写死的,这篇将从mysql数据库中读取。

    一、数据库建表

    这里建了三种表

    一般权限表有四张或者五张,这里有关 角色关联资源表 没有创建,角色和资源的关系依旧在代码里写死。

    建表sql

    /*创建用户表*/
    CREATE TABLE `persistent_logins` (
      `username` varchar(64) NOT NULL,
      `series` varchar(64) NOT NULL,
      `token` varchar(64) NOT NULL,
      `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`series`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    /*创建j角色表*/
    CREATE TABLE `roles` (
      `id` int NOT NULL AUTO_INCREMENT,
      `name` varchar(32) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
    
    /*创建用户关联角色表*/
    CREATE TABLE `roles_user` (
      `id` int NOT NULL AUTO_INCREMENT,
      `rid` int DEFAULT '2',
      `uid` int DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=133 DEFAULT CHARSET=utf8;
    
    
    /*这里密码对应的明文 还是123456*/
    INSERT INTO `user` (`id`, `username`, `nickname`, `password`, `enabled`)
    VALUES
    	(1, '小小', '小小', 'e10adc3949ba59abbe56e057f20f883e', 1);
    
    /*三种角色*/
    INSERT INTO `roles` (`id`, `name`)
    VALUES
    	(1, '校长'),
    	(2, '教师'),
    	(3, '学生');
    	
    /*小小用户关联了 教师和校长角色*/
    INSERT INTO `roles_user` (`id`, `rid`, `uid`)
    VALUES
    	(1, 2, 1),
    	(2, 3, 1);
    
    

    说明:这里数据库只有一个用户

    用户名 :小小

    密码:123456

    她所拥有的角色有两个 教师学生


    二、Spring Security的记住我功能基本原理

    概念 记住我在登陆的时候都会被用户勾选,因为它方便地帮助用户减少了输入用户名和密码的次数,用户一旦勾选记住我功能那么 当服务器重启后依旧可以不用登陆就可以访问

    Spring Security的“记住我”功能的基本原理流程图如下所示:

    这里大致流程如下:

    第一次登陆

    用户请求的时候 remember-me参数为true 时,用户先进行 认证+授权过滤器。然后走记住我过滤器这里需要做两,这里主要做两件事。

    1.将Token数据存入数据库 2.将token数据存入cookie中。
    

    服务重启后

    如果服务重启的话,那么之前的session信息已经不在了,但是cookie中的Token还是存在的。所以当用户重启后去访问需要认证的接口时,会先通过cookie中的Token

    去数据库查询这条Token信息,如果存在那么在通过用户名去查询数据库获取当前用户的信息。


    三、代码实现

    因为上面项目已经完成了整个授权+认证的过程,那么这里就很简单添加一点点代码就可以了。

    在WebSecurityConfig中添加一个Bean,配置完这个Bean就基本完成了 记住我 功能的开发,然后在将这个Bean设置到configure方法中即可。

        @Bean
        public PersistentTokenRepository tokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            //tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    

    上面的代码 tokenRepository.setCreateTableOnStartup(true) ;是自动创建Token存到数据库时候所需要的表,这行代码只能运行一次,如果重新启动数据库,

    必须删除这行代码,否则将报错,因为在第一次启动的时候已经创建了表,不能重复创建。保险起见我们还是注释掉这段代码,手动建这张表。

    CREATE TABLE `persistent_logins` (
      `username` varchar(64) NOT NULL,
      `series` varchar(64) NOT NULL,
      `token` varchar(64) NOT NULL,
      `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`series`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    在配置里再加上这些就可以了。


    四、测试

    主要测试两个点地方,

    1、当我登陆时选择记住我功能,看下数据库persistent_logins是否有一条token记录
    2、当使用记住我功能后,关闭服务器在重启服务器,不再登陆直接访问需要认证的接口,看是否能够访问成功。
    

    1、首次登陆

    我们在看数据库token表

    很明显新增了一条token数据。

    2、重启服务器

    这个时候我们重启服务器访问需要认证的接口

    发现就算重启也不需要重启登陆就可以反问需要认证的接口。


    五、源码分析

    同样这里也分为两部分 1、第一次登陆源码流程。 2、重启后未认证再去访问需要认证的接口源码流程。

    1、首次登陆源码流程

    第一步

    当用户发送登录请求的时候,首先到达的是UsernamePasswordAuthenticationFilter这个过滤器,然后执行attemptAuthentication方法的代码,代码如下图所示:

     public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
     //从这里可以看出登陆需要post提交
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            } else {
                String username = this.obtainUsername(request);
                String password = this.obtainPassword(request);
                if (username == null) {
                    username = "";
                }
    
                if (password == null) {
                    password = "";
                }
    
                username = username.trim();
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
                this.setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
    

    之后所走的流程就是 ProviderManager的authenticate方法 ,之后再走AbstractUserDetailsAuthenticationProvider的authenticate方法,再走DaoAuthenticationProvider的方法retrieveUser方法

     protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            this.prepareTimingAttackProtection();
    
            try {
                //这里就走我们自定义的获取用户认证和授权信息的代码了
                UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
                if (loadedUser == null) {
                    throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
                } else {
                    return loadedUser;
                }
            } catch (UsernameNotFoundException var4) {
                this.mitigateAgainstTimingAttack(authentication);
                throw var4;
            } catch (InternalAuthenticationServiceException var5) {
                throw var5;
            } catch (Exception var6) {
                throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
            }
        }
    

    这样一来,认证的流程就已经走完了。那就要走记住我功能的过滤器了。

    第二步

    验证成功之后,将进入AbstractAuthenticationProcessingFilter 类的successfulAuthentication的方法中,首先将认证信息通过代码
    SecurityContextHolder.getContext().setAuthentication(authResult);将认证信息存入到session中,紧接着这个方法中就调用了rememberMeServices的loginSuccess方法

     protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
            }
    
            SecurityContextHolder.getContext().setAuthentication(authResult);
            //记住我
            this.rememberMeServices.loginSuccess(request, response, authResult);
            if (this.eventPublisher != null) {
                this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
            }
    
            this.successHandler.onAuthenticationSuccess(request, response, authResult);
        }
    

    再走PersistentTokenBasedRememberMeServices的onLoginSuccess方法

        protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
            String username = successfulAuthentication.getName();
            this.logger.debug("Creating new persistent login for user " + username);
            PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
    
            try {
                //这里就是关键的两步 1、将token存入到数据库 2、将token存入cookie中
                this.tokenRepository.createNewToken(persistentToken);
                this.addCookie(persistentToken, request, response);
            } catch (Exception var7) {
                this.logger.error("Failed to save persistent token ", var7);
            }
    
        }
    

    这个方法中调用了tokenRepository来创建Token并存到数据库中,且将Token写回到了Cookie中。到这里,基本的登录过程基本完成,生成了Token存到了数据库,

    且写回到了Cookie中。

    2、第二次访问

    重启项目,这时候服务器端的session已经不存在了,但是第一次登录成功已经将Token写到了数据库和Cookie中,直接访问一个服务,并且不输入用户名和密码。

    第一步

    首先进入到了RememberMeAuthenticationFilter的doFilter方法中,这个方法首先检查在session中是否存在已经验证过的Authentication了,如果为空,就进行下面的

    RememberMe的验证代码,比如调用rememberMeServices的autoLogin方法,代码如下:

        public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest request = (HttpServletRequest)req;
            HttpServletResponse response = (HttpServletResponse)res;
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                //走记住我流程
                Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
                //省略不重要的代码
                chain.doFilter(request, response);
            } else {
                chain.doFilter(request, response);
            }
        }
    

    我们在看this.rememberMeServices.autoLogin(request, response)方法。最终实现在AbstractRememberMeServices的autoLogin方法

        public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
            //1、获取token
            String rememberMeCookie = this.extractRememberMeCookie(request);
            if (rememberMeCookie == null) {
                return null;
            } else {
        
                    UserDetails user = null;
                    try {
                        String[] cookieTokens = this.decodeCookie(rememberMeCookie);
                        //这步是关键
                        user = this.processAutoLoginCookie(cookieTokens, request, response);
                        this.userDetailsChecker.check(user);
                        this.logger.debug("Remember-me cookie accepted");
                        return this.createSuccessfulAuthentication(request, user);
                    } catch (CookieTheftException var6) {
                        this.cancelCookie(request, response);
                        throw var6;
                    } 
                    this.cancelCookie(request, response);
                    return null;
                }
            }
        }
    

    我们在看 this.processAutoLoginCookie(cookieTokens, request, response);在PersistentTokenBasedRememberMeServices中实现,到这一步就已经很明白了

     protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
            if (cookieTokens.length != 2) {
                throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
            } else {
                String presentedSeries = cookieTokens[0];
                String presentedToken = cookieTokens[1];
                //1、去token表中查询token
                PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
                if (token == null) {
                    throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
                    //2校验数据
                } else if (!presentedToken.equals(token.getTokenValue())) {
                    this.tokenRepository.removeUserTokens(token.getUsername());
                    throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
                    //3、查看token是否过期
                } else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
                    throw new RememberMeAuthenticationException("Remember-me login has expired");
                } else {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
                    }
    
                    PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
    
                    try {
                    //4、更新这条token 没更新一次有效时间就都变成了之间设置的时间
                        this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
                        this.addCookie(newToken, request, response);
                    } catch (Exception var9) {
                        this.logger.error("Failed to update token: ", var9);
                        throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
                    }
                     //5、这里拿着用户名 就又获取当前用户的认证和授权信息
                    return this.getUserDetailsService().loadUserByUsername(token.getUsername());
                }
            }
        }
    

    这样整个流程就完成了,我们可以看出源码的过程和上面图片展示的流程还是非常像的。



    别人骂我胖,我会生气,因为我心里承认了我胖。别人说我矮,我就会觉得好笑,因为我心里知道我不可能矮。这就是我们为什么会对别人的攻击生气。
    攻我盾者,乃我内心之矛(18)
    
  • 相关阅读:
    Mysql的join语句
    Excel的VLOOKUP函数
    python求极值点(波峰波谷)
    python多项式拟合:np.polyfit 和 np.polyld
    hdfs显示、查看、下载、上传、删除文件操作
    pip备份、安装requirements.txt中的包和anaconda的安装(linux)
    spark-submit提交任务到集群,分发虚拟环境和第三方包
    VLAN实验3:理解Hybrid接口的应用
    VLAN实验2:配置Trunk接口
    WLAN实验1:划分不同VLAN及Acess配置
  • 原文地址:https://www.cnblogs.com/qdhxhz/p/12783471.html
Copyright © 2011-2022 走看看