上一篇博客實現了認證+受權的基本功能,這裏在這個基礎上,添加一個 記住個人功能。html
上一篇博客地址:SpringSecurity(1)---認證+受權代碼實現java
說明
:上一遍博客的 用戶數據 和 用戶關聯角色 的信息是在代碼裏寫死的,這篇將從mysql數據庫中讀取。mysql
這裏建了三種表spring
通常權限表有四張或者五張,這裏有關 角色關聯資源表 沒有建立,角色和資源的關係依舊在代碼裏寫死。sql
建表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);
說明
:這裏數據庫只有一個用戶服務器
用戶名 :小小cookie
密碼:123456session
她所擁有的角色有兩個 教師 和 學生。ide
概念
記住我在登錄的時候都會被用戶勾選,由於它方便地幫助用戶減小了輸入用戶名和密碼的次數,用戶一旦勾選記住我功能那麼 當服務器重啓後依舊能夠不用登錄就能夠訪問。
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;
在配置裏再加上這些就能夠了。
主要測試兩個點地方,
一、當我登錄時選擇記住我功能,看下數據庫persistent_logins是否有一條token記錄 二、當使用記住我功能後,關閉服務器在重啓服務器,再也不登錄直接訪問須要認證的接口,看是否可以訪問成功。
咱們在看數據庫token表
很明顯新增了一條token數據。
這個時候咱們重啓服務器訪問須要認證的接口
發現就算重啓也不須要重啓登錄就能夠反問須要認證的接口。
一樣這裏也分爲兩部分 一、第一次登錄源碼流程。 二、重啓後未認證再去訪問須要認證的接口源碼流程。
第一步
當用戶發送登陸請求的時候,首先到達的是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 { //這裏就是關鍵的兩步 一、將token存入到數據庫 二、將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中。
重啓項目,這時候服務器端的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) { //一、獲取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]; //一、去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.")); //三、查看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 { //四、更新這條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"); } //五、這裏拿着用戶名 就又獲取當前用戶的認證和受權信息 return this.getUserDetailsService().loadUserByUsername(token.getUsername()); } } }
這樣整個流程就完成了,咱們能夠看出源碼的過程和上面圖片展現的流程仍是很是像的。
別人罵我胖,我會生氣,由於我內心認可了我胖。別人說我矮,我就會以爲可笑,由於我內心知道我不可能矮。這就是咱們爲何會對別人的攻擊生氣。 攻我盾者,乃我心裏之矛(18)