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!