(1). 概述

比较好奇一件事情,就是Spring Security是如何生成登录页面的呢?

(2). WebSecurityConfigurerAdapter

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
		
		// ***************************************************************************
		// FormLoginConfigurer是核心类
		// ***************************************************************************
        FormLoginConfigurer formLoginConfigurer = http.formLogin();
	}
}		

(3). FormLoginConfigurer

public final class FormLoginConfigurer<H extends HttpSecurityBuilder<H>> extends
		AbstractAuthenticationFilterConfigurer<H, FormLoginConfigurer<H>, UsernamePasswordAuthenticationFilter> {

	public FormLoginConfigurer() {
		// *******************************************************************************
		// 1. 配置了验证:账号和密码登录Filter.暂时,咱们先不管理这个.
		// *******************************************************************************
		super(new UsernamePasswordAuthenticationFilter(), null);
		usernameParameter("username");
		passwordParameter("password");
	}
	
	// 我们知道,init方法最终是会被HttpSecurity统一回调的
	public void init(H http) throws Exception {
		super.init(http);
		// *******************************************************************************
		// 2. 初始化默认登录页面Filter
		// *******************************************************************************
		initDefaultLoginFilter(http);
	}

	private void initDefaultLoginFilter(H http) {
		// *******************************************************************************
		// 3. 从Spring容器的上下文中获得一个Filter:DefaultLoginPageGeneratingFilter
		// *******************************************************************************
		DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http.getSharedObject(DefaultLoginPageGeneratingFilter.class);
		if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
			// 对DefaultLoginPageGeneratingFilter进行配置
			loginPageGeneratingFilter.setFormLoginEnabled(true);
			loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
			loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
			loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
			loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
			loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
		}
	} // end initDefaultLoginFilter
}

(4). DefaultLoginPageGeneratingFilter

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 boolean oauth2LoginEnabled;
	private String authenticationUrl;
	private String usernameParameter;
	private String passwordParameter;
	private String rememberMeParameter;
	private String openIDauthenticationUrl;
	private String openIDusernameParameter;
	private String openIDrememberMeParameter;
	private Map<String, String> oauth2AuthenticationUrlToClientName;
	private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = request -> Collections.emptyMap();
	
	// ************************************************************************************
	// GenericFilterBean实现了Filter,所以,重点关注:filter方法.
	// ************************************************************************************
	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);
	} // end doFilter
	
	
	// 居然直接把要生成的页面HTML,写在代码里了.
	private String generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess) {
		String errorMsg = "none";

		if (loginError) {
			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 style='color:red;'>Your login attempt was not successful, try again.<br/><br/>Reason: ");
			sb.append(errorMsg);
			sb.append("</p>");
		}

		if (logoutSuccess) {
			sb.append("<p style='color:green;'>You have been logged out</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) {
			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>");
		}

		if (oauth2LoginEnabled) {
			sb.append("<h3>Login with OAuth 2.0</h3>");
			sb.append("<table>\n");
			for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
				sb.append(" <tr><td>");
				sb.append("<a href=\"").append(request.getContextPath()).append(clientAuthenticationUrlToClientName.getKey()).append("\">");
				sb.append(HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue(), "UTF-8"));
				sb.append("</a>");
				sb.append("</td></tr>\n");
			}
			sb.append("</table>\n");
		}

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

		return sb.toString();
	} // end generateLoginPageHtml
	
	
	// 创建隐藏域
	private void renderHiddenInputs(StringBuilder sb, HttpServletRequest request) {
		for(Map.Entry<String, String> input : this.resolveHiddenInputs.apply(request).entrySet()) {
			sb.append("	<input name=\"" + input.getKey()
					+ "\" type=\"hidden\" value=\"" + input.getValue() + "\" />\n");
		}
	} // end renderHiddenInputs
	
}	

(5). 总结

在这里没有对UsernamePasswordAuthenticationFilter进行剖析,而是,先剖析了生成登录页面的Filter(DefaultLoginPageGeneratingFilter)进行了剖析.