SpringSecurity Default Form Login Page Shows Process Source

Posted by badapple on Wed, 22 Jan 2020 03:00:48 +0100

This article focuses on how SpringSecurity provides the default form login page and how it presents the process. Involve 1.FilterSecurityInterceptor, 2.ExceptionTranslationFilter , 3.DefaultLoginPageGeneratingFilter filter, The voting mechanism of AccessDecisionManager is also briefly introduced.

_1. Prepare (experience SpringSecurity default form authentication)

_1.1 Create SpringSecurity Project

_First create a SpringBoot project from IDEA and rely on SpringSecurity, Web Dependency

_At this point, pom.xml will be added automatically

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
</dependency>

_1.2 Provides an interface

@RestController
public class HelloController {


@RequestMapping("/hello")
public String hello() {
    return "Hello SpringSecurity";
  }
}

_1.3 Startup Project

_Direct access to the provided interface

http://localhost:8080/hello

_will find that the browser is redirected directly to/login and displays the following default form login page

http://localhost:8080/login

_1.4 Login

_The console prints a seuciryt password:xxx when starting the project

Using generated security password: f520875f-ea2b-4b5d-9b0c-f30c0c17b90b

_Direct login

User name: user password: f520875f-ea2b-4b5d-9b0c-f30c0c17b90b 

_Login succeeds and the browser redirects to the interface it just visited

_2.springSecurityFilterchain filter chain

_If you've read another of my blogs about SpringSecurity initialization sources, you know that when the SpringSecurity project is started and finished, a springSecurityFilterchain is initialized. Its internal additionalFilters property initializes many filters as follows All requests go through a series of filters, Spring Security, through which authentication is authorized, and so on.

_3.FilterSecurityInterceptor (it will determine if the request passes)

_FilterSecurityInterceptor is the last filter in the filter chain, which is mainly used to determine whether a request can pass or not, and votes internally through AccessDecision Manager

_When we are not logged in to visit

http://localhost:8080/hello

_Request will be intercepted by FilterSecurityInterceptor

public void doFilter(ServletRequest request, ServletResponse response,
		FilterChain chain) throws IOException, ServletException {
	FilterInvocation fi = new FilterInvocation(request, response, chain);
	invoke(fi);
}

_Focus on invoke method

public void invoke(FilterInvocation fi) throws IOException, ServletException {
	if ((fi.getRequest() != null)
			&& (fi.getRequest().getAttribute(FILTER_APPLIED) != null)
			&& observeOncePerRequest) {
		// filter already applied to this request and user wants us to observe
		// once-per-request handling, so don't re-do security checking
		fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
	}
	else {
		// first time this request being called, so perform security checking
		if (fi.getRequest() != null && observeOncePerRequest) {
			fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE);
		}

		InterceptorStatusToken token = super.beforeInvocation(fi);

		try {
			fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
		}
		finally {
			super.finallyInvocation(token);
		}

		super.afterInvocation(token, null);
	}
}

_There is a sentence in the source code that actually determines if the current user can access the specified interface, then fi.getChain().doFilter calls the access interface Otherwise an exception will be thrown inside

InterceptorStatusToken token = super.beforeInvocation(fi);

_The beforeInvocation method makes decisions internally through accessDecision Manager _Spring Security already has several built-in voting-based AccessDecision Managers including (AffirmativeBased, ConsensusBased, UnanimousBased) of course you can also implement your own AccessDecision Manager if you want

_In this way, a series of Accesses Decision nVoters will be used by AccessDecision Manager to vote on whether Authentication has access to protected objects, and then decide whether to throw AccessDeniedException based on the voting results

this.accessDecisionManager.decide(authenticated, object, attributes);

_The implementation of decide for AffirmativeBased is as follows

public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException {
    int deny = 0;
    Iterator var5 = this.getDecisionVoters().iterator();

    while(var5.hasNext()) {
        AccessDecisionVoter voter = (AccessDecisionVoter)var5.next();
        int result = voter.vote(authentication, object, configAttributes);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Voter: " + voter + ", returned: " + result);
        }

        switch(result) {
        case -1:
            ++deny;
            break;
        case 1:
            return;
        }
    }

    if (deny > 0) {
        throw new AccessDeniedException(this.messages.getMessage("AbstractAccessDecisionManager.accessDenied", "Access is denied"));
    } else {
        this.checkAllowIfAllAbstainDecisions();
    }
}

_The logic of AffirmativeBased is as follows:

   (1) Access is granted as long as AccessDecisionVoter votes for ACCESS_GRANTED;

   (2) If all abstentions are also expressed as approval;

   (3) If no one votes in favour but some votes against it, AccessDeniedException will be thrown.

_When we first visited

http://When localhost:8080/hello 

_Returning result = -1 throws an AccessDeniedException deny access exception

_4.ExceptionTranslationFilter (catches AccessDeniedException exceptions)

_This filter receives an AccessDeniedException exception thrown by the FilterSecurityInterceptor) captures it and sends a redirect/login request

_The source code is as follows:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
		throws IOException, ServletException {
	HttpServletRequest request = (HttpServletRequest) req;
	HttpServletResponse response = (HttpServletResponse) res;

	try {
		chain.doFilter(request, response);

		logger.debug("Chain processed normally");
	}
	catch (IOException ex) {
		throw ex;
	}
	catch (Exception ex) {
		// Try to extract a SpringSecurityException from the stacktrace
		Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
		RuntimeException ase = (AuthenticationException) throwableAnalyzer
				.getFirstThrowableOfType(AuthenticationException.class, causeChain);

		if (ase == null) {
			ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(
					AccessDeniedException.class, causeChain);
		}

		if (ase != null) {
			if (response.isCommitted()) {
				throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex);
			}
			handleSpringSecurityException(request, response, chain, ase);
		}
		else {
			// Rethrow ServletExceptions and RuntimeExceptions as-is
			if (ex instanceof ServletException) {
				throw (ServletException) ex;
			}
			else if (ex instanceof RuntimeException) {
				throw (RuntimeException) ex;
			}

			// Wrap other Exceptions. This shouldn't actually happen
			// as we've already covered all the possibilities for doFilter
			throw new RuntimeException(ex);
		}
	}
}

_Called when an exception is obtained

handleSpringSecurityException(request, response, chain, ase);

_handleSpringSecurityException source code is as follows:

private void handleSpringSecurityException(HttpServletRequest request,
		HttpServletResponse response, FilterChain chain, RuntimeException exception)
		throws IOException, ServletException {
	if (exception instanceof AuthenticationException) {
		logger.debug(
				"Authentication exception occurred; redirecting to authentication entry point",
				exception);

		sendStartAuthentication(request, response, chain,
				(AuthenticationException) exception);
	}
	else if (exception instanceof AccessDeniedException) {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
			logger.debug(
					"Access is denied (user is " + (authenticationTrustResolver.isAnonymous(authentication) ? "anonymous" : "not fully authenticated") + "); redirecting to authentication entry point",
					exception);

			sendStartAuthentication(
					request,
					response,
					chain,
					new InsufficientAuthenticationException(
						messages.getMessage(
							"ExceptionTranslationFilter.insufficientAuthentication",
							"Full authentication is required to access this resource")));
		}
		else {
			logger.debug(
					"Access is denied (user is not anonymous); delegating to AccessDeniedHandler",
					exception);

			accessDeniedHandler.handle(request, response,
					(AccessDeniedException) exception);
		}
	}
}

_Determine if the exception retrieved is AccessDeniedException before deciding whether it is an anonymous user or if it is, call sendStartAuthentication to redirect to the login page

_The path currently accessed is saved before redirecting the login page, which is why we access the / hello interface and then jump to the / hello interface after successful login because requestCache.saveRequest(request, response) is saved here before redirecting to the / login interface;

protected void sendStartAuthentication(HttpServletRequest request,
		HttpServletResponse response, FilterChain chain,
		AuthenticationException reason) throws ServletException, IOException {
	// SEC-112: Clear the SecurityContextHolder's Authentication, as the
	// existing Authentication is no longer considered valid
	SecurityContextHolder.getContext().setAuthentication(null);
	requestCache.saveRequest(request, response);
	logger.debug("Calling Authentication entry point.");
	authenticationEntryPoint.commence(request, response, reason);
}

_authenticationEntryPoint.commence(request, response, reason); method internal

_Call commence method of LoginUrlAuthenticationEntryPoint

_LoginUrlAuthenticationEntryPoint's commence method has a method to construct a redirect URL inside it

redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);



protected String buildRedirectUrlToLoginPage(HttpServletRequest request,
		HttpServletResponse response, AuthenticationException authException) {

	String loginForm = determineUrlToUseForThisRequest(request, response,
			authException);

protected String determineUrlToUseForThisRequest(HttpServletRequest request,
		HttpServletResponse response, AuthenticationException exception) {

	return getLoginFormUrl();
}

_Eventually you get the URL/login that needs to be redirected

_sendRedirect then redirects both to/login requests

_5.DefaultLoginPageGeneratingFilter (captures redirected/login requests)

_DefaultLoginPageGeneratingFilter is one of the filter chains used to capture/login requests and render a default form page

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

_isLoginUrlRequest determines if the request is a loginPageUrl

private boolean isLoginUrlRequest(HttpServletRequest request) {
	return matches(request, loginPageUrl);
}

_Default loginPageUrl =/login because we have no configuration

_Verify that the loginPageUrl can be matched by the request path

String loginPageHtml = generateLoginPageHtml(request, loginError,
				logoutSuccess);

_GeneeLoginPageHtml draws the default HTML page, so here's where our default login page comes in

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

	StringBuilder sb = new StringBuilder();

	sb.append("<!DOCTYPE html>\n"
			+ "<html lang=\"en\">\n"
			+ "  <head>\n"
			+ "    <meta charset=\"utf-8\">\n"
			+ "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n"
			+ "    <meta name=\"description\" content=\"\">\n"
			+ "    <meta name=\"author\" content=\"\">\n"
			+ "    <title>Please sign in</title>\n"
			+ "    <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"
			+ "    <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n"
			+ "  </head>\n"
			+ "  <body>\n"
			+ "     <div class=\"container\">\n");

	String contextPath = request.getContextPath();
	if (this.formLoginEnabled) {
		sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"
				+ "        <h2 class=\"form-signin-heading\">Please sign in</h2>\n"
				+ createError(loginError, errorMsg)
				+ createLogoutSuccess(logoutSuccess)
				+ "        <p>\n"
				+ "          <label for=\"username\" class=\"sr-only\">Username</label>\n"
				+ "          <input type=\"text\" id=\"username\" name=\"" + this.usernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
				+ "        </p>\n"
				+ "        <p>\n"
				+ "          <label for=\"password\" class=\"sr-only\">Password</label>\n"
				+ "          <input type=\"password\" id=\"password\" name=\"" + this.passwordParameter + "\" class=\"form-control\" placeholder=\"Password\" required>\n"
				+ "        </p>\n"
				+ createRememberMe(this.rememberMeParameter)
				+ renderHiddenInputs(request)
				+ "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
				+ "      </form>\n");
	}

	if (openIdEnabled) {
		sb.append("      <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"
				+ "        <h2 class=\"form-signin-heading\">Login with OpenID Identity</h2>\n"
				+ createError(loginError, errorMsg)
				+ createLogoutSuccess(logoutSuccess)
				+ "        <p>\n"
				+ "          <label for=\"username\" class=\"sr-only\">Identity</label>\n"
				+ "          <input type=\"text\" id=\"username\" name=\"" + this.openIDusernameParameter + "\" class=\"form-control\" placeholder=\"Username\" required autofocus>\n"
				+ "        </p>\n"
				+ createRememberMe(this.openIDrememberMeParameter)
				+ renderHiddenInputs(request)
				+ "        <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Sign in</button>\n"
				+ "      </form>\n");
	}

	if (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");
		for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
			sb.append(" <tr><td>");
			String url = clientAuthenticationUrlToClientName.getKey();
			sb.append("<a href=\"").append(contextPath).append(url).append("\">");
			String clientName = HtmlUtils.htmlEscape(clientAuthenticationUrlToClientName.getValue());
			sb.append(clientName);
			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");
		for (Map.Entry<String, String> relyingPartyUrlToName : saml2AuthenticationUrlToProviderName.entrySet()) {
			sb.append(" <tr><td>");
			String url = relyingPartyUrlToName.getKey();
			sb.append("<a href=\"").append(contextPath).append(url).append("\">");
			String partyName = HtmlUtils.htmlEscape(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();
}

The SpringSecurity default form login page now shows the source code of the process. The following page will be rendered, but there must be a network or the style may change

6. Summary

This article mainly explains how SpringSecurity provides the default form login page and how it presents the process, including three filters related to this process 1.FilterSecurityInterceptor, 2.ExceptionTranslationFilter , 3.DefaultLoginPageGeneratingFilter filter, It also gives a brief introduction to AccessDecision Manager, which votes primarily to determine whether the user has access to the appropriate resources. AccessDecision Manager voting mechanism I don't go into it either. I'll go into it a little more

Personal blogging system: https://www.askajohnny.com Welcome! This article is published by blog OpenWrite Release!

Topics: Programming Spring Session SpringBoot xml