Spring Security基本认证

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,页面如下:

初识SpringSecurity

​ 默认的登录用户名是user,登陆密码则是一个随机生成的UUID字符串,在工程启动日志中可以看到登陆密码,所以每次启动,登陆密码都会变化:

Using generated security password: abbdbac5-614e-41eb-afee-11ad947c6788

​ 输入默认的用户名和密码,就可以成功登录了。这就是Spring Security的强大之处,只需要引入一个依赖,所有接口就会被自动保护起来。


2.流程分析

​ 通过一个简单的流程图分析一下上面案例中的请求流程:

SpringSecurity登录请求流程图

​ 流程图清晰地说明了整个请求过程:

  • 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()方法:放回当前账户是否可用。

​ 这是用户对象的定义,而负责提供用户数据源的接口是UserDetailsServiceUserDetailsService中只有一个查询用户的方法,接口定义如下:

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;
    }
}

​ 有两个比较重要的条件促使系统自动装配一个InMemoryUserDetailsManagerBean实例:

  • 当前classpath下存在AuthenticationManager类。
  • 当前工程中,系统没有提供AuthenticationManagerAuthenticationProviderUserDetailsService以及ClientRegistrationRepository实例。

​ 默认情况下,上述条件都会满足。此时Spring Security会提供一个InMemoryUserDetailsManagerBean实例。

​ 从上述源码中可以知道,用户数据来源于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()方法中进行了二次处理。由于默认的encodernull,所以密码的二次处理只是给密码加了一个前缀{noop},表示密码是明文存储的。

5.默认页面生成

​ 在上面介绍的Spring Security基本认证中,一共存在两个默认页面。一个是默认的登陆页面,另外一个则是注销登陆页面。当用户登陆成功之后,在浏览器中访问http://localhost:9096/logout就可以访问到默认的注销页面。

logout页面

​ 这些默认页面是如何生成的,这里来具体说一下。

​ 在Spring Security中存在一些常见的过滤器,在这些常见的过滤器中就包含两个和页面相关的过滤器,分别是:DefaultLoginPageGeneratingFilterDefaultLogoutPageGeneratingFilter

​ 通过语义化命名就可以识别出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请求,则执行下一个过滤器。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值