Spring Security
认证
对于所有安全管理框架而言,认证功能可以说是一切的起点。在Spring Security
中,对认证功能做了大量的封装,以至于开发者只需要稍微配置一下就能使用认证功能。
1.Spring Security
基本认证
在Spring Boot
项目中使用Spring Security
非常方便,只需引入如下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
</parent>
<groupId>com.kapcb.ccc</groupId>
<artifactId>springsecurity-helloworld</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>
在工程中提供一个用于测试的接口/hello
:
package com.kapcb.ccc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <a>Title: Controller </a>
* <a>Author: Kapcb <a>
* <a>Description: Controller <a>
*
* @author Kapcb
* @version 1.0
* @date 2022/4/23 14:15
* @since 1.0
*/
@RestController
@RequestMapping("/")
public class Controller {
@GetMapping("hello")
public String hello() {
return "Hello, Spring Security!";
}
}
启动工程,/hello
接口就会被自动保护起来。当用户访问/hello
接口时,会自动跳转到登录页面,用户登陆成功后,才能访问到/hello
接口。
访问http://localhost:9096/hello
会自动重定向到http://localhost:9096/login
,页面如下:
默认的登录用户名是user
,登陆密码则是一个随机生成的UUID
字符串,在工程启动日志中可以看到登陆密码,所以每次启动,登陆密码都会变化:
Using generated security password: abbdbac5-614e-41eb-afee-11ad947c6788
输入默认的用户名和密码,就可以成功登录了。这就是Spring Security
的强大之处,只需要引入一个依赖,所有接口就会被自动保护起来。
2.流程分析
通过一个简单的流程图分析一下上面案例中的请求流程:
流程图清晰地说明了整个请求过程:
- 1、客户端发起请求访问受保护接口
/hello
,这个接口默认是需要认证之后才能访问。 - 2、这个请求会走一遍
Spring Security
中的SecurityFilterChain
过滤链,在最后的FilterSecurityInterceptor
过滤器中被拦截下来,因为系统发现用户未认证。请求拦截下来之后,接下来会抛出AccessDeniedException
异常。 - 3、抛出的
AccessDeniedException
异常在ExceptionTranslationFilter
过滤器中被捕获,ExceptionTranslationFilter
过滤器通过调用LoginUrlAuthenticationEntryPoint#commence()
方法给客户端返回302
,要求客户端重定向到/login
页面。 - 4、客户端发起
/login
请求。 - 5、
/login
请求被DefaultLoginPageGeneratingFilter
过滤器拦截下来,并在该过滤器中返回登陆页面。所以当用户直接访问/login
接口时会先看到登陆页面。
在整个过程中,相当于客户端一共发送了两个请求,第一个是/hello
,服务端接收到请求之后,返回302
要求客户端重定向到/login
,于是客户端又发送了/login
请求。
3.原理分析
虽然开发者只是引入了一个依赖,但是Spring Boot
背后却默默做了很多事情:
- 开启
Spring Security
自动化配置。开启后,会自动创建一个名为SpringSecurityFilterChain
的过滤器,并注入到Spring
容器中,这个过滤器将负责所有的安全管理,包括用户的认证、授权、重定向到登录页面等。SpringSecurityFilterChain
实际上代理了Spring Security
中的过滤器链。 - 创建一个
UserDetailService
实例,UserDetailService
负责提供用户数据,默认的用户数据是基于内存的用户,用户名为user
,密码为工程启动时随机生成的UUID
字符串。 - 为用户生成一个默认的登陆页面。
- 开启
CSRF
攻击防御。 - 开启会话固定攻击防御。
- 集成
X-XSS-Protection
。 - 集成
X-Frame-Options
以防止点击劫持。
4.默认用户生成
Spring Security
中定义了UserDetails
接口来规范开发者自定义的用户对象,这样方便一些旧系统、用户表已近固定的系统集成到Spring Security
认证体系中。
UserDetails
接口定义如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
接口中一共定义了7
个方法:
getAuthorities()
方法:返回当前账户所具备的权限。getPassword()
方法:返回当前账户的密码。getUsername()
方法:放回当前账户的用户名。isAccountNonExpired()
方法:返回当前账户是否未过期。isAccountNonLocked()
方法:返回当前账户是否未锁定。isCredentialsNonExpired()
方法:放回当前账户凭证(如密码)是否未过期。isEnabled()
方法:放回当前账户是否可用。
这是用户对象的定义,而负责提供用户数据源的接口是UserDetailsService
,UserDetailsService
中只有一个查询用户的方法,接口定义如下:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
loadUserByUsername
方法有一个参数是username
,这是用户在认证是传入的用户名,最常见的就是用户在登录表单中输入的用户名,开发者拿到用户名之后,再去数据库中查询用户信息,最终返回一个UserDetails
实例。
在实际开发中,一般需要开发者自定义UserDetailsService
的实现。如果开发者没有自定义UserDetailsService
实现,Spring Security
也为UserDetailsService
提供了默认实现:
UserDetailsManager
:在UserDetailsService
的基础上,继续定义了添加用户、更新用户、删除用户、修改密码以及判断用户是否存在共5
种方法。JdbcDaoImpl
:在UserDetailsService
的基础上,通过spring-jdbc
实现了从数据库种查询用户的方法。InMemoryUserDetailsManager
:实现了UserDetailsManager
中关于用户的增删改查方法,不过都是基于内存的操作,数据并没有持久化。JdbcUserDetailsManager
:继承自JdbcDaoImpl
同时又实现了UserDetailsManager
接口,因此可以通过JdbcUserDetailsManager
实现对用户的增删改查操作,这些操作都会持久化到数据库中。不过JdbcUserDetailsManager
有一个局限性,就是操作数据库中用户的SQL
语句多是提前写好的,不够灵活。因此在实际开发中JdbcUserDetailsManager
使用并不多。CachingUserDetailsService
:特点是会将UserDetailsService
缓存起来。UserDetailsServiceDelegator
:提供了UserDetailsService
的懒加载功能。ReactiveUserDetailServiceAdapter
:是webflux-web-security
模块定义的UserDetailsService
实现。
但使用Spring Security
时,如果仅仅只是引入一个Spring Security
依赖,则默认使用的用户就是由InMemoryUserDetailsManager
提供的。
Spring Boot
之所以能够做到零配置使用Spring Security
,就是因为它提供了众多的自动配置类。其中,针对UserDetailsService
的自动化配置类就是UserDetailsServiceAutoConfiguration
,其源码如下:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({AuthenticationManager.class})
@ConditionalOnBean({ObjectPostProcessor.class})
@ConditionalOnMissingBean(
value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class},
type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector"}
)
public class UserDetailsServiceAutoConfiguration {
private static final String NOOP_PASSWORD_PREFIX = "{noop}";
private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");
private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);
public UserDetailsServiceAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
type = {"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"}
)
@Lazy
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider<PasswordEncoder> passwordEncoder) {
User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()});
}
private String getOrDeducePassword(User user, PasswordEncoder encoder) {
String password = user.getPassword();
if (user.isPasswordGenerated()) {
logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
}
return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
}
}
有两个比较重要的条件促使系统自动装配一个InMemoryUserDetailsManager
的Bean
实例:
- 当前
classpath
下存在AuthenticationManager
类。 - 当前工程中,系统没有提供
AuthenticationManager
、AuthenticationProvider
、UserDetailsService
以及ClientRegistrationRepository
实例。
默认情况下,上述条件都会满足。此时Spring Security
会提供一个InMemoryUserDetailsManager
的Bean
实例。
从上述源码中可以知道,用户数据来源于SecurityProperties#getUser()
方法:
@ConfigurationProperties(
prefix = "spring.security"
)
public class SecurityProperties {
public static final int BASIC_AUTH_ORDER = 2147483642;
public static final int IGNORED_ORDER = -2147483648;
public static final int DEFAULT_FILTER_ORDER = -100;
private final SecurityProperties.Filter filter = new SecurityProperties.Filter();
private SecurityProperties.User user = new SecurityProperties.User();
public SecurityProperties() {
}
public SecurityProperties.User getUser() {
return this.user;
}
public SecurityProperties.Filter getFilter() {
return this.filter;
}
public static class User {
private String name = "user";
private String password = UUID.randomUUID().toString();
private List<String> roles = new ArrayList();
private boolean passwordGenerated = true;
public User() {
}
// getter setter
}
public static class Filter {
private int order = -100;
private Set<DispatcherType> dispatcherTypes;
public Filter() {
this.dispatcherTypes = new HashSet(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
}
// getter setter
}
}
从SpringSecurity.User
类中,可以看到默认用户名为user
,默认的密码是一个UUID
字符串。
在UserDetailsServiceAutoConfiguration
类中,在构造InMemoryUserDetailsManager
实例时需要构造一个User
对象。而这个User
对象并不是SpringSecurity.User
对象,而是org.springframework.security.core.userdetails.User
对象,org.springframework.security.core.userdetails.User
类实现了UserDetails
接口。该类提供了相应的静态方法,用于构造一个User
实例。同时,默认的密码还在getOrDeducePassword()
方法中进行了二次处理。由于默认的encoder
为null
,所以密码的二次处理只是给密码加了一个前缀{noop}
,表示密码是明文存储的。
5.默认页面生成
在上面介绍的Spring Security
基本认证中,一共存在两个默认页面。一个是默认的登陆页面,另外一个则是注销登陆页面。当用户登陆成功之后,在浏览器中访问http://localhost:9096/logout
就可以访问到默认的注销页面。
这些默认页面是如何生成的,这里来具体说一下。
在Spring Security
中存在一些常见的过滤器,在这些常见的过滤器中就包含两个和页面相关的过滤器,分别是:DefaultLoginPageGeneratingFilter
和DefaultLogoutPageGeneratingFilter
。
通过语义化命名就可以识别出DefaultLoginPageGeneratingFilter
过滤器是用来生成默认的登陆页面的,DefaultLogoutPageGeneratingFilter
过滤器是用来生成默认的注销页面的。
先看看DefaultLoginPageGeneratingFilter
。作为Spring Security
过滤链中的一员,在第一次访问/hello
接口的时候,就会经过DefaultLoginPageGeneratingFilter
过滤器,但是由于/hello
接口与登陆无关,因此DefaultLoginPageGeneratingFilter
过滤器并未干涉/hello
接口。等到第二次重定向到/login
页面的时候,这个时候就和DefaultLoginPageGeneratingFilter
有关系了,此时用户访问/login
接口时,就会在DefaultLoginPageGeneratingFilter
中进行处理,生成默认的登录页面返回给客户端。
看看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 boolean saml2LoginEnabled;
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 Map<String, String> saml2AuthenticationUrlToProviderName;
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> {
return Collections.emptyMap();
};
public DefaultLoginPageGeneratingFilter() {
}
public DefaultLoginPageGeneratingFilter(AbstractAuthenticationProcessingFilter filter) {
if (filter instanceof UsernamePasswordAuthenticationFilter) {
this.init((UsernamePasswordAuthenticationFilter)filter, (AbstractAuthenticationProcessingFilter)null);
} else {
this.init((UsernamePasswordAuthenticationFilter)null, filter);
}
}
public DefaultLoginPageGeneratingFilter(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) {
this.init(authFilter, openIDFilter);
}
private void init(UsernamePasswordAuthenticationFilter authFilter, AbstractAuthenticationProcessingFilter openIDFilter) {
this.loginPageUrl = "/login";
this.logoutSuccessUrl = "/login?logout";
this.failureUrl = "/login?error";
if (authFilter != null) {
this.initAuthFilter(authFilter);
}
if (openIDFilter != null) {
this.initOpenIdFilter(openIDFilter);
}
}
private void initAuthFilter(UsernamePasswordAuthenticationFilter authFilter) {
this.formLoginEnabled = true;
this.usernameParameter = authFilter.getUsernameParameter();
this.passwordParameter = authFilter.getPasswordParameter();
if (authFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
this.rememberMeParameter = ((AbstractRememberMeServices)authFilter.getRememberMeServices()).getParameter();
}
}
private void initOpenIdFilter(AbstractAuthenticationProcessingFilter openIDFilter) {
this.openIdEnabled = true;
this.openIDusernameParameter = "openid_identifier";
if (openIDFilter.getRememberMeServices() instanceof AbstractRememberMeServices) {
this.openIDrememberMeParameter = ((AbstractRememberMeServices)openIDFilter.getRememberMeServices()).getParameter();
}
}
public void setResolveHiddenInputs(Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) {
Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null");
this.resolveHiddenInputs = resolveHiddenInputs;
}
public boolean isEnabled() {
return this.formLoginEnabled || this.openIdEnabled || this.oauth2LoginEnabled || this.saml2LoginEnabled;
}
public void setLogoutSuccessUrl(String logoutSuccessUrl) {
this.logoutSuccessUrl = logoutSuccessUrl;
}
public String getLoginPageUrl() {
return this.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 setOauth2LoginEnabled(boolean oauth2LoginEnabled) {
this.oauth2LoginEnabled = oauth2LoginEnabled;
}
public void setSaml2LoginEnabled(boolean saml2LoginEnabled) {
this.saml2LoginEnabled = saml2LoginEnabled;
}
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 setOauth2AuthenticationUrlToClientName(Map<String, String> oauth2AuthenticationUrlToClientName) {
this.oauth2AuthenticationUrlToClientName = oauth2AuthenticationUrlToClientName;
}
public void setSaml2AuthenticationUrlToProviderName(Map<String, String> saml2AuthenticationUrlToProviderName) {
this.saml2AuthenticationUrlToProviderName = saml2AuthenticationUrlToProviderName;
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.doFilter((HttpServletRequest)request, (HttpServletResponse)response, chain);
}
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean loginError = this.isErrorPage(request);
boolean logoutSuccess = this.isLogoutSuccess(request);
if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
chain.doFilter(request, response);
} else {
String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
response.setContentType("text/html;charset=UTF-8");
response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
response.getWriter().write(loginPageHtml);
}
}
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("SPRING_SECURITY_LAST_EXCEPTION");
errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
}
}
String contextPath = request.getContextPath();
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n");
sb.append("<html lang=\"en\">\n");
sb.append(" <head>\n");
sb.append(" <meta charset=\"utf-8\">\n");
sb.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
sb.append(" <meta name=\"description\" content=\"\">\n");
sb.append(" <meta name=\"author\" content=\"\">\n");
sb.append(" <title>Please sign in</title>\n");
sb.append(" <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");
sb.append(" <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
sb.append(" </head>\n");
sb.append(" <body>\n");
sb.append(" <div class=\"container\">\n");
if (this.formLoginEnabled) {
sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n");
sb.append(" <h2 class=\"form-signin-heading\">Please sign in</h2>\n");
sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n");
sb.append(" <label for=\"username\" class=\"sr-only\">Username</label>\n");
sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
sb.append(" </p>\n");
sb.append(" <p>\n");
sb.append(" <label for=\"password\" class=\"sr-only\">Password</label>\n");
sb.append(" <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n");
sb.append(" </p>\n");
sb.append(this.createRememberMe(this.rememberMeParameter) + this.renderHiddenInputs(request));
sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
sb.append(" </form>\n");
}
if (this.openIdEnabled) {
sb.append(" <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n");
sb.append(" <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n");
sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + " <p>\n");
sb.append(" <label for=\"username\" class=\"sr-only\">Identity</label>\n");
sb.append(" <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n");
sb.append(" </p>\n");
sb.append(this.createRememberMe(this.openIDrememberMeParameter) + this.renderHiddenInputs(request));
sb.append(" <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n");
sb.append(" </form>\n");
}
Iterator var7;
Map.Entry relyingPartyUrlToName;
String url;
String partyName;
if (this.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");
var7 = this.oauth2AuthenticationUrlToClientName.entrySet().iterator();
while(var7.hasNext()) {
relyingPartyUrlToName = (Map.Entry)var7.next();
sb.append(" <tr><td>");
url = (String)relyingPartyUrlToName.getKey();
sb.append("<a href=\"").append(contextPath).append(url).append("\">");
partyName = HtmlUtils.htmlEscape((String)relyingPartyUrlToName.getValue());
sb.append(partyName);
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");
var7 = this.saml2AuthenticationUrlToProviderName.entrySet().iterator();
while(var7.hasNext()) {
relyingPartyUrlToName = (Map.Entry)var7.next();
sb.append(" <tr><td>");
url = (String)relyingPartyUrlToName.getKey();
sb.append("<a href=\"").append(contextPath).append(url).append("\">");
partyName = HtmlUtils.htmlEscape((String)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();
}
private String renderHiddenInputs(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
Iterator var3 = ((Map)this.resolveHiddenInputs.apply(request)).entrySet().iterator();
while(var3.hasNext()) {
Map.Entry<String, String> input = (Map.Entry)var3.next();
sb.append("<input name=\"");
sb.append((String)input.getKey());
sb.append("\" type=\"hidden\" value=\"");
sb.append((String)input.getValue());
sb.append("\" />\n");
}
return sb.toString();
}
private String createRememberMe(String paramName) {
return paramName == null ? "" : "<p><input type='checkbox' name='" + paramName + "'/> Remember me on this computer.</p>\n";
}
private boolean isLogoutSuccess(HttpServletRequest request) {
return this.logoutSuccessUrl != null && this.matches(request, this.logoutSuccessUrl);
}
private boolean isLoginUrlRequest(HttpServletRequest request) {
return this.matches(request, this.loginPageUrl);
}
private boolean isErrorPage(HttpServletRequest request) {
return this.matches(request, this.failureUrl);
}
private static String createError(boolean isError, String message) {
return !isError ? "" : "<div class=\"alert alert-danger\" role=\"alert\">" + HtmlUtils.htmlEscape(message) + "</div>";
}
private static String createLogoutSuccess(boolean isLogoutSuccess) {
return !isLogoutSuccess ? "" : "<div class=\"alert alert-success\" role=\"alert\">You have been signed out</div>";
}
private boolean matches(HttpServletRequest request, String url) {
if ("GET".equals(request.getMethod()) && url != null) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(59);
if (pathParamIndex > 0) {
uri = uri.substring(0, pathParamIndex);
}
if (request.getQueryString() != null) {
uri = uri + "?" + request.getQueryString();
}
return "".equals(request.getContextPath()) ? uri.equals(url) : uri.equals(request.getContextPath() + url);
} else {
return false;
}
}
}
DefaultLoginPageGeneratingFilter
源码中的逻辑非常清晰,梳理一下:
- 在
doFilter
方法中,首先判断出当前请求是否为登录出错请求、注销成功请求或者登录请求。如果是这三个种请求种的任意一个,就会在DefaultLoginPageGeneratingFilter
过滤器中生成登录页面并返回,否则请求继续往下走,执行下一个过滤器。这就是刚刚访问/hello
接口为什么没有被DefaultLoginPageGeneratingFilter
拦截下的原因。 - 如果当前请求是登出错误请求、注销成功请求或者登录请求中的任意一个,就会调用
generateLoginPageHtml()
方法生成登陆页面。在该方法中,如果有异常信息就把异常信息取出来一同返回给前端,然后根据不同的登陆场景,生成不同的登陆页面。 - 登陆页面生成后,接下来通过
HttpServletResponse
将登陆页面写回到前端,然后调用return;
方法跳出过滤器。
理解了DefaultLoginPageGeneratingFilter
过滤器的实现原理,再来看DefaultLogoutPageGeneratingFilter
就很好理解了,DefaultLogoutPageGeneratingFilter
源码如下:
public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter {
private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET");
private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = (request) -> {
return Collections.emptyMap();
};
public DefaultLogoutPageGeneratingFilter() {
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (this.matcher.matches(request)) {
this.renderLogout(request, response);
} else {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Did not render default logout page since request did not match [%s]", this.matcher));
}
filterChain.doFilter(request, response);
}
}
private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html>\n");
sb.append("<html lang=\"en\">\n");
sb.append(" <head>\n");
sb.append(" <meta charset=\"utf-8\">\n");
sb.append(" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n");
sb.append(" <meta name=\"description\" content=\"\">\n");
sb.append(" <meta name=\"author\" content=\"\">\n");
sb.append(" <title>Confirm Log Out?</title>\n");
sb.append(" <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");
sb.append(" <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n");
sb.append(" </head>\n");
sb.append(" <body>\n");
sb.append(" <div class=\"container\">\n");
sb.append(" <form class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n");
sb.append(" <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n");
sb.append(this.renderHiddenInputs(request) + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n");
sb.append(" </form>\n");
sb.append(" </div>\n");
sb.append(" </body>\n");
sb.append("</html>");
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write(sb.toString());
}
public void setResolveHiddenInputs(Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) {
Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null");
this.resolveHiddenInputs = resolveHiddenInputs;
}
private String renderHiddenInputs(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
Iterator var3 = ((Map)this.resolveHiddenInputs.apply(request)).entrySet().iterator();
while(var3.hasNext()) {
Map.Entry<String, String> input = (Map.Entry)var3.next();
sb.append("<input name=\"");
sb.append((String)input.getKey());
sb.append("\" type=\"hidden\" value=\"");
sb.append((String)input.getValue());
sb.append("\" />\n");
}
return sb.toString();
}
}
- 首先判断请求是否是注销登录请求
/logout
,如果是/logout
请求,则渲染一个注销请求的页面返回给客户端,渲染过程和前端登陆页面的渲染过程类似,也是字符串拼接如果不是/logout
请求,则执行下一个过滤器。