简单研究下对于需要登陆的请求,后端的处理逻辑。
1. 前提条件
继续访问/test 接口,按照我们下面的配置,该请求会被authc 过滤器拦截到。
/** * 路径 -> 过滤器名称1[参数1,参数2,参数3...],过滤器名称2[参数1,参数2...]... * 自定义配置(前面是路径, 后面是具体的过滤器名称加参数,多个用逗号进行分割,过滤器参数也多个之间也是用逗号分割)) * 有的过滤器不需要参数,比如anon, authc, shiro 在解析的时候接默认解析一个数组为 [name, null] */ FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // 测试地址 FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[系统管理员,用户管理员],perms['user:manager:*']"); FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // 所有资源都需要经过验证
前提条件回顾
1. org.apache.shiro.spring.web.ShiroFilterFactoryBean.SpringShiroFilter 注册到Spring 中,默认是拦截所有请求。
2. /test 请求进入该拦截器。
3. 调用 org.apache.shiro.web.servlet.OncePerRequestFilter#doFilter
4. 调用org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal。
5. 调用org.apache.shiro.web.servlet.AbstractShiroFilter#executeChain
6. 调用org.apache.shiro.web.servlet.AbstractShiroFilter#getExecutionChain 生成代理FilterChain (逻辑是根据请求的URI, 和自己配置的路径进行正则匹配,如果满足条件,则获取到匹配到路径对应的NameFilterList, 然后生成ProxiedFilterChain(该代理对象内部包含NameFilterList和原来的FApplicationFilterChain))
7. 调用代理FilterChain 的doFilter方法。 该代理对象的方法会先走NameFilterList 的doFilter,然后走ApplicationFilterChain 的doFilter 方法。
2. org.apache.shiro.web.filter.authc.FormAuthenticationFilter 原理
上面根据路径获取到的代理FilterChain 如下:
1. ProxiedFilterChai.doFilter 方法会调用到FormAuthenticationFilter .doFilter 方法。该方法是父类方法: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. 继续调用到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); } }
这里面可分为三步:
第一步:this.preHandle(request, response); 前置处理
第二步:如果前置处理返回true,则调用this.executeChain(request, response, chain); 链条继续执行
第三步:this.postHandle(request, response); 后置处理
3. 前置处理逻辑:
调用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) 这一步默认都是true,也就是会进下面的onPreHandle 方法。 如果返回false,那么直接返回去。进行下一个链条执行。
2》 调用到org.apache.shiro.web.filter.AccessControlFilter#onPreHandle: 这里逻辑理解为逻辑与运算,如果某一个返回true 则返回true, 那么执行下一个链条。也就是如果允许访问或者拒绝访问都会走下一个链条。只有当isAccessAllowed 返回false, 并且onAccessDenied 也返回false, 才会跳过链条。
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return this.isAccessAllowed(request, response, mappedValue) || this.onAccessDenied(request, response, mappedValue); }
这里又有两步:
第一步: 调用isAccessAllowed 判断请求是否允许访问(mappedValue 是配置的请求的参数)。如果已经认证或者不是登陆地址,并且配置的参数包含permissive 则允许访问
会调用到: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 继续调用到:org.apache.shiro.web.filter.authc.AuthenticationFilter#isAccessAllowed 判断请求是否已经授权(关于认证之后如何进行标记之后研究)
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = this.getSubject(request, response); return subject.isAuthenticated() && subject.getPrincipal() != null; }
getSubject 实际调org.apache.shiro.SecurityUtils#getSubject:(实际是从ThreadLocal 中获取对象)
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
- 判断是否是登陆地址:org.apache.shiro.web.filter.AccessControlFilter#isLoginRequest
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) { return this.pathsMatch(this.getLoginUrl(), request); }
第二步:onAccessDenied 判断是否拒绝访问
- 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); } }
首先判断是否是登陆请求:
1》如果是:
判断是否是提交登陆请求,返回执行登陆请求;如果不是则返回true。返回true 的话链条就可以继续执行。执行登录也比较简单:
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); } }
org.apache.shiro.web.filter.authc.FormAuthenticationFilter#createToken 创建Token :(实际就是以默认的username 和 password 为参数去request 获取参数)
public static final String DEFAULT_USERNAME_PARAM = "username"; public static final String DEFAULT_PASSWORD_PARAM = "password"; public static final String DEFAULT_REMEMBER_ME_PARAM = "rememberMe"; protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { String username = getUsername(request); String password = getPassword(request); return createToken(username, password, request, response); } protected String getUsername(ServletRequest request) { return WebUtils.getCleanParam(request, getUsernameParam()); } protected String getPassword(ServletRequest request) { return WebUtils.getCleanParam(request, getPasswordParam()); }
2》如果不是:
调用saveRequestAndRedirectToLogin(request, response); 然后返回false, 返回false,那么请求链条不会继续执行。
org.apache.shiro.web.filter.AccessControlFilter#saveRequestAndRedirectToLogin 就是保存请求并且重定向到登陆地址
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 工具类如下:
/* * 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); } }
至此完成了未登录访问的拦截,并且重定向到登陆地址。 以及对登陆地址的放行。
3. 登陆原理
查看登陆是如何从Controller 调用到realm的,以及是如何维护登陆状态的。
1. 前置修改
1. 增加登陆地址
@GetMapping("/login2") public String login2() { Subject subject = SecurityUtils.getSubject(); AuthenticationToken generateToken = new UsernamePasswordToken("zs", "111222"); subject.login(generateToken); return "success"; }
2. ShiroConfig 配置该地址允许匿名访问
/** * 路径 -> 过滤器名称1[参数1,参数2,参数3...],过滤器名称2[参数1,参数2...]... * 自定义配置(前面是路径, 后面是具体的过滤器名称加参数,多个用逗号进行分割,过滤器参数也多个之间也是用逗号分割)) * 有的过滤器不需要参数,比如anon, authc, shiro 在解析的时候接默认解析一个数组为 [name, null] */ FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // 测试地址 FILTER_CHAIN_DEFINITION_MAP.put("/login2", "anon"); // 登陆地址 FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[系统管理员,用户管理员],perms['user:manager:*']"); FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // 所有资源都需要经过验证
3. 修改自定义realm 认证方法
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { User user = new User(); user.setPassword("111222"); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName()); }
2. 测试登陆
1. 访问测试地址 /login2
2. 首先经过shiro的anon 过滤器会放行,然后进入后面的controller 方法。
3. org.apache.shiro.SecurityUtils#getSubject 获取subject, 实际也就是从ThreadLocal 中获取
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Subject.Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
4. 调用org.apache.shiro.subject.support.DelegatingSubject#login 进行认证流程如下:
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; } }
核心都在securityManager.login(this, token) 调用内部, 传递subject 对象和 usernamePasswordToken。 认证成功之后会验证一些 信息并保存到当前对象, 也就是将当前subject 标记为已经认证。
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) 调用org.apache.shiro.mgt.AuthenticatingSecurityManager#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》继续调用: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》继续调用到org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate: (这里实际就是将请求转交给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); } }
从这里可以看出可以支持多种realm 认证方式。 这里研究单realm 认证方式。 多realm 认证之后研究。
3》继续调用org.apache.shiro.authc.pam.ModularRealmAuthenticator#doSingleRealmAuthentication:
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; }
- 首先调用org.apache.shiro.realm.AuthenticatingRealm#supports 判断是否支持该token (也就是判断是否是类型相匹配)
public boolean supports(AuthenticationToken token) { return token != null && getAuthenticationTokenClass().isAssignableFrom(token.getClass()); }
- 调用realm.getAuthenticationInfo(token); 进行获取认证信息, 如果获取的微null, 那么抛出UnknownAccountException(msg); 异常
4》 继续研究获取认证信息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; }
- 首先根据 org.apache.shiro.authc.UsernamePasswordToken#getPrincipal 也就是根据用户的唯一身份标识(内部是getUsername()), 获取缓存。 如果获取到直接验证密码; 获取不到就调realm 进行获取
- 调用realm 获取认证信息,这里调用到: com.zd.bx.config.shiro.CustomRealm#doGetAuthenticationInfo(也就是自己的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) 构造如下:
public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) { // 身份信息 this.principals = new SimplePrincipalCollection(principal, realmName); // 凭证信息,可以理解为密码(用户数据库的秘密,不是token 里面的密码) this.credentials = credentials; }
org.apache.shiro.subject.SimplePrincipalCollection#SimplePrincipalCollection(java.lang.Object, java.lang.String) 相当于缓存相关细腻些:
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; }
- 获取到认证信息之后缓存起来。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); } }
核心逻辑是如果开启缓存,就根据token 生成缓存的key(默认就是根据username 生成唯一key), 然后缓存起来。
- 如果认证信息不为null, 进行身份凭证信息匹配,也就是验证密码
org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch 验证凭证信息, 不匹配的话就抛出异常
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."); } }
继续调用到:org.apache.shiro.authc.credential.SimpleCredentialsMatcher#doCredentialsMatch 实际就是根据token 的凭证信息和认证信息的凭证信息进行匹配。
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) 创建Subject
1》调用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); }
继续调用: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; }
- 继续调用org.apache.shiro.web.mgt.DefaultWebSubjectFactory#createSubject 创建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); }
- org.apache.shiro.mgt.DefaultSecurityManager#save保存subject
protected void save(Subject subject) { this.subjectDAO.save(subject); }
继续调用: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#mergePrincipals: 保存身份信息到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 保存认证信息到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) 当前subject 记录principals 登录存的身份信息、登陆成功状态、登陆主机信息等然后结束login 方法
6. 上面登录完成之后我们再访问另一个请求查看其如何维护登录状态,subject 如何获取登录状态
(1) 第一个创建Subject 并且记录到ThreadLocal 是在org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal
(2) 继续调用org.apache.shiro.web.servlet.AbstractShiroFilter#createSubject
protected WebSubject createSubject(ServletRequest request, ServletResponse response) { return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject(); }
(3) 最后会调用到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; }
重要方法:
1》 org.apache.shiro.mgt.DefaultSecurityManager#resolveSession 解析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; }
最终解析到的session 如下: (可以看到有关于登录后的相关信息)
2》org.apache.shiro.mgt.DefaultSecurityManager#resolvePrincipals 解析登录后的身份信息
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; }
接续调用:org.apache.shiro.subject.support.DefaultSubjectContext#resolvePrincipals(可以看到有从session 中拿登录用户的信息,所以可以拿到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》 调用org.apache.shiro.web.mgt.DefaultWebSubjectFactory#createSubject 创建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); }
获取属性然后创建Subject,是否认证属性 authenticated 调用org.apache.shiro.subject.support.DefaultSubjectContext#resolveAuthenticated 解析
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》这样就从Session 中拿到认证的信息,然后创建Subject 之后放到ThreadLocal 对象中。
org.apache.shiro.web.servlet.AbstractShiroFilter#doFilterInternal 调用 org.apache.shiro.subject.support.DelegatingSubject#sessionStopped。 会调用到: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 就是绑定线程相关到ThreadLocal:
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); } }