zoukankan      html  css  js  c++  java
  • 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);
            }
        }
    【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
  • 相关阅读:
    大话设计模式:外观模式
    大话设计模式:零篇-目录总结
    大话设计模式:观察者模式
    Spring MVC自动为对象注入枚举数据
    使用idea工具开发webservice
    HTTP协议详解
    mysql 数据库 exists 和count
    eclipse运行maven的jetty插件内存溢出
    400,404,500报错页面总结
    Mac系统下Eclipse代码联想功能(代码助手,代码提示)快捷键
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/15456027.html
Copyright © 2011-2022 走看看