Simply study the back-end processing logic for the request to log in.
1. Preconditions
Continue to access the / test interface. According to the following configuration, the request will be intercepted by the authc filter.
/** * Path - > filter name 1 [parameter 1, parameter 2, parameter 3...], filter name 2 [parameter 1, parameter 2...] * Custom configuration (path in front, specific filter name and parameters in the back, multiple filter parameters are separated by commas, and multiple filter parameters are also separated by commas)) * Some filters do not require parameters, such as anon, authc and shiro. When parsing, an array is parsed as [name, null] by default */ FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // Test address FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[system administrator,User administrator],perms['user:manager:*']"); FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // All resources need to be validated
Prerequisite review
one org.apache.shiro.spring.web.ShiroFilterFactoryBean.SpringShiroFilter is registered in Spring. By default, all requests are intercepted.
2. /test requests to enter the interceptor.
3. Call org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter
4. Call org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal.
5. Call org.apache.shiro.web.servlet.AbstractShiroFilter#executeChain
6. Call org.apache.shiro.web.servlet.AbstractShiroFilter#getExecutionChain to generate proxy FilterChain (logic is to regularly match the path configured by yourself according to the requested URI. If the conditions are met, get the NameFilterList corresponding to the path, and then generate proxiedfilterchain (the proxy object contains NameFilterList and the original faapplicationfilterchain)
7. Call the doFilter method of proxy FilterChain. The method of the proxy object will first go through the doFilter of NameFilterList, and then go through the doFilter method of ApplicationFilterChain.
two org.apache.shiro.web.filter.authc.FormAuthenticationFilter principle
The agent FilterChain obtained according to the path above is as follows:
1. ProxiedFilterChai.doFilter method will call formauthenticationfilter. Dofilter method. This method is the parent method: org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName(); if (request.getAttribute(alreadyFilteredAttributeName) != null) { log.trace("Filter '{}' already executed. Proceeding without invoking this filter.", this.getName()); filterChain.doFilter(request, response); } else if (this.isEnabled(request, response) && !this.shouldNotFilter(request)) { log.trace("Filter '{}' not yet executed. Executing now.", this.getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { this.doFilterInternal(request, response, filterChain); } finally { request.removeAttribute(alreadyFilteredAttributeName); } } else { log.debug("Filter '{}' is not enabled for the current request. Proceeding without invoking this filter.", this.getName()); filterChain.doFilter(request, response); } }
2. Continue to call org.apache.shiro.web.servlet.AdviceFilter#doFilterInternal
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException { Exception exception = null; try { boolean continueChain = this.preHandle(request, response); if (log.isTraceEnabled()) { log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]"); } if (continueChain) { this.executeChain(request, response, chain); } this.postHandle(request, response); if (log.isTraceEnabled()) { log.trace("Successfully invoked postHandle method"); } } catch (Exception var9) { exception = var9; } finally { this.cleanup(request, response, exception); } }
There are three steps:
Step 1: this.preHandle(request, response); preprocessing
Step 2: if the preprocessing returns true, call this.executeChain(request, response, chain); the chain continues to execute
Step 3: this.postHandle(request, response); post processing
3. Preprocessing logic:
Call org.apache.shiro.web.filter.PathMatchingFilter#preHandle
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { if (this.appliedPaths != null && !this.appliedPaths.isEmpty()) { Iterator var3 = this.appliedPaths.keySet().iterator(); String path; do { if (!var3.hasNext()) { return true; } path = (String)var3.next(); } while(!this.pathsMatch(path, request)); log.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path); Object config = this.appliedPaths.get(path); return this.isFilterChainContinued(request, response, path, config); } else { if (log.isTraceEnabled()) { log.trace("appliedPaths property is null or empty. This Filter will passthrough immediately."); } return true; } } private boolean isFilterChainContinued(ServletRequest request, ServletResponse response, String path, Object pathConfig) throws Exception { if (this.isEnabled(request, response, path, pathConfig)) { if (log.isTraceEnabled()) { log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}]. Delegating to subclass implementation for 'onPreHandle' check.", new Object[]{this.getName(), path, pathConfig}); } return this.onPreHandle(request, response, pathConfig); } else { if (log.isTraceEnabled()) { log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}]. The next element in the FilterChain will be called immediately.", new Object[]{this.getName(), path, pathConfig}); } return true; } }
1> this.isEnabled(request, response, path, pathConfig) step is true by default, that is, it will go to the onPreHandle method below. If false is returned, it will be returned directly to the next chain execution.
2> Call org.apache.shiro.web.filter.AccessControlFilter#onPreHandle: logic here is understood as logic and operation. If one returns true, it returns true, and the next chain will be executed. That is, if access is allowed or denied, it will go to the next chain. It will jump only when isAccessAllowed returns false and onAccessDenied returns false Over the chain.
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return this.isAccessAllowed(request, response, mappedValue) || this.onAccessDenied(request, response, mappedValue); }
Here are two more steps:
Step 1: call isAccessAllowed to determine whether the request is allowed to access (mappedValue is the parameter of the configured request). If it is authenticated or not the login address, and the configured parameter contains permission, access is allowed
It will be called to: org.apache.shiro.web.filter.authc.AuthenticatingFilter#isAccessAllowed
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue)); } protected boolean isPermissive(Object mappedValue) { if(mappedValue != null) { String[] values = (String[]) mappedValue; return Arrays.binarySearch(values, PERMISSIVE) >= 0; } return false; }
- super.isAccessAllowed continues to call: org.apache.shiro.web.filter.authc.AuthenticationFilter#isAccessAllowed to judge whether the request has been authorized (about how to conduct post marking research after authentication)
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = this.getSubject(request, response); return subject.isAuthenticated() && subject.getPrincipal() != null; }
getSubject actually calls org.apache.shiro.SecurityUtils#getSubject: (actually gets the object from ThreadLocal)
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
- Determine the landing address of stone Buddha: org.apache.shiro.web.filter.AccessControlFilter#isLoginRequest
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) { return this.pathsMatch(this.getLoginUrl(), request); }
Step 2: onAccessDenied determines whether access is denied
- org.apache.shiro.web.filter.AccessControlFilter#onAccessDenied(javax.servlet.ServletRequest, javax.servlet.ServletResponse, java.lang.Object)
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return onAccessDenied(request, response); }
- org.apache.shiro.web.filter.authc.FormAuthenticationFilter#onAccessDenied
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); } return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } //allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } saveRequestAndRedirectToLogin(request, response); return false; } } protected boolean isLoginSubmission(ServletRequest request, ServletResponse response) { return (request instanceof HttpServletRequest) && WebUtils.toHttp(request).getMethod().equalsIgnoreCase(POST_METHOD); } protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { AuthenticationToken token = createToken(request, response); if (token == null) { String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken " + "must be created in order to execute a login attempt."; throw new IllegalStateException(msg); } try { Subject subject = getSubject(request, response); subject.login(token); return onLoginSuccess(token, subject, request, response); } catch (AuthenticationException e) { return onLoginFailure(token, e, request, response); } }
First, judge whether it is a login request:
1> If yes:
Judge whether to submit the login request and return to execute the login request; if not, return true. If true is returned, the chain can continue to execute.
2> If not:
Call saveRequestAndRedirectToLogin(request, response); then return false. If false is returned, the request chain will not continue to execute.
org.apache.shiro.web.filter.AccessControlFilter#saveRequestAndRedirectToLogin is to save the request and redirect it to the login address
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException { saveRequest(request); redirectToLogin(request, response); } protected void saveRequest(ServletRequest request) { WebUtils.saveRequest(request); } protected void redirectToLogin(ServletRequest request, ServletResponse response) throws IOException { String loginUrl = getLoginUrl(); WebUtils.issueRedirect(request, response, loginUrl); }
WebUtils tool classes are as follows:
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.shiro.web.util; import org.apache.shiro.SecurityUtils; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.DefaultSubjectContext; import org.apache.shiro.util.StringUtils; import org.apache.shiro.web.env.EnvironmentLoader; import org.apache.shiro.web.env.WebEnvironment; import org.apache.shiro.web.filter.AccessControlFilter; import org.owasp.encoder.Encode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContext; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.Map; /** * Simple utility class for operations used across multiple class hierarchies in the web framework code. * <p/> * Some methods in this class were copied from the Spring Framework so we didn't have to re-invent the wheel, * and in these cases, we have retained all license, copyright and author information. * * @since 0.9 */ public class WebUtils { //TODO - complete JavaDoc private static final Logger log = LoggerFactory.getLogger(WebUtils.class); public static final String SERVLET_REQUEST_KEY = ServletRequest.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY"; public static final String SERVLET_RESPONSE_KEY = ServletResponse.class.getName() + "_SHIRO_THREAD_CONTEXT_KEY"; /** * {@link org.apache.shiro.session.Session Session} key used to save a request and later restore it, for example when redirecting to a * requested page after login, equal to {@code shiroSavedRequest}. */ public static final String SAVED_REQUEST_KEY = "shiroSavedRequest"; /** * Standard Servlet 2.3+ spec request attributes for include URI and paths. * <p>If included via a RequestDispatcher, the current resource will see the * originating request. Its own URI and paths are exposed as request attributes. */ public static final String INCLUDE_REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri"; public static final String INCLUDE_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.include.context_path"; public static final String INCLUDE_SERVLET_PATH_ATTRIBUTE = "javax.servlet.include.servlet_path"; public static final String INCLUDE_PATH_INFO_ATTRIBUTE = "javax.servlet.include.path_info"; public static final String INCLUDE_QUERY_STRING_ATTRIBUTE = "javax.servlet.include.query_string"; /** * Standard Servlet 2.4+ spec request attributes for forward URI and paths. * <p>If forwarded to via a RequestDispatcher, the current resource will see its * own URI and paths. The originating URI and paths are exposed as request attributes. */ public static final String FORWARD_REQUEST_URI_ATTRIBUTE = "javax.servlet.forward.request_uri"; public static final String FORWARD_CONTEXT_PATH_ATTRIBUTE = "javax.servlet.forward.context_path"; public static final String FORWARD_SERVLET_PATH_ATTRIBUTE = "javax.servlet.forward.servlet_path"; public static final String FORWARD_PATH_INFO_ATTRIBUTE = "javax.servlet.forward.path_info"; public static final String FORWARD_QUERY_STRING_ATTRIBUTE = "javax.servlet.forward.query_string"; /** * Default character encoding to use when <code>request.getCharacterEncoding</code> * returns <code>null</code>, according to the Servlet spec. * * @see javax.servlet.ServletRequest#getCharacterEncoding */ public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1"; /** * Return the path within the web application for the given request. * Detects include request URL if called within a RequestDispatcher include. * <p/> * For example, for a request to URL * <p/> * <code>http://www.somehost.com/myapp/my/url.jsp</code>, * <p/> * for an application deployed to <code>/mayapp</code> (the application's context path), this method would return * <p/> * <code>/my/url.jsp</code>. * * @param request current HTTP request * @return the path within the web application */ public static String getPathWithinApplication(HttpServletRequest request) { return normalize(removeSemicolon(getServletPath(request) + getPathInfo(request))); } /** * Return the request URI for the given request, detecting an include request * URL if called within a RequestDispatcher include. * <p>As the value returned by <code>request.getRequestURI()</code> is <i>not</i> * decoded by the servlet container, this method will decode it. * <p>The URI that the web container resolves <i>should</i> be correct, but some * containers like JBoss/Jetty incorrectly include ";" strings like ";jsessionid" * in the URI. This method cuts off such incorrect appendices. * * @param request current HTTP request * @return the request URI * @deprecated use getPathWithinApplication() to get the path minus the context path, or call HttpServletRequest.getRequestURI() directly from your code. */ @Deprecated public static String getRequestUri(HttpServletRequest request) { String uri = (String) request.getAttribute(INCLUDE_REQUEST_URI_ATTRIBUTE); if (uri == null) { uri = request.getRequestURI(); } return normalize(decodeAndCleanUriString(request, uri)); } private static String getServletPath(HttpServletRequest request) { String servletPath = (String) request.getAttribute(INCLUDE_SERVLET_PATH_ATTRIBUTE); return servletPath != null ? servletPath : valueOrEmpty(request.getServletPath()); } private static String getPathInfo(HttpServletRequest request) { String pathInfo = (String) request.getAttribute(INCLUDE_PATH_INFO_ATTRIBUTE); return pathInfo != null ? pathInfo : valueOrEmpty(request.getPathInfo()); } private static String valueOrEmpty(String input) { if (input == null) { return ""; } return input; } /** * Normalize a relative URI path that may have relative values ("/./", * "/../", and so on ) it it. <strong>WARNING</strong> - This method is * useful only for normalizing application-generated paths. It does not * try to perform security checks for malicious input. * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in * Tomcat trunk, r939305 * * @param path Relative path to be normalized * @return normalized path */ public static String normalize(String path) { return normalize(path, true); } /** * Normalize a relative URI path that may have relative values ("/./", * "/../", and so on ) it it. <strong>WARNING</strong> - This method is * useful only for normalizing application-generated paths. It does not * try to perform security checks for malicious input. * Normalize operations were was happily taken from org.apache.catalina.util.RequestUtil in * Tomcat trunk, r939305 * * @param path Relative path to be normalized * @param replaceBackSlash Should '\\' be replaced with '/' * @return normalized path */ private static String normalize(String path, boolean replaceBackSlash) { if (path == null) return null; // Create a place for the normalized path String normalized = path; if (replaceBackSlash && normalized.indexOf('\\') >= 0) normalized = normalized.replace('\\', '/'); if (normalized.equals("/.")) return "/"; // Add a leading "/" if necessary if (!normalized.startsWith("/")) normalized = "/" + normalized; // Resolve occurrences of "//" in the normalized path while (true) { int index = normalized.indexOf("//"); if (index < 0) break; normalized = normalized.substring(0, index) + normalized.substring(index + 1); } // Resolve occurrences of "/./" in the normalized path while (true) { int index = normalized.indexOf("/./"); if (index < 0) break; normalized = normalized.substring(0, index) + normalized.substring(index + 2); } // Resolve occurrences of "/../" in the normalized path while (true) { int index = normalized.indexOf("/../"); if (index < 0) break; if (index == 0) return (null); // Trying to go outside our context int index2 = normalized.lastIndexOf('/', index - 1); normalized = normalized.substring(0, index2) + normalized.substring(index + 3); } // Return the normalized path that we have completed return (normalized); } /** * Decode the supplied URI string and strips any extraneous portion after a ';'. * * @param request the incoming HttpServletRequest * @param uri the application's URI string * @return the supplied URI string stripped of any extraneous portion after a ';'. */ private static String decodeAndCleanUriString(HttpServletRequest request, String uri) { uri = decodeRequestString(request, uri); return removeSemicolon(uri); } private static String removeSemicolon(String uri) { int semicolonIndex = uri.indexOf(';'); return (semicolonIndex != -1 ? uri.substring(0, semicolonIndex) : uri); } /** * Return the context path for the given request, detecting an include request * URL if called within a RequestDispatcher include. * <p>As the value returned by <code>request.getContextPath()</code> is <i>not</i> * decoded by the servlet container, this method will decode it. * * @param request current HTTP request * @return the context path */ public static String getContextPath(HttpServletRequest request) { String contextPath = (String) request.getAttribute(INCLUDE_CONTEXT_PATH_ATTRIBUTE); if (contextPath == null) { contextPath = request.getContextPath(); } contextPath = normalize(decodeRequestString(request, contextPath)); if ("/".equals(contextPath)) { // the normalize method will return a "/" and includes on Jetty, will also be a "/". contextPath = ""; } return contextPath; } /** * Find the Shiro {@link WebEnvironment} for this web application, which is typically loaded via the * {@link org.apache.shiro.web.env.EnvironmentLoaderListener}. * <p/> * This implementation rethrows an exception that happened on environment startup to differentiate between a failed * environment startup and no environment at all. * * @param sc ServletContext to find the web application context for * @return the root WebApplicationContext for this web app * @throws IllegalStateException if the root WebApplicationContext could not be found * @see org.apache.shiro.web.env.EnvironmentLoader#ENVIRONMENT_ATTRIBUTE_KEY * @since 1.2 */ public static WebEnvironment getRequiredWebEnvironment(ServletContext sc) throws IllegalStateException { WebEnvironment we = getWebEnvironment(sc); if (we == null) { throw new IllegalStateException("No WebEnvironment found: no EnvironmentLoaderListener registered?"); } return we; } /** * Find the Shiro {@link WebEnvironment} for this web application, which is typically loaded via * {@link org.apache.shiro.web.env.EnvironmentLoaderListener}. * <p/> * This implementation rethrows an exception that happened on environment startup to differentiate between a failed * environment startup and no environment at all. * * @param sc ServletContext to find the web application context for * @return the root WebApplicationContext for this web app, or <code>null</code> if none * @see org.apache.shiro.web.env.EnvironmentLoader#ENVIRONMENT_ATTRIBUTE_KEY * @since 1.2 */ public static WebEnvironment getWebEnvironment(ServletContext sc) { return getWebEnvironment(sc, EnvironmentLoader.ENVIRONMENT_ATTRIBUTE_KEY); } /** * Find the Shiro {@link WebEnvironment} for this web application. * * @param sc ServletContext to find the web application context for * @param attrName the name of the ServletContext attribute to look for * @return the desired WebEnvironment for this web app, or <code>null</code> if none * @since 1.2 */ public static WebEnvironment getWebEnvironment(ServletContext sc, String attrName) { if (sc == null) { throw new IllegalArgumentException("ServletContext argument must not be null."); } Object attr = sc.getAttribute(attrName); if (attr == null) { return null; } if (attr instanceof RuntimeException) { throw (RuntimeException) attr; } if (attr instanceof Error) { throw (Error) attr; } if (attr instanceof Exception) { throw new IllegalStateException((Exception) attr); } if (!(attr instanceof WebEnvironment)) { throw new IllegalStateException("Context attribute is not of type WebEnvironment: " + attr); } return (WebEnvironment) attr; } /** * Decode the given source string with a URLDecoder. The encoding will be taken * from the request, falling back to the default "ISO-8859-1". * <p>The default implementation uses <code>URLDecoder.decode(input, enc)</code>. * * @param request current HTTP request * @param source the String to decode * @return the decoded String * @see #DEFAULT_CHARACTER_ENCODING * @see javax.servlet.ServletRequest#getCharacterEncoding * @see java.net.URLDecoder#decode(String, String) * @see java.net.URLDecoder#decode(String) */ @SuppressWarnings({"deprecation"}) public static String decodeRequestString(HttpServletRequest request, String source) { String enc = determineEncoding(request); try { return URLDecoder.decode(source, enc); } catch (UnsupportedEncodingException ex) { if (log.isWarnEnabled()) { log.warn("Could not decode request string [" + Encode.forHtml(source) + "] with encoding '" + Encode.forHtml(enc) + "': falling back to platform default encoding; exception message: " + ex.getMessage()); } return URLDecoder.decode(source); } } /** * Determine the encoding for the given request. * Can be overridden in subclasses. * <p>The default implementation checks the request's * {@link ServletRequest#getCharacterEncoding() character encoding}, and if that * <code>null</code>, falls back to the {@link #DEFAULT_CHARACTER_ENCODING}. * * @param request current HTTP request * @return the encoding for the request (never <code>null</code>) * @see javax.servlet.ServletRequest#getCharacterEncoding() */ protected static String determineEncoding(HttpServletRequest request) { String enc = request.getCharacterEncoding(); if (enc == null) { enc = DEFAULT_CHARACTER_ENCODING; } return enc; } /* * Returns {@code true} IFF the specified {@code SubjectContext}: * <ol> * <li>A {@link WebSubjectContext} instance</li> * <li>The {@code WebSubjectContext}'s request/response pair are not null</li> * <li>The request is an {@link HttpServletRequest} instance</li> * <li>The response is an {@link HttpServletResponse} instance</li> * </ol> * * @param context the SubjectContext to check to see if it is HTTP compatible. * @return {@code true} IFF the specified context has HTTP request/response objects, {@code false} otherwise. * @since 1.0 */ public static boolean isWeb(Object requestPairSource) { return requestPairSource instanceof RequestPairSource && isWeb((RequestPairSource) requestPairSource); } public static boolean isHttp(Object requestPairSource) { return requestPairSource instanceof RequestPairSource && isHttp((RequestPairSource) requestPairSource); } public static ServletRequest getRequest(Object requestPairSource) { if (requestPairSource instanceof RequestPairSource) { return ((RequestPairSource) requestPairSource).getServletRequest(); } return null; } public static ServletResponse getResponse(Object requestPairSource) { if (requestPairSource instanceof RequestPairSource) { return ((RequestPairSource) requestPairSource).getServletResponse(); } return null; } public static HttpServletRequest getHttpRequest(Object requestPairSource) { ServletRequest request = getRequest(requestPairSource); if (request instanceof HttpServletRequest) { return (HttpServletRequest) request; } return null; } public static HttpServletResponse getHttpResponse(Object requestPairSource) { ServletResponse response = getResponse(requestPairSource); if (response instanceof HttpServletResponse) { return (HttpServletResponse) response; } return null; } private static boolean isWeb(RequestPairSource source) { ServletRequest request = source.getServletRequest(); ServletResponse response = source.getServletResponse(); return request != null && response != null; } private static boolean isHttp(RequestPairSource source) { ServletRequest request = source.getServletRequest(); ServletResponse response = source.getServletResponse(); return request instanceof HttpServletRequest && response instanceof HttpServletResponse; } /** * Returns {@code true} if a session is allowed to be created for a subject-associated request, {@code false} * otherwise. * <p/> * <b>This method exists for Shiro's internal framework needs and should never be called by Shiro end-users. It * could be changed/removed at any time.</b> * * @param requestPairSource a {@link RequestPairSource} instance, almost always a * {@link org.apache.shiro.web.subject.WebSubject WebSubject} instance. * @return {@code true} if a session is allowed to be created for a subject-associated request, {@code false} * otherwise. */ public static boolean _isSessionCreationEnabled(Object requestPairSource) { if (requestPairSource instanceof RequestPairSource) { RequestPairSource source = (RequestPairSource) requestPairSource; return _isSessionCreationEnabled(source.getServletRequest()); } return true; //by default } /** * Returns {@code true} if a session is allowed to be created for a subject-associated request, {@code false} * otherwise. * <p/> * <b>This method exists for Shiro's internal framework needs and should never be called by Shiro end-users. It * could be changed/removed at any time.</b> * * @param request incoming servlet request. * @return {@code true} if a session is allowed to be created for a subject-associated request, {@code false} * otherwise. */ public static boolean _isSessionCreationEnabled(ServletRequest request) { if (request != null) { Object val = request.getAttribute(DefaultSubjectContext.SESSION_CREATION_ENABLED); if (val != null && val instanceof Boolean) { return (Boolean) val; } } return true; //by default } /** * A convenience method that merely casts the incoming <code>ServletRequest</code> to an * <code>HttpServletRequest</code>: * <p/> * <code>return (HttpServletRequest)request;</code> * <p/> * Logic could be changed in the future for logging or throwing an meaningful exception in * non HTTP request environments (e.g. Portlet API). * * @param request the incoming ServletRequest * @return the <code>request</code> argument casted to an <code>HttpServletRequest</code>. */ public static HttpServletRequest toHttp(ServletRequest request) { return (HttpServletRequest) request; } /** * A convenience method that merely casts the incoming <code>ServletResponse</code> to an * <code>HttpServletResponse</code>: * <p/> * <code>return (HttpServletResponse)response;</code> * <p/> * Logic could be changed in the future for logging or throwing an meaningful exception in * non HTTP request environments (e.g. Portlet API). * * @param response the outgoing ServletResponse * @return the <code>response</code> argument casted to an <code>HttpServletResponse</code>. */ public static HttpServletResponse toHttp(ServletResponse response) { return (HttpServletResponse) response; } /** * Redirects the current request to a new URL based on the given parameters. * * @param request the servlet request. * @param response the servlet response. * @param url the URL to redirect the user to. * @param queryParams a map of parameters that should be set as request parameters for the new request. * @param contextRelative true if the URL is relative to the servlet context path, or false if the URL is absolute. * @param http10Compatible whether to stay compatible with HTTP 1.0 clients. * @throws java.io.IOException if thrown by response methods. */ public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative, boolean http10Compatible) throws IOException { RedirectView view = new RedirectView(url, contextRelative, http10Compatible); view.renderMergedOutputModel(queryParams, toHttp(request), toHttp(response)); } /** * Redirects the current request to a new URL based on the given parameters and default values * for unspecified parameters. * * @param request the servlet request. * @param response the servlet response. * @param url the URL to redirect the user to. * @throws java.io.IOException if thrown by response methods. */ public static void issueRedirect(ServletRequest request, ServletResponse response, String url) throws IOException { issueRedirect(request, response, url, null, true, true); } /** * Redirects the current request to a new URL based on the given parameters and default values * for unspecified parameters. * * @param request the servlet request. * @param response the servlet response. * @param url the URL to redirect the user to. * @param queryParams a map of parameters that should be set as request parameters for the new request. * @throws java.io.IOException if thrown by response methods. */ public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams) throws IOException { issueRedirect(request, response, url, queryParams, true, true); } /** * Redirects the current request to a new URL based on the given parameters and default values * for unspecified parameters. * * @param request the servlet request. * @param response the servlet response. * @param url the URL to redirect the user to. * @param queryParams a map of parameters that should be set as request parameters for the new request. * @param contextRelative true if the URL is relative to the servlet context path, or false if the URL is absolute. * @throws java.io.IOException if thrown by response methods. */ public static void issueRedirect(ServletRequest request, ServletResponse response, String url, Map queryParams, boolean contextRelative) throws IOException { issueRedirect(request, response, url, queryParams, contextRelative, true); } /** * <p>Checks to see if a request param is considered true using a loose matching strategy for * general values that indicate that something is true or enabled, etc.</p> * <p/> * <p>Values that are considered "true" include (case-insensitive): true, t, 1, enabled, y, yes, on.</p> * * @param request the servlet request * @param paramName @return true if the param value is considered true or false if it isn't. * @return true if the given parameter is considered "true" - false otherwise. */ public static boolean isTrue(ServletRequest request, String paramName) { String value = getCleanParam(request, paramName); return value != null && (value.equalsIgnoreCase("true") || value.equalsIgnoreCase("t") || value.equalsIgnoreCase("1") || value.equalsIgnoreCase("enabled") || value.equalsIgnoreCase("y") || value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("on")); } /** * Convenience method that returns a request parameter value, first running it through * {@link StringUtils#clean(String)}. * * @param request the servlet request. * @param paramName the parameter name. * @return the clean param value, or null if the param does not exist or is empty. */ public static String getCleanParam(ServletRequest request, String paramName) { return StringUtils.clean(request.getParameter(paramName)); } public static void saveRequest(ServletRequest request) { Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); HttpServletRequest httpRequest = toHttp(request); SavedRequest savedRequest = new SavedRequest(httpRequest); session.setAttribute(SAVED_REQUEST_KEY, savedRequest); } public static SavedRequest getAndClearSavedRequest(ServletRequest request) { SavedRequest savedRequest = getSavedRequest(request); if (savedRequest != null) { Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(); session.removeAttribute(SAVED_REQUEST_KEY); } return savedRequest; } public static SavedRequest getSavedRequest(ServletRequest request) { SavedRequest savedRequest = null; Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(false); if (session != null) { savedRequest = (SavedRequest) session.getAttribute(SAVED_REQUEST_KEY); } return savedRequest; } /** * Redirects the to the request url from a previously * {@link #saveRequest(javax.servlet.ServletRequest) saved} request, or if there is no saved request, redirects the * end user to the specified {@code fallbackUrl}. If there is no saved request or fallback url, this method * throws an {@link IllegalStateException}. * <p/> * This method is primarily used to support a common login scenario - if an unauthenticated user accesses a * page that requires authentication, it is expected that request is * {@link #saveRequest(javax.servlet.ServletRequest) saved} first and then redirected to the login page. Then, * after a successful login, this method can be called to redirect them back to their originally requested URL, a * nice usability feature. * * @param request the incoming request * @param response the outgoing response * @param fallbackUrl the fallback url to redirect to if there is no saved request available. * @throws IllegalStateException if there is no saved request and the {@code fallbackUrl} is {@code null}. * @throws IOException if there is an error redirecting * @since 1.0 */ public static void redirectToSavedRequest(ServletRequest request, ServletResponse response, String fallbackUrl) throws IOException { String successUrl = null; boolean contextRelative = true; SavedRequest savedRequest = WebUtils.getAndClearSavedRequest(request); if (savedRequest != null && savedRequest.getMethod().equalsIgnoreCase(AccessControlFilter.GET_METHOD)) { successUrl = savedRequest.getRequestUrl(); contextRelative = false; } if (successUrl == null) { successUrl = fallbackUrl; } if (successUrl == null) { throw new IllegalStateException("Success URL not available via saved request or via the " + "successUrlFallback method parameter. One of these must be non-null for " + "issueSuccessRedirect() to work."); } WebUtils.issueRedirect(request, response, successUrl, null, contextRelative); } }
So far, the interception of unlisted access has been completed, and the login address has been redirected, as well as the release of the login address.
3. Landing principle
View how login is called from Controller to realm and how login status is maintained.
1. Pre modification
1. Add login address
@GetMapping("/login2") public String login2() { Subject subject = SecurityUtils.getSubject(); AuthenticationToken generateToken = new UsernamePasswordToken("zs", "111222"); subject.login(generateToken); return "success"; }
2. ShiroConfig configures this address to allow anonymous access
/** * Path - > filter name 1 [parameter 1, parameter 2, parameter 3...], filter name 2 [parameter 1, parameter 2...] * Custom configuration (path in front, specific filter name and parameters in the back, multiple filter parameters are separated by commas, and multiple filter parameters are also separated by commas)) * Some filters do not require parameters, such as anon, authc and shiro. When parsing, an array is parsed as [name, null] by default */ FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // Test address FILTER_CHAIN_DEFINITION_MAP.put("/login2", "anon"); // Login address FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[system administrator,User administrator],perms['user:manager:*']"); FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // All resources need to be validated
3. Modify the custom realm authentication method
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { User user = new User(); user.setPassword("111222"); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName()); }
2. Test login
1. Access test address / login2
2. First pass through shiro's anon filter, and then enter the later controller method.
three org.apache.shiro.SecurityUtils#getSubject to obtain the subject, which is actually obtained from ThreadLocal
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
4. Call org.apache.shiro.subject.support.DelegatingSubject#login for authentication. The process is as follows:
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals: principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } if (principals == null || principals.isEmpty()) { String msg = "Principals returned from securityManager.login( token ) returned a null or " + "empty value. This value must be non null and populated with one or more elements."; throw new IllegalStateException(msg); } this.principals = principals; this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; } Session session = subject.getSession(false); if (session != null) { this.session = decorate(session); } else { this.session = null; } }
The core passes the subject object and usernamePasswordToken inside the securityManager.login(this, token) call. After successful authentication, some information will be verified and saved to the current object, that is, the current subject will be marked as authenticated.
5. org.apache.shiro.mgt.DefaultSecurityManager#login
/** * First authenticates the {@code AuthenticationToken} argument, and if successful, constructs a * {@code Subject} instance representing the authenticated account's identity. * <p/> * Once constructed, the {@code Subject} instance is then {@link #bind bound} to the application for * subsequent access before being returned to the caller. * * @param token the authenticationToken to process for the login attempt. * @return a Subject representing the authenticated user. * @throws AuthenticationException if there is a problem authenticating the specified {@code token}. */ public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { try { onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; //propagate } Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn; }
(1) Call org.apache.shiro.mgt.AuthenticatingSecurityManager#authenticate to authenticate
/** * Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication. */ public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); }
1> Continue Calling: org.apache.shiro.authc.AbstractAuthenticator#authenticate
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { if (token == null) { throw new IllegalArgumentException("Method argument (authentication token) cannot be null."); } log.trace("Authentication attempt received for token [{}]", token); AuthenticationInfo info; try { info = doAuthenticate(token); if (info == null) { String msg = "No account information found for authentication token [" + token + "] by this " + "Authenticator instance. Please check that it is configured correctly."; throw new AuthenticationException(msg); } } catch (Throwable t) { AuthenticationException ae = null; if (t instanceof AuthenticationException) { ae = (AuthenticationException) t; } if (ae == null) { //Exception thrown was not an expected AuthenticationException. Therefore it is probably a little more //severe or unexpected. So, wrap in an AuthenticationException, log to warn, and propagate: String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " + "error? (Typical or expected login exceptions should extend from AuthenticationException)."; ae = new AuthenticationException(msg, t); if (log.isWarnEnabled()) log.warn(msg, t); } try { notifyFailure(token, ae); } catch (Throwable t2) { if (log.isWarnEnabled()) { String msg = "Unable to send notification for failed authentication attempt - listener error?. " + "Please check your AuthenticationListener implementation(s). Logging sending exception " + "and propagating original AuthenticationException instead..."; log.warn(msg, t2); } } throw ae; } log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info); notifySuccess(token, info); return info; }
2> Continue to call org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate: (this is actually transferring the request to realm)
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
It can be seen from here that a variety of realm authentication methods can be supported. The single realm authentication method is studied here. Research after multi realm authentication.
3> Continue to call org.apache.shiro.authc.pam.modularrealmauthenticator#dosinglerealauthentication:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } return info; }
- First, call org.apache.shiro.realm.AuthenticatingRealm#supports to determine whether the token is supported (that is, whether the type matches)
public boolean supports(AuthenticationToken token) { return token != null && getAuthenticationTokenClass().isAssignableFrom(token.getClass()); }
- Call realm.getAuthenticationInfo(token); Obtain the authentication information. If the micro null is obtained, an UnknownAccountException(msg) will be thrown; abnormal
4> Continue to study and obtain authentication information org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
- First, according to org.apache.shiro.authc.UsernamePasswordToken#getPrincipal is to get the cache according to the user's unique identity (internally getUsername()). If the direct authentication password is obtained; If it cannot be obtained, call realm to obtain
- Call realm to obtain authentication information. Here, call: Com.zd.bx.config.shiro.customrealm#dogetauthenticationinfo (that is, your own realm)
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { User user = new User(); user.setPassword("111222"); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName()); }
org.apache.shiro.authc.SimpleAuthenticationInfo#SimpleAuthenticationInfo(java.lang.Object, java.lang.Object, java.lang.String) is constructed as follows:
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) { // Identity information this.principals = new SimplePrincipalCollection(principal, realmName); // Voucher information can be understood as password(The secret of the user database, isn't it token The password inside) this.credentials = credentials; }
Org.apache.shiro.subject.simpleprincipal collection #simpleprincipal collection (java.lang.object, Java. Lang.string) is equivalent to cache related data:
private Map<String, Set> realmPrincipals; public SimplePrincipalCollection(Object principal, String realmName) { if (principal instanceof Collection) { addAll((Collection) principal, realmName); } else { add(principal, realmName); } } public void add(Object principal, String realmName) { if (realmName == null) { throw new NullPointerException("realmName argument cannot be null."); } if (principal == null) { throw new NullPointerException("principal argument cannot be null."); } this.cachedToString = null; getPrincipalsLazy(realmName).add(principal); } protected Collection getPrincipalsLazy(String realmName) { if (realmPrincipals == null) { realmPrincipals = new LinkedHashMap<String, Set>(); } Set principals = realmPrincipals.get(realmName); if (principals == null) { principals = new LinkedHashSet(); realmPrincipals.put(realmName, principals); } return principals; }
- After obtaining the authentication information, it is cached. org.apache.shiro.realm.AuthenticatingRealm#cacheAuthenticationInfoIfPossible
private void cacheAuthenticationInfoIfPossible(AuthenticationToken token, AuthenticationInfo info) { if (!isAuthenticationCachingEnabled(token, info)) { log.debug("AuthenticationInfo caching is disabled for info [{}]. Submitted token: [{}].", info, token); //return quietly, caching is disabled for this token/info pair: return; } Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache(); if (cache != null) { Object key = getAuthenticationCacheKey(token); cache.put(key, info); log.trace("Cached AuthenticationInfo for continued authentication. key=[{}], value=[{}].", key, info); } }
The core logic is that if the cache is enabled, the cached key is generated according to the token (by default, the unique key is generated according to the username), and then cached.
- If the authentication information is not null, match the identity certificate information, that is, verify the password
org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch verifies the credential information, and throws an exception if it does not match
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { //not successful - throw an exception to indicate this: String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + "credentials during authentication. If you do not wish for credentials to be examined, you " + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }
Continue to call: org.apache.shiro.authc.credential.simplecredentialsmatcher#docedentialsmatch is actually matched according to the credential information of the token and the credential information of the authentication information.
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenCredentials = getCredentials(token); Object accountCredentials = getCredentials(info); return equals(tokenCredentials, accountCredentials); } protected Object getCredentials(AuthenticationToken token) { return token.getCredentials(); } protected Object getCredentials(AuthenticationInfo info) { return info.getCredentials(); }
(2) createSubject(token, info, subject) creates a Subject
1> Call org.apache.shiro.mgt.DefaultSecurityManager#createSubject(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo, org.apache.shiro.subject.Subject)
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) { SubjectContext context = createSubjectContext(); context.setAuthenticated(true); context.setAuthenticationToken(token); context.setAuthenticationInfo(info); context.setSecurityManager(this); if (existing != null) { context.setSubject(existing); } return createSubject(context); }
Continue to call: org.apache.shiro.mgt.DefaultSecurityManager#createSubject(org.apache.shiro.subject.SubjectContext)
public Subject createSubject(SubjectContext subjectContext) { //create a copy so we don't modify the argument's backing map: SubjectContext context = copy(subjectContext); //ensure that the context has a SecurityManager instance, and if not, add one: context = ensureSecurityManager(context); //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the //process is often environment specific - better to shield the SF from these details: context = resolveSession(context); //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first //if possible before handing off to the SubjectFactory: context = resolvePrincipals(context); Subject subject = doCreateSubject(context); //save this subject for future reference if necessary: //(this is needed here in case rememberMe principals were resolved and they need to be stored in the //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation). //Added in 1.2: save(subject); return subject; }
- Continue to call org.apache.shiro.web.mgt.DefaultWebSubjectFactory#createSubject to create a Subject (in fact, resolve properties and then create an object)
public Subject createSubject(SubjectContext context) { //SHIRO-646 //Check if the existing subject is NOT a WebSubject. If it isn't, then call super.createSubject instead. //Creating a WebSubject from a non-web Subject will cause the ServletRequest and ServletResponse to be null, which wil fail when creating a session. boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject); if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletRequest request = wsc.resolveServletRequest(); ServletResponse response = wsc.resolveServletResponse(); return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); }
- org.apache.shiro.mgt.DefaultSecurityManager#save save save subject
protected void save(Subject subject) { this.subjectDAO.save(subject); }
Continue Calling: org.apache.shiro.mgt.DefaultSubjectDAO#saveToSession
protected void saveToSession(Subject subject) { //performs merge logic, only updating the Subject's session if it does not match the current state: mergePrincipals(subject); mergeAuthenticationState(subject); }
Org.apache.shiro.mgt.defaultsubjectdao#mergeprinciples: save identity information to session
protected void mergePrincipals(Subject subject) { //merge PrincipalCollection state: PrincipalCollection currentPrincipals = null; //SHIRO-380: added if/else block - need to retain original (source) principals //This technique (reflection) is only temporary - a proper long term solution needs to be found, //but this technique allowed an immediate fix that is API point-version forwards and backwards compatible // //A more comprehensive review / cleaning of runAs should be performed for Shiro 1.3 / 2.0 + if (subject.isRunAs() && subject instanceof DelegatingSubject) { try { Field field = DelegatingSubject.class.getDeclaredField("principals"); field.setAccessible(true); currentPrincipals = (PrincipalCollection)field.get(subject); } catch (Exception e) { throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e); } } if (currentPrincipals == null || currentPrincipals.isEmpty()) { currentPrincipals = subject.getPrincipals(); } Session session = subject.getSession(false); if (session == null) { if (!isEmpty(currentPrincipals)) { session = subject.getSession(); session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } // otherwise no session and no principals - nothing to save } else { PrincipalCollection existingPrincipals = (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if (isEmpty(currentPrincipals)) { if (!isEmpty(existingPrincipals)) { session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); } // otherwise both are null or empty - no need to update the session } else { if (!currentPrincipals.equals(existingPrincipals)) { session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } // otherwise they're the same - no need to update the session } } }
org.apache.shiro.mgt.DefaultSubjectDAO#mergeAuthenticationState save the authentication information to the session
protected void mergeAuthenticationState(Subject subject) { Session session = subject.getSession(false); if (session == null) { if (subject.isAuthenticated()) { session = subject.getSession(); session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE); } //otherwise no session and not authenticated - nothing to save } else { Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY); if (subject.isAuthenticated()) { if (existingAuthc == null || !existingAuthc) { session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE); } //otherwise authc state matches - no need to update the session } else { if (existingAuthc != null) { //existing doesn't match the current state - remove it: session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY); } //otherwise not in the session and not authenticated - no need to update the session } } }
(3) The current subject records the identity information, login success status, login host information, etc. stored in principals login, and then ends the login method
6. After the above login is completed, we can access another request to see how it maintains the login status and how the subject obtains the login status
(1) The first Subject to be created and recorded in ThreadLocal is at org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal
(2) Continue calling org.apache.shiro.web.servlet.AbstractShiroFilter#createSubject
protected WebSubject createSubject(ServletRequest request, ServletResponse response) { return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject(); }
(3) Finally, it will call org.apache.shiro.mgt.DefaultSecurityManager#createSubject(org.apache.shiro.subject.SubjectContext)
public Subject createSubject(SubjectContext subjectContext) { //create a copy so we don't modify the argument's backing map: SubjectContext context = copy(subjectContext); //ensure that the context has a SecurityManager instance, and if not, add one: context = ensureSecurityManager(context); //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before //sending to the SubjectFactory. The SubjectFactory should not need to know how to acquire sessions as the //process is often environment specific - better to shield the SF from these details: context = resolveSession(context); //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first //if possible before handing off to the SubjectFactory: context = resolvePrincipals(context); Subject subject = doCreateSubject(context); //save this subject for future reference if necessary: //(this is needed here in case rememberMe principals were resolved and they need to be stored in the //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation). //Added in 1.2: save(subject); return subject; }
Important methods:
1> org.apache.shiro.mgt.DefaultSecurityManager#resolveSession resolve session
protected SubjectContext resolveSession(SubjectContext context) { if (context.resolveSession() != null) { log.debug("Context already contains a session. Returning."); return context; } try { //Context couldn't resolve it directly, let's see if we can since we have direct access to //the session manager: Session session = resolveContextSession(context); if (session != null) { context.setSession(session); } } catch (InvalidSessionException e) { log.debug("Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous " + "(session-less) Subject instance.", e); } return context; }
The final resolved session is as follows: (you can see the relevant information after login)
2> org.apache.shiro.mgt.DefaultSecurityManager#resolvePrincipals resolve the identity information after login
protected SubjectContext resolvePrincipals(SubjectContext context) { PrincipalCollection principals = context.resolvePrincipals(); if (isEmpty(principals)) { log.trace("No identity (PrincipalCollection) found in the context. Looking for a remembered identity."); principals = getRememberedIdentity(context); if (!isEmpty(principals)) { log.debug("Found remembered PrincipalCollection. Adding to the context to be used " + "for subject construction by the SubjectFactory."); context.setPrincipals(principals); // The following call was removed (commented out) in Shiro 1.2 because it uses the session as an // implementation strategy. Session use for Shiro's own needs should be controlled in a single place // to be more manageable for end-users: there are a number of stateless (e.g. REST) applications that // use Shiro that need to ensure that sessions are only used when desirable. If Shiro's internal // implementations used Subject sessions (setting attributes) whenever we wanted, it would be much // harder for end-users to control when/where that occurs. // // Because of this, the SubjectDAO was created as the single point of control, and session state logic // has been moved to the DefaultSubjectDAO implementation. // Removed in Shiro 1.2. SHIRO-157 is still satisfied by the new DefaultSubjectDAO implementation // introduced in 1.2 // Satisfies SHIRO-157: // bindPrincipalsToSession(principals, context); } else { log.trace("No remembered identity found. Returning original context."); } } return context; }
Continuous call: org.apache.shiro.subject.support.defaultsubjectcontext#resolveprincipals (you can see the information of the login user from the session, so you can get principals3)
public PrincipalCollection resolvePrincipals() { PrincipalCollection principals = getPrincipals(); if (isEmpty(principals)) { //check to see if they were just authenticated: AuthenticationInfo info = getAuthenticationInfo(); if (info != null) { principals = info.getPrincipals(); } } if (isEmpty(principals)) { Subject subject = getSubject(); if (subject != null) { principals = subject.getPrincipals(); } } if (isEmpty(principals)) { //try the session: Session session = resolveSession(); if (session != null) { principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY); } } return principals; }
3> Call org.apache.shiro.web.mgt.DefaultWebSubjectFactory#createSubject to create a Subject
public Subject createSubject(SubjectContext context) { //SHIRO-646 //Check if the existing subject is NOT a WebSubject. If it isn't, then call super.createSubject instead. //Creating a WebSubject from a non-web Subject will cause the ServletRequest and ServletResponse to be null, which wil fail when creating a session. boolean isNotBasedOnWebSubject = context.getSubject() != null && !(context.getSubject() instanceof WebSubject); if (!(context instanceof WebSubjectContext) || isNotBasedOnWebSubject) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletRequest request = wsc.resolveServletRequest(); ServletResponse response = wsc.resolveServletResponse(); return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); }
Get the attribute and create the Subject. Whether to authenticate the attribute or not calls org.apache.shiro.Subject.support.defaultsubjectcontext#resolve authenticated to resolve
public boolean resolveAuthenticated() { Boolean authc = getTypedValue(AUTHENTICATED, Boolean.class); if (authc == null) { //see if there is an AuthenticationInfo object. If so, the very presence of one indicates a successful //authentication attempt: AuthenticationInfo info = getAuthenticationInfo(); authc = info != null; } if (!authc) { //fall back to a session check: Session session = resolveSession(); if (session != null) { Boolean sessionAuthc = (Boolean) session.getAttribute(AUTHENTICATED_SESSION_KEY); authc = sessionAuthc != null && sessionAuthc; } } return authc; }
4> In this way, the authentication information is obtained from the Session, and then the Subject is created and put into the ThreadLocal object.
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal call org.apache.shiro.subject.support.DelegatingSubject#sessionStopped. Will call: org.apache.shiro.subject.support.SubjectCallable#call
public V call() throws Exception { try { threadState.bind(); return doCall(this.callable); } finally { threadState.restore(); } }
org.apache.shiro.subject.support.SubjectThreadState#bind refers to the ThreadLocal related to the binding thread:
public void bind() { SecurityManager securityManager = this.securityManager; if ( securityManager == null ) { //try just in case the constructor didn't find one at the time: securityManager = ThreadContext.getSecurityManager(); } this.originalResources = ThreadContext.getResources(); ThreadContext.remove(); ThreadContext.bind(this.subject); if (securityManager != null) { ThreadContext.bind(securityManager); } }