SpringSecurity(2)---记住我功能实现 川长思鸟来 2022-09-03 14:30 290阅读 0赞 # 前言: # 我将代码下载尝试运行后,出现了连环报错。 观察后我怀疑应该是版本问题,原博主的程序写的比较早,而如今版本也更新好几次了,所以我怀疑是程序版本的问题。但这只会影响程序的运行,并不会影响我对Security的学习。 **原文地址**:[https://www.cnblogs.com/qdhxhz/p/12783471.html][https_www.cnblogs.com_qdhxhz_p_12783471.html] -------------------- 上一篇博客实现了认证+授权的基本功能,这里在这个基础上,添加一个 记住我的功能。 上一篇博客地址:[SpringSecurity(1)—认证+授权代码实现][SpringSecurity_1] 上一遍博客的 用户数据 和 用户关联角色 的信息是在代码里写死的,这篇将从mysql数据库中读取。 # 一、数据库建表 # 这里建了三种表 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center] 一般权限表有四张或者五张,这里有关 角色关联资源表 没有创建,角色和资源的关系依旧在代码里写死。 创建: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); > 这里我感觉给的并不完善,没有user表,但丝毫不影响,因为我创建了user表也还是没运行出来,所以直接过就完事了。 说明:这里数据库只有一个用户 用户名 :小小 密码:123456 她所拥有的角色有两个 教师 和 学生。 # 二、Spring Security的记住我功能基本原理 # 记住我在登陆的时候都会被用户勾选,因为它方便地帮助用户减少了输入用户名和密码的次数,用户一旦勾选记住我功能那么 当服务器重启后依旧可以不用登陆就可以访问。 Spring Security的“记住我”功能的基本原理流程图如下所示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 1] 这里大致流程如下: 第一次登陆 用户请求的时候 **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; 在配置里再加上这些就可以了。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 2] # 四、测试 # 主要测试两个点地方, 1. 当我登陆时选择记住我功能,看下数据库persistent\_logins是否有一条token记录 2. 当使用记住我功能后,关闭服务器在重启服务器,不再登陆直接访问需要认证的接口,看是否能够访问成功。 ## 1、首次登陆 ## ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 3] 我们在看数据库token表 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 4] 很明显新增了一条token数据。 ## 2、重启服务器 ## 这个时候我们重启服务器访问需要认证的接口 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 5] 发现就算重启也不需要重启登陆就可以反问需要认证的接口。 # 五、源码分析(重点!!!) # 同样这里也分为两部分 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()); } } } 这样整个流程就完成了,我们可以看出源码的过程和上面图片展示的流程还是非常像的。 [https_www.cnblogs.com_qdhxhz_p_12783471.html]: https://www.cnblogs.com/qdhxhz/p/12783471.html [SpringSecurity_1]: https://blog.csdn.net/weixin_44096353/article/details/119500682?spm=1001.2014.3001.5501 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center]: /images/20220829/81db22cdb9ad4eafaa7f7a139c3e6455.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 1]: /images/20220829/1a25c2d1cc29470fbadad0fe8d0f13ad.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 2]: /images/20220829/6472a3c015cd4d55a2291f2712a92c45.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 3]: /images/20220829/be4e22405d3d41d89d27f70f15b2c82b.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 4]: /images/20220829/72d0706672324077b1dd1ca255c9a706.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NDA5NjM1Mw_size_16_color_FFFFFF_t_70_pic_center 5]: /images/20220829/3fc24c01e8a44f41b9d612a43e310bfd.png
还没有评论,来说两句吧...