SpringSecurity最主要的就是過濾器鏈。
一、過濾器鏈的原理分析
首先分析web.xml中的如下配置:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
找到DelegatingFilterProxy這個類,其實這個類還是過濾器。
查看doFilter方法。
debug查看上面的方法。所以web.xml中Filter的名字必須是springSecurityFilterChain也就不奇怪了。
繼續查看上述方法返回的FilterChainProxy這個類。
還是過濾器,繼續看doFilter方法。
繼續debug上述方法。
繼續進入getFilters方法。
SecurityFilterChain其實是個接口,實現關係如下圖:
二、CSRF過濾器分析
CSRF跨站點請求僞造(Cross—Site Request Forgery),存在巨大的危害性。
CSRF攻擊攻擊原理及過程如下:
1. 用戶C打開瀏覽器,訪問受信任網站A,輸入用戶名和密碼請求登錄網站A;
2.在用戶信息通過驗證後,網站A產生Cookie信息並返回給瀏覽器,此時用戶登錄網站A成功,可以正常發送請求到網站A;
3. 用戶未退出網站A之前,在同一瀏覽器中,打開一個TAB頁訪問網站B;
4. 網站B接收到用戶請求後,返回一些攻擊性代碼,併發出一個請求要求訪問第三方站點A;
5. 瀏覽器在接收到這些攻擊性代碼後,根據網站B的請求,在用戶不知情的情況下攜帶Cookie信息,向網站A發出請求。網站A並不知道該請求其實是由B發起的,所以會根據用戶C的Cookie信息以C的權限處理該請求,導致來自網站B的惡意代碼被執行。
現在想明白了,爲什麼我之前的QQ號被盜了。。。裂開啊。。。那肯定是小雜碎通過誘惑的手段,讓我去點擊某個網址,並在該網址上造了一個URL(羣發騷擾消息的請求,也是之前的誘惑網址,發給幾個羣之後,修改自己密碼爲雜碎自己定義的密碼)
,OK接下來我密碼直接沒了。聯想CSRF攻擊,我在點那個網址的時候,相當於我把手機上的緩存信息也帶過去了,就相當於黑客僞造我在發請求,emmm,信息安全很重要吶。
接下來看一下SpringSecurity提供的CsrfFilter過濾器,
點到最後會發現,如下圖。
非GET等如下請求都會經過Csrf過濾器,而GET請求等直接放行。要想通過CSRF過濾器,可以手動添加csrf的token信息,可通過SpringSecurity提供的動態標籤直接添加。
三、認證的過濾器分析
UsernamePasswordAuthenticationToken類中其實就包括了用戶名和密碼兩部分。
繼續進入認證方法。
SpringSecurity自帶的用戶對象爲UserDetails 。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
//UserDetails就是SpringSecurity自己的用戶對象
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//用自己數據庫中的數據實現認證操作啊
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
通過retrieveUser方法查看認證業務。
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
所以我們自己通過數據庫實現認證是需要實現UserDetailsService 接口,並返回SpringSecurity提供的用戶對象UserDetails即可。
再看之前源碼中的最後一句createSuccessAuthentication方法。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
這裏又封裝了UsernamePasswordAuthenticationToken 對象,但是和之前不同的是,此時多了角色信息。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
順便看一下authorities參數的來源,來到實現了GrantedAuthoritiesMapper接口到的父類中。
然而一頓操作猛如虎,一看戰績0-5的感覺油然而生。
接着看super(authorities)方法。
找到最開始的UsernamePasswordAuthenticationFilter的父類,並查看父類實現的doFilter方法。
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);
}
}
四、查看記住我源碼分析
找到接口的實現類。
判斷表單中記住我標記的具體邏輯。
protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
if (this.alwaysRemember) {
return true;
} else {
String paramValue = request.getParameter(parameter);
//忽略大小寫的on、true、yes以及1都可以通過
if (paramValue != null && (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1"))) {
return true;
} else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Did not send remember-me cookie (principal did not set parameter '" + parameter + "')");
}
return false;
}
}
}
繼續查看父類PersistentTokenBasedRememberMeServices實現的onLoginSuccess方法。
SpringSecurity需要手動開啓其開啓remember me的過濾器,並且持久化到數據庫表時,表名以及各個字段名都是固定的,無法改變,表結構如下:
CREATE TABLE persistent_logins(
username varchar(64) NOT NULL,
series varchar(64) NOT NULL,
token varchar(64) NOT NULL,
last_used timestamp NOT NULL,
PRIMARY KEY(series)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;