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());
}
}
}
這樣整個流程就完成了,我們可以看出源碼的過程和上面圖片展示的流程還是非常像的。
Github地址 : spring-boot-security-study-02
別人罵我胖,我會生氣,因為我心里承認了我胖。別人說我矮,我就會覺得好笑,因為我心里知道我不可能矮。這就是我們為什么會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(18)
