• shiro源码(三)-认证过滤器原理


      简单研究下对于需要登陆的请求,后端的处理逻辑。

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

      至此完成了未登录访问的拦截,并且重定向到登陆地址。 以及对登陆地址的放行。

    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);
            }
        }
    【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
  • 相关阅读:
    delphi TMS FlexCel 导出PDF
    delphi TMS FlexCel Sheet工作表信息
    delphi TMS FlexCel 预览Excel
    delphi TMS FlexCel Sheet工作表新增复制
    delphi TMS FlexCel 打印Excel
    delphi TMS FlexCel 保存Excel
    delphi TMS FlexCel Sheet工作表选择和查找
    【算法】字典树
    【算法】拓扑排序
    【算法】单调栈
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/15456027.html
Copyright © 2020-2023  润新知