一崔赌、springSecurity配置信息詳解
參考:http://www.spring4all.com/article/446
二健芭、springSecurity過濾器詳解
- 我們已經知道Spring Security使用了springSecurityFillterChian作為了安全過濾的入口慈迈,接下來主要分析一下這個過濾器鏈都包含了哪些關鍵的過濾器痒留,并且各自的使命是什么伸头。
- springSecurity中過濾鏈及每一個過濾器的作用(按順序)熊锭,同時加粗的部分代表一些核心過濾器碗殷,需要重點了解。
1仿粹、SecurityContextPersistenceFilter :兩個主要職責:請求來臨時吭历,創(chuàng)建SecurityContext安全上下文信息晌区,請求結束時清空SecurityContextHolder朗若。
2、HeaderWriterFilter (文檔中并未介紹遣总,非核心過濾器) 用來給http響應添加一些Header,比如X-Frame-Options, X-XSS-Protection*彤避,X-Content-Type-Options.
3董饰、CsrfFilter 在spring4這個版本中被默認開啟的一個過濾器卒暂,用于防止csrf攻擊也祠,前后端使用json交互需要注意的一個問題诈嘿。
4、LogoutFilter 顧名思義削葱,處理注銷的過濾器
5奖亚、UsernamePasswordAuthenticationFilter 這個會重點分析,表單提交了username和password析砸,被封裝成token進行一系列的認證昔字,便是主要通過這個過濾器完成的,在表單認證的方法中首繁,這是最最關鍵的過濾器作郭。
6、RequestCacheAwareFilter (文檔中并未介紹,非核心過濾器) 內部維護了一個RequestCache,用于緩存request請求
7状土、SecurityContextHolderAwareRequestFilter 此過濾器對ServletRequest進行了一次包裝累驮,使得request具有更加豐富的API
8、AnonymousAuthenticationFilter 匿名身份過濾器,這個過濾器個人認為很重要,需要將它與UsernamePasswordAuthenticationFilter 放在一起比較理解,spring security為了兼容未登錄的訪問,也走了一套認證流程,只不過是一個匿名的身份辈赋。
9篷就、SessionManagementFilter 和session相關的過濾器未辆,內部維護了一個SessionAuthenticationStrategy攘残,兩者組合使用病曾,常用來防止session-fixation protection attack立叛,以及限制同一用戶開啟多個會話的數量
10赁还、ExceptionTranslationFilter 直譯成異常翻譯過濾器朋蔫,還是比較形象的青扔,這個過濾器本身不處理異常黎茎,而是將認證過程中出現的異常交給內部維護的一些類去處理,具體是那些類下面詳細介紹
11、FilterSecurityInterceptor 這個過濾器決定了訪問特定路徑應該具備的權限屏积,訪問的用戶的角色独榴,權限是什么症歇?訪問的路徑需要什么樣的角色和權限苍息?這些判斷和處理都是由該類進行的爆办。
1跨算、SecurityContextPersistenceFilter
- 用戶在登錄過一次之后,后續(xù)的訪問便是通過sessionId來識別坏瘩,從而認為用戶已經被認證提陶。具體在何處存放用戶信息铅忿,便是在SecurityContextHolder中荧琼,認證相關的信息是如何被存放到其中的被盈,便是通過SecurityContextPersistenceFilter
- 在Spring Security中汞扎,雖然安全上下文信息被存儲于Session中鲫构,但我們在實際使用中不應該直接操作Session塞耕,而應當使用SecurityContextHolder。
- 源碼分析:
public class SecurityContextPersistenceFilter extends GenericFilterBean {
static final String FILTER_APPLIED = "__spring_security_scpf_applied";
//安全上下文存儲的倉庫
private SecurityContextRepository repo;
public SecurityContextPersistenceFilter() {
//HttpSessionSecurityContextRepository是SecurityContextRepository接口的一個實現類
//使用HttpSession來存儲SecurityContext
this(new HttpSessionSecurityContextRepository());
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
if (request.getAttribute("__spring_security_scpf_applied") != null) {
// ensure that filter is only applied once per request
chain.doFilter(request, response);
} else {
boolean debug = this.logger.isDebugEnabled();
request.setAttribute("__spring_security_scpf_applied", Boolean.TRUE);
if (this.forceEagerSessionCreation) {
HttpSession session = request.getSession();
if (debug && session.isNew()) {
this.logger.debug("Eagerly created session: " + session.getId());
}
}
//包裝request溺欧,response
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
//從Session中獲取安全上下文信息
SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
boolean var13 = false;
try {
var13 = true;
//請求開始,設置安全上下文信息控汉,這樣就避免了用戶直接從Session中獲取安全上下文信息 SecurityContextHolder.setContext(contextBeforeChainExecution);
chain.doFilter(holder.getRequest(), holder.getResponse());
var13 = false;
} finally {
//過程操作失敗森逮,清空session
if (var13) {
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
if (debug) {
this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
//請求結束后,清空安全上下文信息
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
request.removeAttribute("__spring_security_scpf_applied");
if (debug) {
this.logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
}
- 上面可以知道,過濾器一般負責核心的處理流程,而具體的業(yè)務實現乎折,通常交給其中聚合的其他實體類。存儲安全上下文和讀取安全上下文的工作完全委托給了HttpSessionSecurityContextRepository處理侄榴,SecurityContextPersistenceFilter和HttpSessionSecurityContextRepository配合使用饲齐,構成了Spring Security整個調用鏈路的入口福青,為什么將它放在最開始的地方也是顯而易見的,后續(xù)的過濾器中大概率會依賴Session信息和安全上下文信息。
2瞒窒、UsernamePasswordAuthenticationFilter
-
表單認證是最常用的一個認證方式匿辩,一個最直觀的業(yè)務場景便是允許用戶在表單中輸入用戶名和密碼進行登錄然走,而這背后的UsernamePasswordAuthenticationFilter,在整個Spring Security的認證體系中則扮演著至關重要的角色。UsernamePasswordAuthenticationFilter主要肩負起了調用身份認證器噪珊,校驗身份的作用选酗,具體可參照下面的時序圖:
- 源碼分析:
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();
// //組裝成username+password形式的token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//Allow subclasses to set the "details" property
this.setDetails(request, authRequest);
//交給內部的AuthenticationManager去認證栓拜,并返回認證信息
return this.getAuthenticationManager().authenticate(authRequest);
}
}
UsernamePasswordAuthenticationFilter本身的代碼只包含了上述這么一個方法饲漾,非常簡略,而在其父類AbstractAuthenticationProcessingFilter中包含了大量的細節(jié)缕溉,值得我們分析,整個流程理解起來也并不難考传,主要就是內部調用了authenticationManager完成認證,根據認證結果執(zhí)行successfulAuthentication或者unsuccessfulAuthentication证鸥,無論成功失敗僚楞,一般的實現都是轉發(fā)或者重定向等處理.
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
//包含了一個身份認證器
private AuthenticationManager authenticationManager;
//用于實現remeberMe
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private RequestMatcher requiresAuthenticationRequestMatcher;
//這兩個Handler很關鍵,分別代表了認證成功和失敗相應的處理器
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
...
Authentication authResult;
try {
//此處實際上就是調用UsernamePasswordAuthenticationFilter的attemptAuthentication方法
authResult = attemptAuthentication(request, response);
if (authResult == null) {
//子類未完成認證枉层,立刻返回
return;
}
//認證成功后泉褐,處理一些與session相關的方法
sessionStrategy.onAuthentication(authResult, request, response);
}
//在認證過程中可以直接拋出異常,在過濾器中鸟蜡,就像此處一樣膜赃,進行捕獲
catch (InternalAuthenticationServiceException failed) {
//內部服務異常
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
//認證失敗
unsuccessfulAuthentication(request, response, failed);
return;
}
//認證成功
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//注意,認證成功后過濾器把authResult結果也傳遞給了成功處理器
successfulAuthentication(request, response, chain, authResult);
}
}
- 總結整個過程的四個步驟
1.判斷filter是否可以處理當前的請求揉忘,如果不可以則放行交給下一個filter
2.調用抽象方法attemptAuthentication進行驗證跳座,該方法由子類UsernamePasswordAuthenticationFilter實現
3.認證成功以后,回調一些與 session 相關的方法泣矛;
4.認證成功以后疲眷,認證成功后的相關回調方法;認證成功以后您朽,認證成功后的相關回調方法狂丝;
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
3、AnonymousAuthenticationFilter
- 匿名認證過濾器哗总,可能有人會想:匿名了還有身份美侦?我自己對于Anonymous匿名身份的理解是Spirng Security為了整體邏輯的統一性,即使是未通過認證的用戶魂奥,也給予了一個匿名身份菠剩。而AnonymousAuthenticationFilter該過濾器的位置也是非常的科學的,它位于常用的身份認證過濾器(如UsernamePasswordAuthenticationFilter耻煤、BasicAuthenticationFilter具壮、RememberMeAuthenticationFilter)之后,意味著只有在上述身份過濾器執(zhí)行完畢后哈蝇,SecurityContext依舊沒有用戶信息棺妓,AnonymousAuthenticationFilter該過濾器才會有意義——基于用戶一個匿名身份挑庶。
public class AnonymousAuthenticationFilter extends GenericFilterBean implements
InitializingBean {
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private String key;
private Object principal;
private List<GrantedAuthority> authorities;
//自動創(chuàng)建一個"anonymousUser"的匿名用戶,其具有ANONYMOUS角色
public AnonymousAuthenticationFilter(String key) {
this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
}
/**
*
* @param key key用來識別該過濾器創(chuàng)建的身份
* @param principal principal代表匿名用戶的身份
* @param authorities authorities代表匿名用戶的權限集合
*/
public AnonymousAuthenticationFilter(String key, Object principal,
List<GrantedAuthority> authorities) {
Assert.hasLength(key, "key cannot be null or empty");
Assert.notNull(principal, "Anonymous authentication principal must be set");
Assert.notNull(authorities, "Anonymous authorities must be set");
this.key = key;
this.principal = principal;
this.authorities = authorities;
}
...
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
//過濾器鏈都執(zhí)行到匿名認證過濾器這兒了還沒有身份信息掀抹,塞一個匿名身份進去
if (SecurityContextHolder.getContext().getAuthentication() == null) {
SecurityContextHolder.getContext().setAuthentication(
createAuthentication((HttpServletRequest) req));
}
chain.doFilter(req, res);
}
protected Authentication createAuthentication(HttpServletRequest request) {
//創(chuàng)建一個AnonymousAuthenticationToken
AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
principal, authorities);
auth.setDetails(authenticationDetailsSource.buildDetails(request));
return auth;
}
...
}
其實對比AnonymousAuthenticationFilter和UsernamePasswordAuthenticationFilter就可以發(fā)現一些門道了,UsernamePasswordAuthenticationToken對應AnonymousAuthenticationToken锥腻,他們都是Authentication的實現類,而Authentication則是被SecurityContextHolder(SecurityContext)持有的性芬,一切都被串聯在了一起峡眶。
4、ExceptionTranslationFilter
- ExceptionTranslationFilter異常轉換過濾器位于整個springSecurityFilterChain的后方植锉,用來轉換整個鏈路中出現的異常辫樱,將其轉化,顧名思義俊庇,轉化以意味本身并不處理狮暑。一般其只處理兩大類異常:AccessDeniedException訪問異常和AuthenticationException認證異常
- 這個過濾器非常重要,因為它將Java中的異常和HTTP的響應連接在了一起辉饱,這樣在處理異常時搬男,我們不用考慮密碼錯誤該跳到什么頁面,賬號鎖定該如何彭沼,只需要關注自己的業(yè)務邏輯缔逛,拋出相應的異常便可。如果該過濾器檢測到AuthenticationException溜腐,則將會交給內部的AuthenticationEntryPoint去處理,如果檢測到AccessDeniedException瓜喇,需要先判斷當前用戶是不是匿名用戶挺益,如果是匿名訪問,則和前面一樣運行AuthenticationEntryPoint乘寒,否則會委托給AccessDeniedHandler去處理望众,而AccessDeniedHandler的默認實現,是AccessDeniedHandlerImpl伞辛。所以ExceptionTranslationFilter內部的AuthenticationEntryPoint是至關重要的烂翰,顧名思義:認證的入口點。
5蚤氏、FilterSecurityInterceptor
- 由什么控制哪些資源是受限的甘耿,這些受限的資源需要什么權限,需要什么角色…這一切和訪問控制相關的操作竿滨,都是由FilterSecurityInterceptor完成的佳恬。也就是說FilterSecurityInterceptor和鑒權有關。
- FilterSecurityInterceptor的工作流程
FilterSecurityInterceptor從SecurityContextHolder中獲取Authentication對象于游,然后比對用戶擁有的權限和資源所需的權限毁葱。前者可以通過Authentication對象直接獲得,而后者則需要引入我們之前一直未提到過的兩個類:SecurityMetadataSource贰剥,AccessDecisionManager倾剿。
- 源碼:
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
if ((fi.getRequest() != null)
&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
&& observeOncePerRequest) {
// filter already applied to this request and user wants us to observe
// once-per-request handling, so don't re-do security checking
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
else {
// first time this request being called, so perform security checking
if (fi.getRequest() != null) {
fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
}
#1. before invocation重要
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
#2. 可以理解開始請求真正的 /persons 服務
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
}
finally {
super.finallyInvocation(token);
}
#3. after Invocation
super.afterInvocation(token, null);
}
}
- 總結整個過程為三個步驟
1、before invocation重要
2蚌成、請求真正的 /persons 服務
3前痘、after Invocation
(1)before invocation: AccessDecisionManager
protected InterceptorStatusToken beforeInvocation(Object object) {
...
Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource()
.getAttributes(object);
...
Authentication authenticated = authenticateIfRequired();
// Attempt authorization
try {
#1.重點
this.accessDecisionManager.decide(authenticated, object, attributes);
}
catch (AccessDeniedException accessDeniedException) {
publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated,accessDeniedException));
throw accessDeniedException;
}
...
}
attributes和object 是什么凛捏?發(fā)現object為當前請求的 url:/persons, 那么getAttributes方法就是使用當前的訪問資源路徑去匹配我們自己定義的匹配規(guī)則
三、授權
Spring Security默認使用AffirmativeBased實現 AccessDecisionManager 的 decide 方法來實現授權际度。
public void decide(Authentication authentication, Object object,
Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
int deny = 0;
#1.調用AccessDecisionVoter 進行vote(投票)
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
#1.1只要有voter投票為ACCESS_GRANTED葵袭,則通過 直接返回
case AccessDecisionVoter.ACCESS_GRANTED://1
return;
@#1.2只要有voter投票為ACCESS_DENIED,則記錄一下
case AccessDecisionVoter.ACCESS_DENIED://-1
deny++;
break;
default:
break;
}
}
if (deny > 0) {
#2.如果有兩個及以上AccessDecisionVoter(姑且稱之為投票者吧)都投ACCESS_DENIED乖菱,則直接就不通過了
throw new AccessDeniedException(messages.getMessage(
"AbstractAccessDecisionManager.accessDenied", "Access is denied"));
}
// To get this far, every AccessDecisionVoter abstained
checkAllowIfAllAbstainDecisions();
}
- 總結整個過程
1坡锡、調用AccessDecisionVoter 進行vote(投票)
2、只要有投通過(ACCESS_GRANTED)票窒所,則直接判為通過鹉勒。
3、如果沒有投通過則 deny++ ,最后判斷if(deny>0 拋出AccessDeniedException(未授權)
參考:
http://www.spring4all.com/article/447
https://github.com/qiuzhangwei/logback