javascript
springsecurity不拦截某个接口_SpringSecurity 默认表单登录页展示流程源码
SpringSecurity 默認表單登錄頁展示流程源碼
本篇主要講解 SpringSecurity提供的默認表單登錄頁 它是如何展示的的流程,涉及1.FilterSecurityInterceptor,2.ExceptionTranslationFilter ,3.DefaultLoginPageGeneratingFilter 過濾器,并且簡單介紹了 AccessDecisionManager 投票機制
?1.準備工作(體驗SpringSecurity默認表單認證)
??1.1 創建SpringSecurity項目
??先通過IDEA 創建一個SpringBoot項目 并且依賴SpringSecurity,Web依賴
??此時pom.xml會自動添加
org.springframework.boot spring-boot-starter-security??1.2 提供一個接口
@RestController public class HelloController { @RequestMapping("/hello") public String hello() { return "Hello SpringSecurity"; } }??1.3 啟動項目
??直接訪問 提供的接口
http://localhost:8080/hello??會發現瀏覽器被直接重定向到了 /login 并且顯示如下默認的表單登錄頁
http://localhost:8080/login??顯示如下:
1.4 登錄
??在啟動項目的時候 控制臺會打印一個 seuciryt password : xxx
Using generated security password: f520875f-ea2b-4b5d-9b0c-f30c0c17b90b??直接登錄
用戶名:user 密碼 :f520875f-ea2b-4b5d-9b0c-f30c0c17b90b??登錄成功并且 瀏覽器又會重定向到 剛剛訪問的接口
?2.springSecurityFilterchain 過濾器鏈
?如果你看過我另一篇關于SpringSecurity初始化源碼的博客,那么你一定知道當SpringSecurity項目啟動完成后會初始化一個 springSecurityFilterchain 它內部 additionalFilters屬性初始化了很多Filter 如下所有的請求都會經過這一系列的過濾器 Spring Security就是通過這些過濾器 來進行認證授權等
?3.FilterSecurityInterceptor (它會判斷這次請求能否通過)
?FilterSecurityInterceptor是過濾器鏈中最后一個過濾器,主要用于判斷請求能否通過,內部通過AccessDecisionManager 進行投票判斷
?當我們未登錄訪問
http://localhost:8080/hello?請求會被 FilterSecurityInterceptor 攔截
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); }?重點看invoke方法
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 && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } }?源碼中有這樣一句,其實就是判斷當前用戶是否能夠訪問指定的接口,可以則執行 fi.getChain().doFilter 調用訪問的接口否則 內部會拋出異常
InterceptorStatusToken token = super.beforeInvocation(fi);**?beforeInvocation 方法內部是通過 accessDecisionManager 去做決定的?Spring Security已經內置了幾個基于投票的AccessDecisionManager包括(AffirmativeBased ,ConsensusBased ,UnanimousBased)當然如果需要你也可以實現自己的AccessDecisionManager**
?使用這種方式,一系列的AccessDecisionVoter將會被AccessDecisionManager用來對Authentication是否有權訪問受保護對象進行投票,然后再根據投票結果來決定是否要拋出AccessDeniedException
this.accessDecisionManager.decide(authenticated, object, attributes);?AffirmativeBased的 decide的實現如下
public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException { int deny = 0; Iterator var5 = this.getDecisionVoters().iterator(); while(var5.hasNext()) { AccessDecisionVoter voter = (AccessDecisionVoter)var5.next(); int result = voter.vote(authentication, object, configAttributes); if (this.logger.isDebugEnabled()) { this.logger.debug("Voter: " + voter + ", returned: " + result); } switch(result) { case -1: ++deny; break; case 1: return; } } if (deny > 0) { throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied")); } else { this.checkAllowIfAllAbstainDecisions(); } }?AffirmativeBased的邏輯是這樣的:
(1)只要有AccessDecisionVoter的投票為ACCESS_GRANTED則同意用戶進行訪問; (2)如果全部棄權也表示通過; (3)如果沒有一個人投贊成票,但是有人投反對票,則將拋出AccessDeniedException。?當我們第一次訪問的時候
http://localhost:8080/hello的時候?返回 result = -1 會拋出 AccessDeniedException 拒絕訪問異常
?4.ExceptionTranslationFilter (捕獲AccessDeniedException異常)
?該過濾器它會接收到FilterSecurityInterceptor拋出的 AccessDeniedException異常)并且進行捕獲,然后發送重定向到/login請求
?源碼如下:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; try { chain.doFilter(request, response); logger.debug("Chain processed normally"); } catch (IOException ex) { throw ex; } catch (Exception ex) { // Try to extract a SpringSecurityException from the stacktrace Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); RuntimeException ase = (AuthenticationException) throwableAnalyzer .getFirstThrowableOfType(AuthenticationException.class, causeChain); if (ase == null) { ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType( AccessDeniedException.class, causeChain); } if (ase != null) { if (response.isCommitted()) { throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex); } handleSpringSecurityException(request, response, chain, ase); } else { // Rethrow ServletExceptions and RuntimeExceptions as-is if (ex instanceof ServletException) { throw (ServletException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } // Wrap other Exceptions. This shouldn't actually happen // as we've already covered all the possibilities for doFilter throw new RuntimeException(ex); } } }?當獲取異常后 調用
handleSpringSecurityException(request, response, chain, ase);?handleSpringSecurityException 源碼如下:
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException { if (exception instanceof AuthenticationException) { logger.debug( "Authentication exception occurred; redirecting to authentication entry point", exception); sendStartAuthentication(request, response, chain, (AuthenticationException) exception); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { logger.debug( "Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point", exception); sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( messages.getMessage( "ExceptionTranslationFilter.insufficientAuthentication", "Full authentication is required to access this resource"))); } else { logger.debug( "Access is denied (user is not anonymous); delegating to AccessDeniedHandler", exception); accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); } } }?先判斷獲取的異常是否是AccessDeniedException 再判斷是否是匿名用戶,如果是則調用 sendStartAuthentication 重定向到登錄頁面
?重定向登錄頁面之前會保存當前訪問的路徑,這就是為什么我們訪問 /hello接口后 再登錄成功后又會跳轉到 /hello接口,因為在重定向到/login接口前 這里進行了保存 requestCache.saveRequest(request, response);
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException { // SEC-112: Clear the SecurityContextHolder's Authentication, as the // existing Authentication is no longer considered valid SecurityContextHolder.getContext().setAuthentication(null); requestCache.saveRequest(request, response); logger.debug("Calling Authentication entry point."); authenticationEntryPoint.commence(request, response, reason); }?authenticationEntryPoint.commence(request, response, reason);方法內部
?調用LoginUrlAuthenticationEntryPoint 的 commence方法
?LoginUrlAuthenticationEntryPoint 的commence方法內部有 構造重定向URL的方法
redirectUrl = buildRedirectUrlToLoginPage(request, response, authException); protected String buildRedirectUrlToLoginPage(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { String loginForm = determineUrlToUseForThisRequest(request, response, authException); protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) { return getLoginFormUrl(); }?最終會獲取到需要重定向的URL /login?然后sendRedirect 既會重定向到 /login 請求
?5.DefaultLoginPageGeneratingFilter (會捕獲重定向的/login 請求)
?DefaultLoginPageGeneratingFilter是過濾器鏈中的一個用于捕獲/login請求,并且渲染出一個默認表單頁面
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; boolean loginError = isErrorPage(request); boolean logoutSuccess = isLogoutSuccess(request); if (isLoginUrlRequest(request) || loginError || logoutSuccess) { String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess); response.setContentType("text/html;charset=UTF-8"); response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length); response.getWriter().write(loginPageHtml); return; } chain.doFilter(request, response); }?isLoginUrlRequest 判斷請求是否是 loginPageUrl
private boolean isLoginUrlRequest(HttpServletRequest request) { return matches(request, loginPageUrl); }?因為我們沒有配置所以 默認的 loginPageUrl = /login
?驗證通過請求路徑 能匹配 loginPageUrl
String loginPageHtml = generateLoginPageHtml(request, loginError, logoutSuccess);?generateLoginPageHtml 繪制默認的HTML 頁面,到此我們默認的登錄頁面怎么來的就解釋清楚了
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) { String errorMsg = "Invalid credentials"; if (loginError) { HttpSession session = request.getSession(false); if (session != null) { AuthenticationException ex = (AuthenticationException) session .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION); errorMsg = ex != null ? ex.getMessage() : "Invalid credentials"; } } StringBuilder sb = new StringBuilder(); sb.append("" + "" + " " + " " + " " + " " + " " + " Please sign in" + " " + " " + " " + " " + " "); String contextPath = request.getContextPath(); if (this.formLoginEnabled) { sb.append(" " + "Please sign in
" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "" + " Username" + " " + "
" + "" + " Password" + " " + "
" + createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request) + " Sign in" + " "); } if (openIdEnabled) { sb.append(" " + "Login with OpenID Identity
" + createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "" + " Identity" + " " + "
" + createRememberMe(this.openIDrememberMeParameter) + renderHiddenInputs(request) + " Sign in" + " "); } if (oauth2LoginEnabled) { sb.append("Login with OAuth 2.0
"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append(""); for (Map.Entry clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) { sb.append(" "); String url = clientAuthenticationUrlToClientName.getKey(); sb.append(""); String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue()); sb.append(clientName); sb.append(""); sb.append(""); } sb.append(""); } if (this.saml2LoginEnabled) { sb.append("Login with SAML 2.0
"); sb.append(createError(loginError, errorMsg)); sb.append(createLogoutSuccess(logoutSuccess)); sb.append(""); for (Map.Entry relyingPartyUrlToName : saml2AuthenticationUrlToProviderName.entrySet()) { sb.append(" "); String url = relyingPartyUrlToName.getKey(); sb.append(""); String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue()); sb.append(partyName); sb.append(""); sb.append(""); } sb.append(""); } sb.append(""); sb.append(""); return sb.toString(); }至此 SpringSecurity 默認表單登錄頁展示流程源碼部分已經全部講解完畢,會渲染出下面的頁面,但是一定要有網的情況,否則樣式可能會變化
6.總結
本篇主要講解 SpringSecurity提供的默認表單登錄頁 它是如何展示的的流程,包括涉及這一流程中相關的 3個過濾器
1.FilterSecurityInterceptor
2.ExceptionTranslationFilter
3.DefaultLoginPageGeneratingFilter
并且簡單介紹了一下 AccessDecisionManager 它主要進行投票來判斷該用戶是否能夠訪問相應的 資源AccessDecisionManager 投票機制我也沒有深究 后續我會詳細深入一下再展開
個人博客網站 https://www.askajohnny.com 歡迎訪問!
本文由博客一文多發平臺 https://openwrite.cn?from=article_bottom 發布!
總結
以上是生活随笔為你收集整理的springsecurity不拦截某个接口_SpringSecurity 默认表单登录页展示流程源码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 意外的服务器响应_响应式系统reacti
- 下一篇: inner join 和 exists