• Shiro源码分析


    1.入口类:AbstractAuthenticator

    用户输入的登录信息经过其authenticate方法:

    public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    if (token == null) {
    throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
    } else {
    log.trace("Authentication attempt received for token [{}]", token);

    AuthenticationInfo info;
    try {
    info = this.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 var8) {
    AuthenticationException ae = null;
    if (var8 instanceof AuthenticationException) {
    ae = (AuthenticationException)var8;
    }

    if (ae == null) {
    String msg = "Authentication failed for token submission [" + token + "]. Possible unexpected " + "error? (Typical or expected login exceptions should extend from AuthenticationException).";
    ae = new AuthenticationException(msg, var8);
    if (log.isWarnEnabled()) {
    log.warn(msg, var8);
    }
    }

    try {
    this.notifyFailure(token, ae);
    } catch (Throwable var7) {
    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, var7);
    }
    }

    throw ae;
    }

    log.debug("Authentication successful for token [{}]. Returned account [{}]", token, info);
    this.notifySuccess(token, info);
    return info;
    }
    }

    其中的token包含用户输入的登录信息,如果是用户名/密码登录,这里是UsernamePasswordToken,其属性:username(这里测试账号是admin)、password(这里是111111)、rememberMe记住我,前台勾选,默认false)、host

    2.在上面的方法里,进入内层doAuthenticate方法(传入上面的用户登录token),这个方法是子类ModularRealmAuthenticator实现的方法(1中是模板设计模式,抽象父类声明,子类实现):

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    this.assertRealmsConfigured();
    Collection<Realm> realms = this.getRealms();
    return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
    }

    这里是根据认证类型选择单Realm还是多Realm认证,进入不同的方法。这里进入前者,即单Realm认证

    无论那种认证,这里最终返回一个AuthenticationInfo实例,为   类型

    3.在上面的方法里,进入doSingleRealmAuthentication方法,传入了Realm和上面的用户登录token,其中Realm是Realm链中的一个,是我们自定义的一个Realm:

    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);
    } else {
    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);
    } else {
    return info;
    }
    }
    }

    从这里开始,我们要进入自定义Realm的逻辑。这里,我们自定义的Realm完整如下:

    /**
     * Copyright 2018-2020 stylefeng & fengshuonan (https://gitee.com/stylefeng)
     * <p>
     * Licensed 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
     * <p>
     * http://www.apache.org/licenses/LICENSE-2.0
     * <p>
     * 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 cn.stylefeng.guns.core.shiro;
    
    import cn.stylefeng.guns.core.shiro.service.UserAuthService;
    import cn.stylefeng.guns.core.shiro.service.impl.UserAuthServiceServiceImpl;
    import cn.stylefeng.guns.modular.system.model.User;
    import cn.stylefeng.roses.core.util.ToolUtil;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.authc.credential.CredentialsMatcher;
    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    public class ShiroDbRealm extends AuthorizingRealm {
    
        /**
         * 登录认证
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
                throws AuthenticationException {
            UserAuthService shiroFactory = UserAuthServiceServiceImpl.me();
            UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
            User user = shiroFactory.user(token.getUsername());
            ShiroUser shiroUser = shiroFactory.shiroUser(user);
            return shiroFactory.info(shiroUser, user, super.getName());
        }
    
        /**
         * 权限认证
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            UserAuthService shiroFactory = UserAuthServiceServiceImpl.me();
            ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal();
            List<Integer> roleList = shiroUser.getRoleList();
    
            Set<String> permissionSet = new HashSet<>();
            Set<String> roleNameSet = new HashSet<>();
    
            for (Integer roleId : roleList) {
                List<String> permissions = shiroFactory.findPermissionsByRoleId(roleId);
                if (permissions != null) {
                    for (String permission : permissions) {
                        if (ToolUtil.isNotEmpty(permission)) {
                            permissionSet.add(permission);
                        }
                    }
                }
                String roleName = shiroFactory.findRoleNameByRoleId(roleId);
                roleNameSet.add(roleName);
            }
    
            SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
            info.addStringPermissions(permissionSet);
            info.addRoles(roleNameSet);
            return info;
        }
    
        /**
         * 设置认证加密方式
         */
        @Override
        public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
            HashedCredentialsMatcher md5CredentialsMatcher = new HashedCredentialsMatcher();
            md5CredentialsMatcher.setHashAlgorithmName(ShiroKit.hashAlgorithmName);
            md5CredentialsMatcher.setHashIterations(ShiroKit.hashIterations);
            super.setCredentialsMatcher(md5CredentialsMatcher);
        }
    }

    4.在3中的方法里,继续传入token,进入我们自定义的RealmgetAuthenticationInfo方法里,这又是一个模板方法实现在父类AuthenticatingRealm中,AuthenticatingRealm是AuthorizingRealm的父类,而我们的自定义Realm继承AuthorizingRealm

    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info = this.getCachedAuthenticationInfo(token);//这是从缓存获取,这里为null
    if (info == null) {
    info = this.doGetAuthenticationInfo(token);
    log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
    if (token != null && info != null) {
    this.cacheAuthenticationInfoIfPossible(token, info);//这是放入缓存
    }
    } else {
    log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
    }

    if (info != null) {
    this.assertCredentialsMatch(token, info);
    } else {
    log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
    }

    return info;
    }

    这里先从缓存获取认证信息,这里为null,获取不到则调用自定义RealmdoGetAuthenticationInfo方法获取认证信息(模板方法模式),传入的仍然是上面的用户登录token,这样又进入到了我们的自定义Realm方法实现中:

        /**
         * 登录认证
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken)
                throws AuthenticationException {
            UserAuthService shiroFactory = UserAuthServiceServiceImpl.me();
            UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
            User user = shiroFactory.user(token.getUsername());
            ShiroUser shiroUser = shiroFactory.shiroUser(user);
            return shiroFactory.info(shiroUser, user, super.getName());
        }

    我们的Realm获取认证信息的方式:

    a.从Spring容器获取我们自定义实现的业务类,该类注入了数据库用户操作的Mapper(继承了MyBatis-Plus的BaseMapper接口)

    b.使用自定义业务类的数据库Mapper,根据用户登录token中的用户名到数据库获取数据库对应的用户信息

    c.根据用户表roleId字段,关联查询用户对应角色(集合)信息

    d.调用了业务类自定义的如下方法:

        @Override
        public SimpleAuthenticationInfo info(ShiroUser shiroUser, User user, String realmName) {
            String credentials = user.getPassword();
    
            // 密码加盐处理
            String source = user.getSalt();//数据库存储原始盐值
            ByteSource credentialsSalt = new Md5Hash(source);//转换后使用的哈希盐值
            return new SimpleAuthenticationInfo(shiroUser, credentials, credentialsSalt, realmName);
        }

    这里使用了盐值加密,获取了该用户数据库存储的原始盐值,调用Shiro框架Md5Hash方法获取了哈希盐值

    使用数据库用户信息(Object类型,这里是自定义ShiroUser)、数据库存储的哈希后的密码哈希盐值realmName(自定义Realm全路径名加上一个并发登录的原子整形自增值,这里是自定义Realm直接调用的super.getName())生成了一个Shiro框架要求的一个SimpleAuthenticationInfo对象返回

    5.自定义Realm执行结束,向外回到4的后面部分继续执行,调用了下面方法:

    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
    CredentialsMatcher cm = this.getCredentialsMatcher();
    if (cm != null) {
    if (!cm.doCredentialsMatch(token, info)) {
    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.");
    }
    }

    传入的是用户登录token和我们上面使用自定义Realm获取数据库用户信息,并进行封装SimpleAuthenticationInfo实例(接口为AuthenticationInfo )

    注意这里获取到的CredentialsMatcher是我们上面自定义的Realm中的第三个@Override方法setCredentialsMatcher设置进去的,是可以自定义设置的,设置了哈希算法(这里为MD5)哈希迭代次数(这里为1024),这里为HashedCredentialsMatcher实例。

    6.上面方法中的if判断里,又进入下面方法:

    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenHashedCredentials = this.hashProvidedCredentials(token, info);
    Object accountCredentials = this.getCredentials(info);
    return this.equals(tokenHashedCredentials, accountCredentials);
    }

    实现类是HashedCredentialsMatcher,这里面的三个方法:

    hashProvidedCredentials:最终通过

    new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);

    获取一个SimpleHash(接口是Hash)实例。

    其中参数分别为哈希算法(这里是MD5)、登录token中用户输入的原始密码(字符数组格式,这里是[1,1,1,1,1,1])、哈希盐值哈希迭代次数(这里设置成了1024),这几个参数都是自定义可配。这里最终获取的是用户输入密码通过哈希算法、哈希盐值生成的哈希密码信息

    getCredentials:最终获取的是用户数据库哈希密码信息

    equals:比较上述两个信息

    最终返回到doCredentialsMatch,又返回到5中的assertCredentialsMatch方法。如果验证失败,返回IncorrectCredentialsException异常,提示(密码凭证不匹配)信息,否则验证成功,接着4中自定义RealmgetAuthenticationInfo方法逻辑返回从数据库获取、封装的SimpleAuthenticationInfo实例。

    7.可以看到上面3以下的几步是层层深入到Realm里面的处理逻辑(有父类的有自定义子类的,也有调用的HashedCredentialsMatcher工具类等),现在开始走出Realm的逻辑,回到3的后半段,仍然是AbstractAuthenticator的实现类ModularRealmAuthenticatordoSingleRealmAuthentication方法:

    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);
    } else {
    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);
    } else {
    return info;
    }
    }
    }

    返回自定义Realm返回SimpleAuthenticationInfo实例,继而到外面的doAuthenticate方法:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    this.assertRealmsConfigured();
    Collection<Realm> realms = this.getRealms();
    return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
    }

    继续返回自定义Realm返回SimpleAuthenticationInfo实例,最终回到抽象类AbstractAuthenticatorauthenticate方法,即我们开头的方法。

    最终返回的就是自定义Realm返回AuthenticationInfo的实现类SimpleAuthenticationInfo的实例,实例包括用户自定义ShiroUser信息(principals,可有多个自定义用户信息实体)、哈希密码(credentials)、哈希盐值(credentialsSalt)信息。

    8.最终返回到的是外层的类AuthenticatingSecurityManager,继承了RealmSecurityManager,最终实现的是SecurityManager.这里是调用了AuthenticatingSecurityManager的下面方法:

    public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    return this.authenticator.authenticate(token);
    }

    返回的就是上面几步获取认证AuthenticationInfo。这个方法又是模板方法,继而向更外层返回到子类DefaultSecurityManagerlogin方法:

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
    AuthenticationInfo info;
    try {
    info = this.authenticate(token);
    } catch (AuthenticationException var7) {
    AuthenticationException ae = var7;

    try {
    this.onFailedLogin(token, ae, subject);
    } catch (Exception var6) {
    if (log.isInfoEnabled()) {
    log.info("onFailedLogin method threw an exception. Logging and propagating original AuthenticationException.", var6);
    }
    }

    throw var7;
    }

    Subject loggedIn = this.createSubject(token, info, subject);
    this.onSuccessfulLogin(token, info, loggedIn);
    return loggedIn;
    }

    这里传入的参数Subject里面有request信息token仍然是用户输入登录信息(这里是用户名、密码),上面的所有过程并没有Subject参与,而是token.最终登录成功后,是使用token,登录成功返回的AuthenticationInfo,和上面这个包含request信息的Subject,封装了另外一个Subject

    protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) {
    SubjectContext context = this.createSubjectContext();
    context.setAuthenticated(true);
    context.setAuthenticationToken(token);
    context.setAuthenticationInfo(info);
    if (existing != null) {
    context.setSubject(existing);
    }

    return this.createSubject(context);
    }

    这个方法使用SubjectContext封装认证成功信息,再把带request的Subject设置其中,最后使用这个context调用下面方法:

    public Subject createSubject(SubjectContext subjectContext) {
    SubjectContext context = this.copy(subjectContext);
    context = this.ensureSecurityManager(context);
    context = this.resolveSession(context);
    context = this.resolvePrincipals(context);
    Subject subject = this.doCreateSubject(context);
    this.save(subject);
    return subject;
    }

    这里的前三个方法最终都是获取DefaultSubjectContext(它是的SubjectContext子类)对应的SecurityManager,Session,Principals组件进行确认

    最后的doCreateSubject方法最终调用到类DefaultWebSubjectFactory(父类是DefaultSubjectFactory,实现了SubjectFactory接口)的下面方法:

    public Subject createSubject(SubjectContext context) {
    if (!(context instanceof WebSubjectContext)) {
    return super.createSubject(context);
    } else {
    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);
    }
    }

    这里充分说明我们封装的SubjectContextWeb环境上下文,包含了request信息认证成功后的信息Session等信息。这里重新拿到这些信息,最终用这些信息生成了一个WebDelegatingSubject实例进行返回,它是一个代理类,最终实现了Subject接口。

    回到上面的doCreateSubject方法,进而回到createSubject方法,返回的就是这个WebDelegatingSubject实例:

    public Subject createSubject(SubjectContext subjectContext) {
    SubjectContext context = this.copy(subjectContext);
    context = this.ensureSecurityManager(context);
    context = this.resolveSession(context);
    context = this.resolvePrincipals(context);
    Subject subject = this.doCreateSubject(context);
    this.save(subject);
    return subject;
    }

    继续走save方法,这里将包含已认证用户信息的Subject放入Session中

    向外继续回到

    createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing)

    方法,返回的是这个WebDelegatingSubject实例。

    向外继续返回到DefaultSecurityManagerlogin方法剩余部分:

    Subject loggedIn = this.createSubject(token, info, subject);
    this.onSuccessfulLogin(token, info, loggedIn);
    return loggedIn;

    onSuccessfulLogin方法是处理rememberMe相关信息。

    最终返回到:

    9.DelegatingSubject(实现了Subject接口)的login方法:

    public void login(AuthenticationToken token) throws AuthenticationException {
    this.clearRunAsIdentitiesInternal();
    Subject subject = this.securityManager.login(this, token);
    String host = null;
    PrincipalCollection principals;
    if (subject instanceof DelegatingSubject) {
    DelegatingSubject delegating = (DelegatingSubject)subject;
    principals = delegating.principals;
    host = delegating.host;
    } else {
    principals = subject.getPrincipals();
    }

    if (principals != null && !principals.isEmpty()) {
    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 = this.decorate(session);
    } else {
    this.session = null;
    }

    } else {
    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);
    }
    }

    看来SecurityManager的login方法是这里调用的,返回了认证成功的WebDelegatingSubject实例后,这里继续向下执行就是DelegatingSubject实例的一些简单的赋值逻辑了,也就是把上面返回的认证成功的WebDelegatingSubject实例信息给到这个DelegatingSubject

    10.之后这个DelegatingSubject实例返回给我们自定义的@Controller类。原来我们是在自定义的@Controller类中调用了Shiro

    SecurityUtils.getSubject();

    来获取的9中DelegatingSubject的Web实例WebDelegatingSubject,执行了上面的login操作。

    此后我们自定义的@Controller类的逻辑就是使用返回的WebDelegatingSubject中的用户认证信息设置Session,最终重定向到主页

    登录Controller完整的登录方法如下:

        /**
         * 点击登录执行的动作
         */
        @RequestMapping(value = "/login", method = RequestMethod.POST)
        public String loginVali() {
    
            String username = super.getPara("username").trim();
            String password = super.getPara("password").trim();
            String remember = super.getPara("remember");
    
            //验证验证码是否正确
            if (KaptchaUtil.getKaptchaOnOff()) {
                String kaptcha = super.getPara("kaptcha").trim();
                String code = (String) super.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
                if (ToolUtil.isEmpty(kaptcha) || !kaptcha.equalsIgnoreCase(code)) {
                    throw new InvalidKaptchaException();
                }
            }
    
            Subject currentUser = ShiroKit.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(username, password.toCharArray());
    
            if ("on".equals(remember)) {
                token.setRememberMe(true);
            } else {
                token.setRememberMe(false);
            }
    
            currentUser.login(token);
    
            ShiroUser shiroUser = ShiroKit.getUser();
            super.getSession().setAttribute("shiroUser", shiroUser);
            super.getSession().setAttribute("username", shiroUser.getAccount());
    
            LogManager.me().executeLog(LogTaskFactory.loginLog(shiroUser.getId(), getIp()));
    
            ShiroKit.getSession().setAttribute("sessionFlag", true);
    
            return REDIRECT + "/";
        }

    可以看到我们自己使用用户传入的登录信息封装了UsernamePasswordToken,并使用它来进行了Shiro的登录流程。

    进一步研究提示

    1.SecurityUtils.getSubject()原理(和配置的Shiro怎样整合、获取Web环境及request有关)。

    2.Session配置和获取相关,分布式Session问题。

    3.rememberMe原理和配置。

    补充:Shiro集成到Web(Tomcat、SpringMVC、Spring、Spring Boot)

    1.请求不同的Servlet,其过滤器链可能不同(每个过滤器配置了过滤规则),所以每次Web容器使用createFilterChain方法为每个Servlet调用创建FilterChain(Java原生,这里实例为ApplicationFilterChain),包含该Servlet每个配置的Filter(Java原生),形成责任链调用(这里集成SpringMVC,所以Servlet类型为DispatcherServlet,即SpringMVC的前端控制器),即从一个FilterMap里匹配出一个和当前请求url对应的Filter集合加入到创建的FilterChain当中

    2.执行ApplicationFilterChaindoFilter方法,里面是对每个Filter的链式调用:迭代每个Filter,执行其doFilter方法,这个方法的内部逻辑是:还没执行的就执行其内部的doFilterInterval方法,在这个方法里使用FilterChaindoFilter返回FilterChain;执行完的直接执行FilterChain的doFilter返回FilterChain。这样不断回到FilterChain继续迭代Filter(Filter数组游标++并判断是否到头),直到执行完最后一个Filter,其调用的FilterChain的doFilter方法进入else,结束,返回到这个Filter,再返回到调用它的FilterChain的doFilter方法,再返回到上一个Filter...这样反向层层返回到Filter和FilterChain的doFilter方法,直到整个责任链在最外层的ApplicationFilterChaindoFilter方法返回

    3.其中责任链走到SpringMVCDelegatingFilterProxy时,这个代理Filter代理了一个名为delegatingFilterProxy的类,这就是在Spring中配置的ShiroFilter,这里在DelegatingFilterProxy的下面方法中:

    protected void invokeDelegate(Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    delegate.doFilter(request, response, filterChain);
    }

    走到ShiroAbstractShiroFilter,在其doFilterInternal方法里,执行:

    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {
    Throwable t = null;

    try {
    final ServletRequest request = this.prepareServletRequest(servletRequest, servletResponse, chain);
    final ServletResponse response = this.prepareServletResponse(request, servletResponse, chain);
    Subject subject = this.createSubject(request, response);
    subject.execute(new Callable() {
    public Object call() throws Exception {
    AbstractShiroFilter.this.updateSessionLastAccessTime(request, response);
    AbstractShiroFilter.this.executeChain(request, response, chain);
    return null;
    }
    });
    } catch (ExecutionException var8) {
    t = var8.getCause();
    } catch (Throwable var9) {
    t = var9;
    }

    if (t != null) {
    if (t instanceof ServletException) {
    throw (ServletException)t;
    } else if (t instanceof IOException) {
    throw (IOException)t;
    } else {
    String msg = "Filtered request failed.";
    throw new ServletException(msg, t);
    }
    }
    }

    这里的request类型变换为ShiroHttpServletRequest,调用的createSubject方法如下:

    protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
    return (new Builder(this.getSecurityManager(), request, response)).buildWebSubject();
    }

    这里获取到配置的SecurityManager配置了一个WebDelegatingSubject返回,这个类原理流程可以参照上文的源码分析

    4.接着executeChain方法内开始走Shiro内部配置的Filter(实际是又创建了一个ProxiedFilterChain进行内部转发,最后回到Web容器创建的ApplicationFilterChain),这里走我们配置的自定义的OAuth2Filter,该类继承了AuthenticatingFilter,下面是走该类的一系列父类:

    AdviceFilterdoFilterInternal方法,它是PathMatchingFilter的父类,在这个方法里接着走PathMatchingFilterpreHandle方法(模板方法模式),进行迭代路径匹配,这个路径是我们在Shiro中自定义配置的拦截路径,比如配置为"anon"的路径不需要认证就可访问,其他均需走oauth2,也就是下面我们以此名字配置的子类OAuth2Filter,配置如下:

    @Bean("shiroFilter")//2.用来初始化DelegatingFilterProxy来向Web容器注册Shiro的Filter,拦截所有请求
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);//设置了SecurityManager
    
        //oauth过滤
        Map<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", new OAuth2Filter());//3.Shiro级过滤器,拦截带token的请求,获取请求token,封装到OAuth2Token(实现了AuthenticationToken),继续传递给Realm处理接口
        shiroFilter.setFilters(filters);
    
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/app/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/v2/api-docs", "anon");
        filterMap.put("/swagger-ui.html", "anon");
        filterMap.put("/swagger-resources/**", "anon");
        filterMap.put("/captcha.jpg", "anon");//以上匿名访问
        filterMap.put("/**", "oauth2");//4.所有其他,转到oauth2认证,名字和上面配置的oauth2的filter名字匹配
        shiroFilter.setFilterChainDefinitionMap(filterMap);
    
        return shiroFilter;
    }

    这里如果不是anon管理的路径,则在preHandle方法里面,继续走我们的OAuth2Filter的父类AccessControlFilteronPreHandle方法,它继承了PathMatchingFilter:

    public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    return this.isAccessAllowed(request, response, mappedValue) || this.onAccessDenied(request, response, mappedValue);
    }

    这个方法里走的两个方法,则是我们OAuth2Filter覆盖的两个方法,执行前一个返回true则不再执行后一个,否则执行后一个,我们定义的OAuth2Filter完整如下:

    package io.renren.modules.sys.oauth2;
    
    import com.google.gson.Gson;
    import io.renren.common.utils.HttpContextUtils;
    import io.renren.common.utils.R;
    import org.apache.commons.lang.StringUtils;
    import org.apache.http.HttpStatus;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import javax.servlet.ServletRequest;
    import javax.servlet.ServletResponse;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * oauth2过滤器
     *
     * @author chenshun
     * @email sunlightcs@gmail.com
     * @date 2017-05-20 13:00
     */
    public class OAuth2Filter extends AuthenticatingFilter {
    
        @Override
        protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
            //获取请求token
            String token = getRequestToken((HttpServletRequest) request);
    
            if(StringUtils.isBlank(token)){
                return null;
            }
    
            return new OAuth2Token(token);
        }
    
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
                return true;
            }
    
            return false;
        }
    
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            //获取请求token,如果token不存在,直接返回401
            String token = getRequestToken((HttpServletRequest) request);
            if(StringUtils.isBlank(token)){
                HttpServletResponse httpResponse = (HttpServletResponse) response;
                httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
                httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
    
                String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));
    
                httpResponse.getWriter().print(json);
    
                return false;
            }
    
            return executeLogin(request, response);
        }
    
        @Override
        protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setContentType("application/json;charset=utf-8");
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
            try {
                //处理登录失败的异常
                Throwable throwable = e.getCause() == null ? e : e.getCause();
                R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());
    
                String json = new Gson().toJson(r);
                httpResponse.getWriter().print(json);
            } catch (IOException e1) {
    
            }
    
            return false;
        }
    
        /**
         * 获取请求的token
         */
        private String getRequestToken(HttpServletRequest httpRequest){
            //从header中获取token
            String token = httpRequest.getHeader("token");
    
            //如果header中不存在token,则从参数中获取token
            if(StringUtils.isBlank(token)){
                token = httpRequest.getParameter("token");
            }
    
            return token;
        }
    
    
    }

    这里如果没登录过,都是走后一个方法,即onAccessDenied方法,这里是走我们自定义的onAccessDenied方法,该方法最后调用了executeLogin方法,表明任何未认证请求都被导向登录逻辑,该方法是父类AuthenticatingFilter的方法:

    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    AuthenticationToken token = this.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);
    } else {
    try {
    Subject subject = this.getSubject(request, response);
    subject.login(token);
    return this.onLoginSuccess(token, subject, request, response);
    } catch (AuthenticationException var5) {
    return this.onLoginFailure(token, var5, request, response);
    }
    }
    }

    这个方法里走的就是Shiro的登录逻辑,使用的是我们自定义的Shiro配置,其中Realm这里走的是我们自定义的OAuth2Realm,这个逻辑最上面已经分析过。

    这样最后走出executeLogin方法,再走出我们的onAccessDenied方法,这个方法是上面AccessControlFilteronPreHandle方法调用的。

    5.这样4中顺着onPreHandle的返回层层向上返回,最后又返回到3中,随着executeChain方法的返回,返回到ShiroAbstractShiroFilterdoFilterInternal方法后半段,最后按这个Filter前面的Filter责任链层层向外返回,逻辑结束。

    待查:重定向跳转问题,Session问题,认证成功的token有效期配置和有效期内免登录逻辑原理。

  • 相关阅读:
    flex布局
    redis持久化的四种方式
    list all index in elasticsearch
    Java Thread停止关闭
    关于线程的一些操作方法
    将redis key打印到文本
    spout和bolt
    java读取redis的timeout异常
    storm中,ack与fail
    好文要收藏(大数据)
  • 原文地址:https://www.cnblogs.com/free-wings/p/9816431.html
Copyright © 2020-2023  润新知