天天看點

Spring Security Web 5.1.2 源碼解析 -- DefaultLoginPageGeneratingFilter概述源代碼解析參考文章

概述

當開發人員在安全配置中沒有配置登入頁面時,

Spring Security Web

會自動構造一個登入頁面給使用者。完成這一任務是通過一個過濾器來完成的,該過濾器就是

DefaultLoginPageGeneratingFilter

該過濾器支援兩種登入情景:

  • 使用者名/密碼表單登入
  • OpenID

    表單登入

無論以上哪種登入情景,該過濾器都會使用以下資訊用于建構登入HTML頁面 :

  • 目前請求是否為登入頁面請求的比對器定義–由配置明确指定或者使用預設值

    /login

  • 目前請求是否跳轉自登入錯誤處理以及相應異常資訊 – 預設對應url :

    /login?error

  • 目前請求是否跳轉自登出成功處理 – 預設對應url :

    /login?logout

  • 配置指定使用使用者名/密碼表單登入還是

    OpenID

    表單登入
  • 表單建構資訊
    • csrf token

      資訊
    • 針對使用者名/密碼表單登入的表單建構資訊
      • 登入表單送出處理位址
      • 使用者名表單字段名稱
      • 密碼表單字段名稱
      • RememberMe

        表單字段名稱
    • 針對

      OpenID

      表單登入的表單建構資訊
      • 登入表單送出處理位址
      • OpenID

        表單字段名稱
      • RememberMe

        表單字段名稱

該過濾器被請求到達時會首先看是不是自己關注的請求,如果是,則會根據相應資訊建構一個登入頁面

HTML

直接寫回浏覽器端,對該請求的處理也到此結束,不再繼續調用

filter chain

中的其他邏輯。

生成的使用者名/密碼表單登入頁面效果

由該Filter生成的使用者名/密碼表單登入頁面如下所示:

Spring Security Web 5.1.2 源碼解析 -- DefaultLoginPageGeneratingFilter概述源代碼解析參考文章

對應的 HTML 大緻如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
  </head>
  <body>
     <div class="container">
      <form class="form-signin" method="post" action="/login">
        <h2 class="form-signin-heading">Please sign in</h2>
        <p>
          <label for="username" class="sr-only">Username</label>
          <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
        </p>
        <p>
          <label for="password" class="sr-only">Password</label>
          <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
        </p>
<p><input type='checkbox' name='remember-me'/> Remember me on this computer.</p>
<input name="_csrf" type="hidden" value="befcd3c2-6ee1-461d-a7ba-316dce846d4a" />
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      </form>
</body></html>
           

生成的OpenID表單登入頁面效果

由該Filter生成的OpenID表單登入頁面如下所示:

Spring Security Web 5.1.2 源碼解析 -- DefaultLoginPageGeneratingFilter概述源代碼解析參考文章

對應的 HTML 大緻如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Please sign in</title>
    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
  </head>
  <body>
     <div class="container">
      <form name="oidf" class="form-signin" method="post" action="/login/openid">
        <h2 class="form-signin-heading">Login with OpenID Identity</h2>
        <p>
          <label for="username" class="sr-only">Identity</label>
          <input type="text" id="username" name="openid_identifier" class="form-control" placeholder="Username" required autofocus>
        </p>
<p><input type='checkbox' name='remember-me'/> Remember me on this computer.</p>
<input name="_csrf" type="hidden" value="9efcd951-5bf6-488f-93c2-83bd2240c2dc" />
        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
      </form>
</body></html>
           

源代碼解析

package org.springframework.security.web.authentication.ui;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.filter.GenericFilterBean;

/**
 * For internal use with namespace configuration in the case where a user doesn't
 * configure a login page. The configuration code will insert this filter in the chain
 * instead.
 * 内部使用的一個過濾器,當使用者沒有指定一個登入頁面時,安全配置邏輯自動插入這樣一個過濾器用于
 * 自動生成一個登入頁面。
 * 
 * Will only work if a redirect is used to the login page.
 *
 * @author Luke Taylor
 * @since 2.0
 */
public class DefaultLoginPageGeneratingFilter extends GenericFilterBean {
	public static final String DEFAULT_LOGIN_PAGE_URL = "/login";
	public static final String ERROR_PARAMETER_NAME = "error";
	private String loginPageUrl;
	private String logoutSuccessUrl;
	private String failureUrl;
	private boolean formLoginEnabled;
	private boolean openIdEnabled;
	// 用于構造使用者名/密碼表單登入頁面的參數
	/ 表單送出時的認證處理位址
	private String authenticationUrl;
	 使用者名表單字段的名稱
	private String usernameParameter;
	 密碼表單字段的名稱
	private String passwordParameter;
	 rememberMe表單字段的名稱
	private String rememberMeParameter;
	// 用于構造openID表單登入頁面的參數
	 送出時的認證處理位址
	private String openIDauthenticationUrl;
	 使用者名表單字段的名稱
	private String openIDusernameParameter;
	 rememberMe表單字段的名稱
	private String openIDrememberMeParameter;

	public DefaultLoginPageGeneratingFilter() {
	}

	public DefaultLoginPageGeneratingFilter(AbstractAuthenticationProcessingFilter filter) {
		if (filter instanceof UsernamePasswordAuthenticationFilter) {
			init((UsernamePasswordAuthenticationFilter) filter, null);
		}
		else {
			init(null, filter);
		}
	}

	public DefaultLoginPageGeneratingFilter(
			UsernamePasswordAuthenticationFilter authFilter,
			AbstractAuthenticationProcessingFilter openIDFilter) {
		init(authFilter, openIDFilter);
	}
	// 支援兩種登入方式:使用者名/密碼表單登入,openID登入,根據提供的filter參數的類型
	// 判斷使用了哪種登入方式
	private void init(UsernamePasswordAuthenticationFilter authFilter,
			AbstractAuthenticationProcessingFilter openIDFilter) {
		// 登入頁面,預設為 /logoin	
		this.loginPageUrl = DEFAULT_LOGIN_PAGE_URL;		
		// 預設的登出成功頁面 /login?logout
		this.logoutSuccessUrl = DEFAULT_LOGIN_PAGE_URL + "?logout";
		// 登入出錯頁面, 預設為 /login?error
		this.failureUrl = DEFAULT_LOGIN_PAGE_URL + "?" + ERROR_PARAMETER_NAME;
		if (authFilter != null) {
			formLoginEnabled = true;
			usernameParameter = authFilter.getUsernameParameter();
			passwordParameter = authFilter.getPasswordParameter();

			if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
				rememberMeParameter = ((AbstractRememberMeServices) authFilter
						.getRememberMeServices()).getParameter();
			}
		}

		if (openIDFilter != null) {
			openIdEnabled = true;
			openIDusernameParameter = "openid_identifier";

			if (openIDFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
				openIDrememberMeParameter = ((AbstractRememberMeServices) openIDFilter
						.getRememberMeServices()).getParameter();
			}
		}
	}

	public boolean isEnabled() {
		return formLoginEnabled || openIdEnabled;
	}

	public void setLogoutSuccessUrl(String logoutSuccessUrl) {
		this.logoutSuccessUrl = logoutSuccessUrl;
	}

	public String getLoginPageUrl() {
		return loginPageUrl;
	}

	public void setLoginPageUrl(String loginPageUrl) {
		this.loginPageUrl = loginPageUrl;
	}

	public void setFailureUrl(String failureUrl) {
		this.failureUrl = failureUrl;
	}

	public void setFormLoginEnabled(boolean formLoginEnabled) {
		this.formLoginEnabled = formLoginEnabled;
	}

	public void setOpenIdEnabled(boolean openIdEnabled) {
		this.openIdEnabled = openIdEnabled;
	}

	public void setAuthenticationUrl(String authenticationUrl) {
		this.authenticationUrl = authenticationUrl;
	}

	public void setUsernameParameter(String usernameParameter) {
		this.usernameParameter = usernameParameter;
	}

	public void setPasswordParameter(String passwordParameter) {
		this.passwordParameter = passwordParameter;
	}

	public void setRememberMeParameter(String rememberMeParameter) {
		this.rememberMeParameter = rememberMeParameter;
		this.openIDrememberMeParameter = rememberMeParameter;
	}

	public void setOpenIDauthenticationUrl(String openIDauthenticationUrl) {
		this.openIDauthenticationUrl = openIDauthenticationUrl;
	}

	public void setOpenIDusernameParameter(String openIDusernameParameter) {
		this.openIDusernameParameter = openIDusernameParameter;
	}

	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) {
			// 如果是上面三種任何一種情況,則自動生成一個登入HTML頁面寫回響應,
			// 該方法傳回,目前請求的處理結束。

			// 生成登入頁面的HTML内容
			String loginPageHtml = generateLoginPageHtml(request, loginError,
					logoutSuccess);
			response.setContentType("text/html;charset=UTF-8");
			response.setContentLength(loginPageHtml.length());
			// 将登入頁面HTML内容寫回浏覽器
			response.getWriter().write(loginPageHtml);

			// 目前請求的處理已經結果,方法傳回,不再繼續filter chain的調用
			return;
		}

		// 如果不是需要渲染一個登入頁面的其他情形,繼續filter chain的調用
		chain.doFilter(request, response);
	}
	// 生成登入頁面
	// 會根據目前是使用者名/密碼表單登入請求還是openID表單登入請求生成不同的HTML
	private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
			boolean logoutSuccess) {
		String errorMsg = "none";

		if (loginError) {
			// 如果是登入錯誤,則從session中擷取登入錯誤異常資訊,該錯誤資訊會組織到
			// 回寫給浏覽器端的HTML頁面中
			HttpSession session = request.getSession(false);

			if (session != null) {
				AuthenticationException ex = (AuthenticationException) session
						.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
				errorMsg = ex != null ? ex.getMessage() : "none";
			}
		}

		StringBuilder sb = new StringBuilder();

		sb.append("<html><head><title>Login Page</title></head>");

		if (formLoginEnabled) {
			sb.append("<body onload='document.f.").append(usernameParameter)
					.append(".focus();'>\n");
		}

		if (loginError) {
			// 出現登入錯誤的情況下,需要把登入錯誤資訊追加到頁面中
			sb.append("<p><font color='red'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
			sb.append(errorMsg);
			sb.append("</font></p>");
		}

		if (logoutSuccess) {
			// 如果該頁面登入請求跳轉自登出成功,在頁面中追加該資訊
			sb.append("<p><font color='green'>You have been logged out</font></p>");
		}

		if (formLoginEnabled) {
			// 針對使用者名/密碼表單登入的情形建構相應的表單
			sb.append("<h3>Login with Username and Password</h3>");
			sb.append("<form name='f' action='").append(request.getContextPath())
					.append(authenticationUrl).append("' method='POST'>\n");
			sb.append("<table>\n");
			sb.append("	<tr><td>User:</td><td><input type='text' name='");
			sb.append(usernameParameter).append("' value='").append("'></td></tr>\n");
			sb.append("	<tr><td>Password:</td><td><input type='password' name='")
					.append(passwordParameter).append("'/></td></tr>\n");

			if (rememberMeParameter != null) {
				sb.append("	<tr><td><input type='checkbox' name='")
						.append(rememberMeParameter)
						.append("'/></td><td>Remember me on this computer.</td></tr>\n");
			}

			sb.append("	<tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
			renderHiddenInputs(sb, request);
			sb.append("</table>\n");
			sb.append("</form>");
		}

		if (openIdEnabled) {
			// 針對OpenID表單登入的情形建構相應的表單
			sb.append("<h3>Login with OpenID Identity</h3>");
			sb.append("<form name='oidf' action='").append(request.getContextPath())
					.append(openIDauthenticationUrl).append("' method='POST'>\n");
			sb.append("<table>\n");
			sb.append("	<tr><td>Identity:</td><td><input type='text' size='30' name='");
			sb.append(openIDusernameParameter).append("'/></td></tr>\n");

			if (openIDrememberMeParameter != null) {
				sb.append("	<tr><td><input type='checkbox' name='")
						.append(openIDrememberMeParameter)
						.append("'></td><td>Remember me on this computer.</td></tr>\n");
			}

			sb.append("	<tr><td colspan='2'><input name=\"submit\" type=\"submit\" value=\"Login\"/></td></tr>\n");
			sb.append("</table>\n");
			renderHiddenInputs(sb, request);
			sb.append("</form>");
		}

		sb.append("</body></html>");

		return sb.toString();
	}
	// 如果請求的屬性:CsrfToken.class.getName()有值,則渲染一個隐藏的針對csrf token的表單輸入框
	// 預設名稱是 _csrf
	private void renderHiddenInputs(StringBuilder sb, HttpServletRequest request) {
		CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());

		if (token != null) {
			sb.append("	<input name=\"" + token.getParameterName()
					+ "\" type=\"hidden\" value=\"" + token.getToken() + "\" />\n");
		}
	}

	private boolean isLogoutSuccess(HttpServletRequest request) {
		return logoutSuccessUrl != null && matches(request, logoutSuccessUrl);
	}

	// 檢測目前請求是否是一個登入頁面請求
	private boolean isLoginUrlRequest(HttpServletRequest request) {
		return matches(request, loginPageUrl);
	}
	// 檢測目前請求是否是一個登入錯誤頁面請求
	private boolean isErrorPage(HttpServletRequest request) {
		return matches(request, failureUrl);
	}

	private boolean matches(HttpServletRequest request, String url) {
		if (!"GET".equals(request.getMethod()) || url == null) {
			// 參數檢查:
			// 1. 對登入頁面的請求僅僅支援GET方式
			// 2. url 不能為空
			return false;
		}
		// 擷取請求uri,注意其中不包含QueryString部分
		String uri = request.getRequestURI();
		int pathParamIndex = uri.indexOf(';');

		if (pathParamIndex > 0) {
			// strip everything after the first semi-colon
			uri = uri.substring(0, pathParamIndex);
		}

		if (request.getQueryString() != null) {
			uri += "?" + request.getQueryString();
		}
		// 比較請求的uri和預期的url是否相同
		if ("".equals(request.getContextPath())) {
			return uri.equals(url);
		}

		return uri.equals(request.getContextPath() + url);
	}
}

           

參考文章

Spring Security Web 5.1.2 源碼解析 – 安全相關Filter清單