Springboot security cas整合方案中不可或缺的校驗Filter類或者稱為認證Filter類,其內部包含校驗器、權限獲取等,特開辟新地啃啃
繼承結構
- AbstractAuthenticationProcessingFilter
- CasAuthenticationFilter
其中父類AbstractAuthenticationProcessingFilter#doFilter()
是模板處理邏輯方法,而子類主要實現了校驗方法CasAuthenticationFilter#attemptAuthentication()
方法。下面就對這兩塊進行代碼層面的分析
AbstractAuthenticationProcessingFilter#doFilter-處理邏輯
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//是否需要驗證,這里cas子類對其進行了復寫
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
//憑證信息
Authentication authResult;
try {
//調用子類來進行相關的驗證操作,供子類復寫
authResult = attemptAuthentication(request, response);
if (authResult == null) {
//返回為空,則校驗停止
return;
}
//session策略校驗,默認不校驗
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
//對其中產生的異常進行頁面輸出,即直接以頁面呈現錯誤
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
//對其中產生的異常進行頁面輸出,即直接以頁面呈現錯誤
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
//認證成功后是否還往下走,默認為false
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//直接跳轉至配置好的登錄成功頁面,這里cas子類對其進行了復寫
successfulAuthentication(request, response, chain, authResult);
}
其中CasAuthenticationFilter
對以下方法進行了復寫,分別為requiresAuthentication()
、successfulAuthentication()
、attemptAuthentication()
方法
CasAuthenticationFilter#requiresAuthentication-是否校驗判斷
protected boolean requiresAuthentication(final HttpServletRequest request,
final HttpServletResponse response) {
//是否與設置的登錄路徑匹配
final boolean serviceTicketRequest = serviceTicketRequest(request, response);
//對含有ticket參數的請求會返回true
final boolean result = serviceTicketRequest || proxyReceptorRequest(request)
|| (proxyTicketRequest(serviceTicketRequest, request));
if (logger.isDebugEnabled()) {
logger.debug("requiresAuthentication = " + result);
}
return result;
}
對login請求以及token請求則返回true表示需要驗證
CasAuthenticationFilter#attemptAuthentication-具體校驗處理
@Override
public Authentication attemptAuthentication(final HttpServletRequest request,
final HttpServletResponse response) throws AuthenticationException,
IOException {
// if the request is a proxy request process it and return null to indicate the
// request has been processed
//代理服務的請求處理,涉及PGT
if (proxyReceptorRequest(request)) {
logger.debug("Responding to proxy receptor request");
//直接響應輸出
CommonUtils.readAndRespondToProxyReceptorRequest(request, response,
this.proxyGrantingTicketStorage);
return null;
}
//判斷是否對應指定的請求(login請求),支持ant-style方式
final boolean serviceTicketRequest = serviceTicketRequest(request, response);
//login請求為"_cas_stateful_",非login請求為"_cas_stateless_"
final String username = serviceTicketRequest ? CAS_STATEFUL_IDENTIFIER
: CAS_STATELESS_IDENTIFIER;
//獲取ticket
String password = obtainArtifact(request);
//passwprd一般不可為空,這在provider處理類中會拋異常
if (password == null) {
logger.debug("Failed to obtain an artifact (cas ticket)");
password = "";
}
final UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
//通過CasAuthenticationProvider來進行具體的校驗,包含ticket驗證以及當前用戶權限集合獲取
return this.getAuthenticationManager().authenticate(authRequest);
}
具體的校驗通過CasAuthenticationProvider
來實現
CasAuthenticationProvider-cas校驗器
cas校驗器,看下主要的實現方法
- CasAuthenticationProvider#afterPropertiesSet()
主要是檢驗必須的屬性是否設置public void afterPropertiesSet() throws Exception { //權限獲取處理對象 Assert.notNull(this.authenticationUserDetailsService, "An authenticationUserDetailsService must be set"); //ticket校驗器 Assert.notNull(this.ticketValidator, "A ticketValidator must be set"); //stateless對應的緩存,默認為NullStatelessTicketCache Assert.notNull(this.statelessTicketCache, "A statelessTicketCache must be set"); //必須設置key Assert.hasText( this.key, "A Key is required so CasAuthenticationProvider can identify tokens it previously authenticated"); //默認為SpringSecurityMessageSource.getAccessor() Assert.notNull(this.messages, "A message source must be set"); }
- CasAuthenticationProvider#authenticate
校驗處理方法,源碼如下//此處傳過來的authentication類型為UsernamePasswordAuthenticationToken public Authentication authenticate(Authentication authentication) throws AuthenticationException { //此處為true if (!supports(authentication.getClass())) { return null; } if (authentication instanceof UsernamePasswordAuthenticationToken && (!CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER .equals(authentication.getPrincipal().toString()) && !CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER .equals(authentication.getPrincipal().toString()))) { // UsernamePasswordAuthenticationToken not CAS related return null; } // If an existing CasAuthenticationToken, just check we created it if (authentication instanceof CasAuthenticationToken) { if (this.key.hashCode() == ((CasAuthenticationToken) authentication) .getKeyHash()) { return authentication; } else { throw new BadCredentialsException( messages.getMessage("CasAuthenticationProvider.incorrectKey", "The presented CasAuthenticationToken does not contain the expected key")); } } // Ensure credentials are presented,確保ticket不為空,否則將拋出異常 if ((authentication.getCredentials() == null) || "".equals(authentication.getCredentials())) { throw new BadCredentialsException(messages.getMessage( "CasAuthenticationProvider.noServiceTicket", "Failed to provide a CAS service ticket to validate")); } boolean stateless = false; if (authentication instanceof UsernamePasswordAuthenticationToken && CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER.equals(authentication .getPrincipal())) { stateless = true; } CasAuthenticationToken result = null; //對非login請求的嘗試從緩存中獲取 if (stateless) { // Try to obtain from cache result = statelessTicketCache.getByTicketId(authentication.getCredentials() .toString()); } if (result == null) { //第一次校驗則用ticketValidator去cas服務端進行ticket校驗 result = this.authenticateNow(authentication); result.setDetails(authentication.getDetails()); } //對非login請求的castoken進行緩存 if (stateless) { // Add to cache statelessTicketCache.putTicketInCache(result); } return result; }
- CasAuthenticationProvider#authenticateNow
實際的校驗處理方法,源碼如下private CasAuthenticationToken authenticateNow(final Authentication authentication) throws AuthenticationException { try { //TicketValidator一般只需要設置casServerUrlPrefix前綴,實際的請求全路徑如下,以Cas20ServiceTicketValidator為例 //https://example.casserver.com/cas/serviceValidator?service=https://example.casclient.com/ final Assertion assertion = this.ticketValidator.validate(authentication .getCredentials().toString(), getServiceUrl(authentication)); //調用authenticationUserDetailsService獲取當前用戶所擁有的權限 final UserDetails userDetails = loadUserByAssertion(assertion); userDetailsChecker.check(userDetails); //組裝成CasAuthenticationToken來保存校驗信息,供保存至spring的安全上下文中 return new CasAuthenticationToken(this.key, userDetails, authentication.getCredentials(), authoritiesMapper.mapAuthorities(userDetails.getAuthorities()), userDetails, assertion); } catch (final TicketValidationException e) { //ticket校驗失敗則拋出異常,此異常會被父類獲取而調用failerhandler將錯誤寫向頁面 throw new BadCredentialsException(e.getMessage(), e); } }
- CasAuthenticationProvider的必要屬性含義
- authenticationUserDetailsService-權限獲取對象
- ticketValidator-ticket校驗器,其中需要設置cas服務端的校驗地址前綴
casServerUrlPrefix
- key-設置唯一標識
- CasAuthenticationProvider校驗過程中如果ticket為空或者ticket校驗失敗都會由
AbstractAuthenticationProcessingFilter
類抓取並將錯誤信息寫入到頁面中,從而關於ticket的異常信息都會顯示至前端頁面- CasAuthenticationProvider校驗成功后會生成
CasAuthenticationToken
,且設置authenticated
為true
並保存至spring的安全上下文中,這在FilterSecurityInterceptor
Filter類會有所作用
CasAuthenticationFilter#successfulAuthentication-校驗成功處理
protected final void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
//如果請求含有ticket參數,返回true
//login請求則直接返回false從而調用父類的successfulAuthentication()來直接響應頁面
boolean continueFilterChain = proxyTicketRequest(
serviceTicketRequest(request, response), request);
if (!continueFilterChain) {
super.successfulAuthentication(request, response, chain, authResult);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//保存Authentication憑證信息
SecurityContextHolder.getContext().setAuthentication(authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
//往下繼續走
chain.doFilter(request, response);
}
小結
CasAuthenticationFilter的放行策略:非登錄請求;非代理接收請求;非ticket請求
對登錄請求的成功處理是直接跳轉至指定的頁面,可通過
SimpleUrlAuthenticationSuccessHandler#setDefaultTargetUrl(String url)
設置;
對非登錄請求比如token請求
的操作將保存校驗通過的Authentication
對象至SecurityContextHolder.getContext()
上下文中再放行CasAuthenticationProvider校驗過程中如果ticket為空或者ticket校驗失敗都會由
AbstractAuthenticationProcessingFilter
類抓取並將錯誤信息寫入到頁面中,從而關於ticket的異常信息都會顯示至前端頁面
溫馨提示:cas服務端登錄成功后的service路徑不要為login請求,避免token沒拿到就被攔截從而輸出錯誤頁面其中對ticket進行校驗的是
CasAuthenticationProvider
對象,包括ticket校驗以及權限獲取