版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/andy_zhang2007/article/details/84748206
概述
当开发人员在安全配置中没有配置登录页面时,Spring Security Web
会自动构造一个登录页面给用户。完成这一任务是通过一个过滤器来完成的,该过滤器就是DefaultLoginPageGeneratingFilter
。
该过滤器支持两种登录情景:
- 用户名/密码表单登录
OpenID
表单登录
无论以上哪种登录情景,该过滤器都会使用以下信息用于构建登录HTML页面 :
- 当前请求是否为登录页面请求的匹配器定义–由配置明确指定或者使用缺省值
/login
- 当前请求是否跳转自登录错误处理以及相应异常信息 – 缺省对应url :
/login?error
- 当前请求是否跳转自退出登录成功处理 – 缺省对应url :
/login?logout
- 配置指定使用用户名/密码表单登录还是
OpenID
表单登录 - 表单构建信息
csrf token
信息- 针对用户名/密码表单登录的表单构建信息
- 登录表单提交处理地址
- 用户名表单字段名称
- 密码表单字段名称
RememberMe
表单字段名称
- 针对
OpenID
表单登录的表单构建信息- 登录表单提交处理地址
OpenID
表单字段名称RememberMe
表单字段名称
该过滤器被请求到达时会首先看是不是自己关注的请求,如果是,则会根据相应信息构建一个登录页面HTML
直接写回浏览器端,对该请求的处理也到此结束,不再继续调用filter chain
中的其他逻辑。
生成的用户名/密码表单登录页面效果
由该Filter生成的用户名/密码表单登录页面如下所示:
对应的 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表单登录页面如下所示:
对应的 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);
}
}