spring security 學習三-rememberMe
功能:登錄時的“記住我”功能
原理:
rememberMeAuthenticationFilter在security過濾器鏈中的位置,在請求走認證流程是,當前邊的filter都不通過時,會走rememberMeAuthenticationFilter
代碼:
html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>自定義登錄頁面</title> </head> <body> <h2>自定義登錄頁面</h2> <form action="/authentication/form" method="POST"> <table> <tr> <td>用戶名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密碼:</td> <td><input type="password" name="password"></td> </tr> <tr> <td>圖形驗證碼:</td> <td> <input type="text" name="imageCode"> <img src="/code/image?width=200" > </td> </tr> <tr> <td colspan="2"><!-- 其中的name值 remember-me 是固定不變的,security默認 --> <input type="checkbox" name="remember-me"value="true">記住我 </td> </tr> <tr> <td colspan="2"> <button type="submit">登錄</button> </td> </tr> </table> </form> </body> </html>
security配置:
@Autowired private DataSource dataSource;//datasource 用的是springboot默認的application.yml中的配置 @Autowired private UserDetailsService userDetailsService; @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository= new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); //自動創建相關的token表 jdbcTokenRepository.setCreateTableOnStartup(true); return jdbcTokenRepository; } @Override protected void configure(HttpSecurity http) throws Exception { //用戶可自定義、亦可以使用mysecurity默認的登錄頁 String loginPage = securityProperties.getBrowser().getLoginPage(); http.formLogin() .loginPage("/authentication/require").loginProcessingUrl("/authentication/form") .successHandler(custAuthenticationSuccessHandler) .failureHandler(custAuthenticationFailerHandler) .and() .rememberMe() .tokenRepository(persistentTokenRepository())//用於將token信息存儲到數據庫中 .tokenValiditySeconds(3600) .userDetailsService(userDetailsService)//用於登錄 .and() .authorizeRequests() .antMatchers("/authentication/require", loginPage, "/code/image").permitAll() .anyRequest() .authenticated(); http.csrf().disable();//暫時設為disable 防止跨站請求偽造的功能 }
啟動項目:(因配置了dbcTokenRepository.setCreateTableOnStartup(true) 所以會自動創建下表用於存儲用戶token信息)
頁面:點擊記住我後登錄
登錄成功之後會在persistent_login表中插入一條數據:
重啟項目,直接訪問資源路徑:
http://localhost:8080/user/1
不需要跳轉到登錄頁面,直接返回具體信息
源碼:
UserNamePasswordAuthenticationFilter.class文件中的attempAuthentication方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 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); } }
查看其父類:
AbstractAuthenticationProcessingFilter類,this.attempAuthentication方法使用的是子類方法,也就是usernamepasswordauthenticationFilter類中的方法(上邊的)
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Request is to process authentication"); } Authentication authResult; try { authResult = this.attemptAuthentication(request, response); if (authResult == null) { return; } this.sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException var8) { this.logger.error("An internal error occurred while trying to authenticate the user.", var8); this.unsuccessfulAuthentication(request, response, var8); return; } catch (AuthenticationException var9) { this.unsuccessfulAuthentication(request, response, var9); return; } if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } this.successfulAuthentication(request, response, chain, authResult); } }
attempauthentication方法執行完之後,回到successfulAuthentication方法(也是AbstractAuthenticationProcessingFilter類),會有remberMeService類
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); }
loginSuccess方法會進入AbstractRememberMeService抽象類中:
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { if (!this.rememberMeRequested(request, this.parameter)) { this.logger.debug("Remember-me login not requested."); } else { this.onLoginSuccess(request, response, successfulAuthentication); } }
onLoginSuccess方法會進入PersistentTokenBaseRememberMeService類中(AbstractRememberMEService的子類)
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 { this.tokenRepository.createNewToken(persistentToken); this.addCookie(persistentToken, request, response); } catch (Exception var7) { this.logger.error("Failed to save persistent token ", var7); } }
這裏的這個tokenRepository就是在配置文件中定義的persistentTokenRepository,addCookie方法會將token寫到瀏覽器的cookie中,等下次請求的時候回自動帶著token
下一次訪問會進入到RememberAuthenticationFilter過濾器中:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res;
//判斷context中是否已經有了一個認證的Authentication對象 if (SecurityContextHolder.getContext().getAuthentication() == null) {
//是否可以自動登錄 Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { try { rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth); //將一個已經認證的authentication對象放到context中
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); this.onSuccessfulAuthentication(request, response, rememberMeAuth); if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder populated with remember-me token: ‘" + SecurityContextHolder.getContext().getAuthentication() + "‘"); } if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass())); } if (this.successHandler != null) { this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth); return; } } catch (AuthenticationException var8) { if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: ‘" + rememberMeAuth + "‘; invalidating remember-me token", var8); } this.rememberMeServices.loginFail(request, response); this.onUnsuccessfulAuthentication(request, response, var8); } } chain.doFilter(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: ‘" + SecurityContextHolder.getContext().getAuthentication() + "‘"); } chain.doFilter(request, response); } }
autoLogin方法會進入到AbstractRememberMeService抽象類中:
public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { String rememberMeCookie = this.extractRememberMeCookie(request); if (rememberMeCookie == null) { return null; } else { this.logger.debug("Remember-me cookie detected"); if (rememberMeCookie.length() == 0) { this.logger.debug("Cookie was empty"); this.cancelCookie(request, response); 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); return null; } } }
processAutoLoginCookie方法會走實現類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]; PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries); if (token == null) { throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries); } 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.")); } 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 { 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"); } return this.getUserDetailsService().loadUserByUsername(token.getUsername()); } } }
spring security 學習三-rememberMe