spring|spring security之 默认登录页源码跟踪

spring security之 默认登录页源码跟踪 ? 2021年的最后2个月,立个flag,要把Spring Security和Spring Security OAuth2的应用及主流程源码研究透彻!
? 项目中使用过Spring Security的童鞋都知道,当我们没有单独自定义登录页时,Spring Security自己在初始化的时候会帮我们配置一个默认的登录页,之前一直疑问默认登录页是怎么配置的,今晚特地找了源码跟一下。
springboot项目依赖

org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security

在项目中随意编写一个接口,然后进行访问
@GetMapping("/") public String hello() { return "hello, spring security"; }

在tomcat默认端口8080,localhost:8080 下访问该接口,spring security会帮我们将路径重定向到默认的登录页
spring|spring security之 默认登录页源码跟踪
文章图片

? 那么这个默认页是怎么来的呢?
原来Spring Security有一个默认的WebSecurityConfigurerAdapter,发现其中有一个init方法,于是在这个方法打了断点,在应用启动的时候进行跟踪。
? 跟踪getHttp()方法,this.disableDefaults变量默认为false,意味着将会执行applyDefaultConfiguration(this.http); 方法。查看applyDefaultConfiguration方法
public void init(WebSecurity web) throws Exception { // 首先配置security要拦截的哪些http请求 HttpSecurity http = getHttp(); web.addSecurityFilterChainBuilder(http).postBuildAction(() -> { FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); }); }protected final HttpSecurity getHttp() throws Exception { if (this.http != null) { return this.http; } AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher(); this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); AuthenticationManager authenticationManager = authenticationManager(); this.authenticationBuilder.parentAuthenticationManager(authenticationManager); Map, Object> sharedObjects = createSharedObjects(); this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects); if (!this.disableDefaults) { // 默认的配置将会走这个分支 applyDefaultConfiguration(this.http); ClassLoader classLoader = this.context.getClassLoader(); List defaultHttpConfigurers = SpringFactoriesLoader .loadFactories(AbstractHttpConfigurer.class, classLoader); for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) { this.http.apply(configurer); } } configure(this.http); return this.http; }

查看applyDefaultConfiguration(this.http)方法,发现http对象new了一个DefaultLoginPageConfigurer对象属性,
private void applyDefaultConfiguration(HttpSecurity http) throws Exception { http.csrf(); http.addFilter(new WebAsyncManagerIntegrationFilter()); http.exceptionHandling(); http.headers(); http.sessionManagement(); http.securityContext(); http.requestCache(); http.anonymous(); http.servletApi(); http.apply(new DefaultLoginPageConfigurer<>()); http.logout(); }

? 查看DefaultLoginPageConfigurer类定义,发现它在初始化的同时,它也初始化了自己的2个私有成员变量,分别是DefaultLoginPageGeneratingFilter默认登录页面生成Filter,DefaultLogoutPageGeneratingFilter默认登录页面Filter, 名字起得很好,见名知意,我们马山知道这2个类的含义。
? 查看DefaultLoginPageGeneratingFilter的类成员变量,发现定义了一系列跟登录有关的成员变量,包括登录、登录等路径,默认的登录页面路径是"/login"
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; .....

? 再结合类名思考,发现是个Filter类,那么它们应该都会重新Filter的doFilter(ServletRequest request, ServletResponse response, FilterChain chain)方法,我们查看一下DefaultLoginPageConfigurer类的``doFilter方法,果然,在doFilter`方法中发现了生成默认登录页面的方法。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // 判断当前的请求是否被认证通过 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); }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(WebAttributes.AUTHENTICATION_EXCEPTION); errorMsg = (ex != null) ? ex.getMessage() : "Invalid credentials"; } } String contextPath = request.getContextPath(); StringBuilder sb = new StringBuilder(); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("Please sign in - 锐客网\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); if (this.formLoginEnabled) { sb.append("
\n"); sb.append("Please sign in\n"); sb.append(createError(loginError, errorMsg) + createLogoutSuccess(logoutSuccess) + "\n"); sb.append("\n"); sb.append("\n"); sb.append("
\n"); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append("
\n"); sb.append(createRememberMe(this.rememberMeParameter) + renderHiddenInputs(request)); sb.append("\n"); sb.append("
\n"); } if (this.openIdEnabled) { sb.append("
\n"); sb.append("Login with OpenID Identity\n"); ...... return sb.toString(); }

? 我们发现generateLoginPageHtml(HttpServletRequest request, boolean loginError, boolean logoutSuccess)这个方法中使用了最原始的Servlet写html页面的方法,将登录页的html代码写到字符串中写出到前端展示。到这里,我们就大体知道默认登录页面及登出页面是怎么生成的了。
【spring|spring security之 默认登录页源码跟踪】? 默认登录页到这里就结束了,有兴趣的可以关注下,接下来会继续写springsecurity的自定义表单认证、授权、会话等内容剖析。距离2022年只剩54天!

    推荐阅读