本篇主要講解 SpringSecurity提供的默認表單登錄頁 它是如何展示的的流程,

涉及


1.FilterSecurityInterceptor," />

SpringSecurity 默認表單登錄頁展示流程源碼

 更新時間:2020-01-22 11:00:21   作者:佚名   我要評論(0)

SpringSecurity 默認表單登錄頁展示流程源碼
本篇主要講解 SpringSecurity提供的默認表單登錄頁 它是如何展示的的流程,

涉及


1.FilterSecurityInterceptor,

SpringSecurity 默認表單登錄頁展示流程源碼

本篇主要講解 SpringSecurity提供的默認表單登錄頁 它是如何展示的的流程,
涉及

1.FilterSecurityInterceptor,
2.ExceptionTranslationFilc,xmccmc,ter ,
3.DefaultLoginPageGeneratingFilter 過濾器,
并且簡單介紹了 AccessDecisionManager 投票機制

 1.準備工作(體驗SpringSecurity默認表單認證)

  1.1 創建SpringSecurity項目

  先通過IDEA 創建一個SpringBoot項目 并且依賴SpringSecurity,Web依賴

  此時pom.xml會自動添加

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>

  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<ConfigAttribute> 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("<!DOCTYPE html>\n"
   + "<html lang=\"en\">\n"
   + " <head>\n"
   + " <meta charset=\"utf-8\">\n"
   + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"
   + " <meta name=\"description\" content=\"\">\n"
   + " <meta name=\"author\" content=\"\">\n"
   + " <title>Please sign in</title>\n"
   + " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n"
   + " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
   + " </head>\n"
   + " <body>\n"
   + "  <div class=\"container\">\n");

 String contextPath = request.getContextPath();
 if (this.formLoginEnabled) {
  sb.append("  <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"
    + "  <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
    + createError(loginError, errorMsg)
    + createLogoutSuccess(logoutSuccess)
    + "  <p>\n"
    + "   <label for=\"username\" class=\"sr-only\">Username</label>\n"
    + "   <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
    + "  </p>\n"
    + "  <p>\n"
    + "   <label for=\"password\" class=\"sr-only\">Password</label>\n"
    + "   <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n"
    + "  </p>\n"
    + createRememberMe(this.rememberMeParameter)
    + renderHiddenInputs(request)
    + "  <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
    + "  </form>\n");
 }

 if (openIdEnabled) {
  sb.append("  <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"
    + "  <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n"
    + createError(loginError, errorMsg)
    + createLogoutSuccess(logoutSuccess)
    + "  <p>\n"
    + "   <label for=\"username\" class=\"sr-only\">Identity</label>\n"
    + "   <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
    + "  </p>\n"
    + createRememberMe(this.openIDrememberMeParameter)
    + renderHiddenInputs(request)
    + "  <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
    + "  </form>\n");
 }

 if (oauth2LoginEnabled) {
  sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
  sb.append(createError(loginError, errorMsg));
  sb.append(createLogoutSuccess(logoutSuccess));
  sb.append("<table class=\"table table-striped\">\n");
  for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
   sb.append(" <tr><td>");
   String url = clientAuthenticationUrlToClientName.getKey();
   sb.append("<a href=\"").append(contextPath).append(url).append("\">");
   String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue());
   sb.append(clientName);
   sb.append("</a>");
   sb.append("</td></tr>\n");
  }
  sb.append("</table>\n");
 }

 if (this.saml2LoginEnabled) {
  sb.append("<h2 class=\"form-signin-heading\">Login with SAML 2.0</h2>");
  sb.append(createError(loginError, errorMsg));
  sb.append(createLogoutSuccess(logoutSuccess));
  sb.append("<table class=\"table table-striped\">\n");
  for (Map.Entry<String, String> relyingPartyUrlToName : saml2AuthenticationUrlToProviderName.entrySet()) {
   sb.append(" <tr><td>");
   String url = relyingPartyUrlToName.getKey();
   sb.append("<a href=\"").append(contextPath).append(url).append("\">");
   String partyName = HtmlUtils.htmlEscape(relyingPartyUrlToName.getValue());
   sb.append(partyName);
   sb.append("</a>");
   sb.append("</td></tr>\n");
  }
  sb.append("</table>\n");
 }
 sb.append("</div>\n");
 sb.append("</body></html>");

 return sb.toString();
}

至此 SpringSecurity 默認表單登錄頁展示流程源碼部分已經全部講解完畢,會渲染出下面的頁面,但是一定要有網的情況,否則樣式可能會變化

6.總結

本篇主要講解 SpringSecurity提供的默認表單登錄頁 它是如何展示的的流程,包括涉及這一流程中相關的 3個過濾器

1.FilterSecurityInterceptor,
2.ExceptionTranslationFilter ,
3.DefaultLoginPageGeneratingFilter 過濾器,
并且簡單介紹了一下 AccessDecisionManager 它主要進行投票來判斷該用戶是否能夠訪問相應的 資源
AccessDecisionManager 投票機制我也沒有深究 后續我會詳細深入一下再展開

以上所述是小編給大家介紹的SpringSecurity 默認表單登錄頁展示流程源碼,希望對大家有所幫助!

您可能感興趣的文章:

  • Spring Security 表單登錄功能的實現方法
  • Spring Security在標準登錄表單中添加一個額外的字段
  • spring security自定義登錄頁面
  • SpringBoot+Spring Security+JWT實現RESTful Api權限控制的方法
  • Spring Security OAuth2實現使用JWT的示例代碼
  • 詳解spring security之httpSecurity使用示例
  • SpringBoot + SpringSecurity 短信驗證碼登錄功能實現

相關文章

  • SpringSecurity 默認表單登錄頁展示流程源碼

    SpringSecurity 默認表單登錄頁展示流程源碼

    SpringSecurity 默認表單登錄頁展示流程源碼 本篇主要講解 SpringSecurity提供的默認表單登錄頁 它是如何展示的的流程, 涉及 1.FilterSecurityInterceptor,
    2020-01-22
  • java GUI編程之監聽操作實例分析

    java GUI編程之監聽操作實例分析

    本文實例講述了java GUI編程之監聽操作。分享給大家供大家參考,具體如下: 當點擊Frame中的component組件時,會產生相應的效果,但是相應的其必須進行監聽,確定是
    2020-01-22
  • JavaScript設計模型Iterator實例解析

    JavaScript設計模型Iterator實例解析

    這篇文章主要介紹了JavaScript設計模型Iterator實例解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下 Iter
    2020-01-22
  • java GUI編程之paint繪制操作示例

    java GUI編程之paint繪制操作示例

    本文實例講述了java GUI編程之paint繪制操作。分享給大家供大家參考,具體如下: import java.awt.*; public class Testpint { public static void main(Stri
    2020-01-22
  • java GUI編程之布局控制器(Layout)實例分析

    java GUI編程之布局控制器(Layout)實例分析

    本文實例講述了java GUI編程之布局控制器(Layout)。分享給大家供大家參考,具體如下: 布局控制器,是用來系統自動分配各個component在window內部是怎么排布的;默
    2020-01-22
  • Vue設置長時間未操作登錄自動到期返回登錄頁

    Vue設置長時間未操作登錄自動到期返回登錄頁

    Vue設置長時間未操作登錄以后自動到期返回登錄頁 首先我們寫在main.js文件中 import routerUtil from "@/utils/routerutil";//先將js文件在main.js中引入 routerU
    2020-01-22
  • Python @property裝飾器原理解析

    Python @property裝飾器原理解析

    這篇文章主要介紹了Python @property裝飾器原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下 1.通過@p
    2020-01-22
  • spring cloud gateway整合sentinel實現網關限流

    spring cloud gateway整合sentinel實現網關限流

    這篇文章主要介紹了spring cloud gateway整合sentinel實現網關限流,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參
    2020-01-22
  • python基于property()函數定義屬性

    python基于property()函數定義屬性

    這篇文章主要介紹了python基于property()函數定義屬性,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下 正常情
    2020-01-22
  • vue中js判斷長時間不操作界面自動退出登錄(推薦)

    vue中js判斷長時間不操作界面自動退出登錄(推薦)

    需求說明,后臺有做半個小時不請求接口的話返回標識退出登錄,但是要請求接口才行,現在要實現前端用js判斷半個小時不操作界面的話自動跳轉到登錄頁面。 創建一個.j
    2020-01-22

最新評論

买宝宝用品赚钱吗