【万字长文】微服务整合Shiro+Jwt,源码分析鉴权实战

前言

Shiro是什么,我这里就不多介绍了,全网也有好多是介绍Spring BootShiroJwt整合的教程,我想要写这篇文章,是因为有好多的内容,基本上都是类似的,自己想要的效果,我并没有找到合适的解决方案。

整合之后,我想要的一个效果是:

  1. 支持RBAC
  2. 通过JWT生成token,做到无状态
  3. 能够动态的根据用户角色对当前请求路径进行鉴权,而不是使用Shiro提供的注解,把角色,权限等信息写死
  4. 对Shiro的异常进行统一捕获和处理

源码分析

我之前并没有学过Shiro,现在工作中又需要做权限这一块,就快速的学了一下Shiro,后面的分析,我因为是应届生,可能还存在很多考虑不周到的地方和Bug,欢迎各位大佬指出,我也再完善一下。如果你要学Shiro,并不想看官方文档的话,可以试试这篇教程,我个人感觉是非常仔细的,推荐搭配源码一起看。

组件介绍

想要上手Shiro,我们必须要明白Shiro中的几个概念:

  1. Subject(主题):当前"用户"是谁,这个"用户"并不是我们现实世界中的人类,你可以这样理解,向Shiro发起操作的就是一个"用户",比如一个网络请求,他就是一个Subject。

    java 复制代码
    // 从当前线程上下文中获取一个subject
    Subject subject = SecurityUtils.getSubject();
    subject.hasRole("admin");// 此subject是否有admin角色
    subject.hasRole(xxx);
    subject.isPermitted(xxx);
  2. SecurityManager(安全管理器):管理系统中所有"Subject",是Shiro中的核心类,该类下面有很多的方法,但是这些方法基本上和Subject对象中的方法是一样的,假如我们获取到Subject,想要验证此Subject是否有admin这个角色,调用subject.hasRole("admin"),但是其最终执行的是securityManager.hasRole(getPrincipals(), "admin")

    java 复制代码
    // 获取安全管理器
    SecurityManager securityManager = SecurityUtils.getSecurityManager();
    securityManager.hasRole(xxx, xxx);
  3. Authenticator(身份认证):验证用户身份,该类只有一个方法authenticate(),该方法需要返回一个AuthenticationInfo对象,此对象中就包含用户的用户名和密码(数据库中存储的密码,并不是表单中密码)

  4. CredentialsMatcher(凭证匹配器):也可以叫做密码匹配器,调用subject.login()方法,从Authenticator中获取到用户名,密码,在CredentialsMatcher中进行密码的比较。

  5. Authorizer(授权):进行鉴权操作,验证某个用户是否具有某某权限/角色

    java 复制代码
    boolean hasRole(PrincipalCollection subjectPrincipal, String roleIdentifier);
    boolean isPermitted(PrincipalCollection subjectPrincipal, Permission permission);
    ...
  6. Realm:Shiro和数据之间的桥梁,我们的用户数据可以存放在本地、数据库、Redis、第三方等等,Shiro他并不关心,也不需要知道它所需要的数据是以什么方式存储,它关心的仅仅是,我(Shiro)需要用户的账号和密码,或者用户的权限信息,你就必须给我。他们之间就是通过Realm来进行数据通信的,在一个Security Manager,可以有多个Realm,对于存在多个Realm的情况,我们可以定义策略(AuthenticationStrategy),来决定如何处理。

  7. Session Management(Session管理器):如果使用Jwt,这个基本上用不到,主要作用就是保存session

  8. Cache(缓存):也就是获取用户信息或者权限信息等,可以从缓存中获取,Shiro中的缓存全部都是org.apache.shiro.cache.Cache对象

上面就是我们需要了解的一些概念,下面我将对上面这些内容进行源码分析。源码分析的时候,我们只需要关注两个方法,分别是SecurityUtils.getSubject().login()SecurityUtils.getSubject().hasRole()(使用hasRole举例,其他的方法一样)

调用SecurityUtils.getSubject().login()

  1. 调用SecurityUtils.getSubject().login(),该login方法需要传入一个AuthenticationToken类型的参数,对象里面包含用户名和密码

    java 复制代码
    public void login(AuthenticationToken token) throws AuthenticationException {
        // 1. 先从session中移除属性
        clearRunAsIdentitiesInternal();
        // 2. 核心方法
        Subject subject = securityManager.login(this, token);
        PrincipalCollection principals;
        // 剩余的代码不需要太关注
    }

DefaultSecurityManager

  1. 我们进入 securityManager.login(this, token)方法,该方法只有一个实现,位置是org.apache.shiro.mgt.DefaultSecurityManager#login

    java 复制代码
    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            // 通过AuthenticationToken对象信息,获取用户名和密码
            info = authenticate(token);
        } catch (AuthenticationException ae) {
            try {
                // 处理rememberMe
                onFailedLogin(token, ae, subject);
            } catch (Exception e) {
                // ...
            }
            throw ae; //propagate
        }
    	// 处理rememberMe
        onSuccessfulLogin(token, info, loggedIn);
    }

AuthenticatingSecurityManager

  1. 进入org.apache.shiro.mgt.AuthenticatingSecurityManager#authenticate方法

    java 复制代码
    public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }

    我们可以看到,这里最终是在SecurityManager对象中调用this.authenticator.authenticate(token),而这个方法的最终目的就是对用户传入的用户名和密码进行验证,那如果想要自定义这个认证流程的话,就只需要在securityManager对象中设置Authenticator就行了

    java 复制代码
    @Bean("securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setAuthenticator(xxx);
        return securityManager;
    }

AbstractAuthenticator

  1. 继续往下,this.authenticator.authenticate(token)会进入到org.apache.shiro.authc.AbstractAuthenticator#authenticate

    java 复制代码
    public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
    	// ....
        AuthenticationInfo info;
        try {
            info = doAuthenticate(token);
    	// ....
        } catch (Throwable t) {
            	// ....
        }
    
        notifySuccess(token, info);
    	return info;
    }

    我先说最后的notifySuccess(token, info)方法,该方法就是类似于通知监听器的作用,监听器是AuthenticationListener类型,有三个方法,onSuccess(登录成功后执行什么)onFailure(登录失败执行什么)onLogout(登出成功执行),该监听器存放于AbstractAuthenticator类中的,该类只有一个子类ModularRealmAuthenticator,如果我们需要设置监听器,可以参照下面方式

    java 复制代码
    @Bean("securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
     	// 设置认证器,ModularRealmAuthenticator是一个支持多Realm的认证器
        securityManager.setAuthenticator(modularRealmAuthenticator());
        return securityManager;
    }
    
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator() {
        ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
      	// 设置监听器
        modularRealmAuthenticator.setAuthenticationListeners(Collection<AuthenticationListener>/**自己实现AuthenticationListener**/);
        return modularRealmAuthenticator;
    }

    notifySuccess(token, info)方法完了,我们继续回到流程中的info = doAuthenticate(token),最终会调用org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate,请注意了,ModularRealmAuthenticator这个类非常重要,他是一个支持多Realm的认证器,正常我们在SecurityManager中都将Authenticator设置为ModularRealmAuthenticator,他的doAuthenticate方法为

    java 复制代码
    protected Collection<Realm> getRealms() {
        return this.realms;
    }
    protected void assertRealmsConfigured() throws IllegalStateException {
        Collection<Realm> realms = getRealms();
        if (CollectionUtils.isEmpty(realms)) {
            throw new IllegalStateException(msg);
        }
    }
    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);
        }
    }

    可以看到,在doAuthenticate()方法中,首先会判断当前认证器中有没有Realm对象,因为之前说过了,Shiro想要知道用户信息,就必须要有Realm这个中间件,就算数据存储在本地的也不行,我们设置Realm可以在SecurityManagerModularRealmAuthenticator中都可以设置,效果是一样的。

    doSingleRealmAuthentication()doMultiRealmAuthentication()分别是对单个Realm和多个Realm进行处理

    先看doSingleRealmAuthentication(realms.iterator().next(), authenticationToken)方法,此方法是针对SecurityManager中只存在于一个Realm的情况,其源码如下

    java 复制代码
    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if (!realm.supports(token)) {
            // ............
        }
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
           // ............
        }
        return info;
    }

    为了看懂上面的代码,我们需要先看一下Realm这个接口

    java 复制代码
    public interface Realm {
        // Returns the (application-unique) name assigned to this Realm. All realms configured for a single application must have a unique name.(返回该realm对象的名字,在同一个SecurityManager下,此realmName必须唯一)
        String getName();
    
        // Returns true if this realm wishes to authenticate the Subject represented by the given AuthenticationToken instance, false otherwise. (此realm是否支持对传入的AuthenticationToken进行账号密码认证)
        boolean supports(AuthenticationToken token);
    
        // Returns an account's authentication-specific information for the specified token, or null if no account could be found based on the token. (根据传入的AuthenticationToken信息,返回其对应的AuthenticationInfo)
        AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
    
    }
    
    public interface AuthenticationInfo extends Serializable {
    	// 主体信息,如用户名等用户的标识,请注意,密码不放在此对象中
        PrincipalCollection getPrincipals()
        // getPrincipals()方法中的主体凭据,可以理解为主体的密码
        Object getCredentials();
    
    }

    因为在登录的时候,我们调用的是subject.login(AuthenticationToken),但是对于每一个应用而言,其登录逻辑是不同的,我们需要的AuthenticationToken信息也是不同的,在正常的业务中,我们一般都是需要自定义我们自己的AuthenticationToken

    java 复制代码
    @Data
    public class JwtTokenAuthenticationToken implements AuthenticationToken {
        // 账户名
        private String account;
        // 账户密码
        private String password;
    
        public JwtTokenAuthenticationToken(String account, String password) {
            this.account = account;
            this.password = password;
        }
    
        public JwtTokenAuthenticationToken() {
        }
    
        @Override
        public Object getPrincipal() {
            return this.account;
        }
    
        @Override
        public Object getCredentials() {
            return this.password;
        }
    }

    然后我们在自定义的Realm中,对supports方法进行重写,用于判断传入的AuthenticationToken是否是JwtTokenAuthenticationToken类型,从而从AuthenticationToken中获取到账户名和密码

    java 复制代码
    public abstract class AbstractAuthenticationRealm extends AuthorizingRealm {
    
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtTokenAuthenticationToken;
        }
    }

    现在重新回到org.apache.shiro.authc.pam.ModularRealmAuthenticator#doSingleRealmAuthentication方法,可以看到其获取AuthenticationInfo对象,就是调用realmgetAuthenticationInfo进行获取,我们点击进入该方法,在讲解该getAuthenticationInfo方法之前,我再说一下Realm

    我们目前已经了解到Realm是Shiro和数据之间的桥梁,对于一个安全框架来说,获取用户信息和用户权限是必不可少的,但是在Realm接口中,我们只看到了获取用户信息的方法,也就是org.apache.shiro.realm.Realm#getAuthenticationInfo,但是并没有获取用户权限信息的方法,这个是因为获取用户的权限信息是在Realm的实现类中去完成的,我们看一下Realm接口的子类关系图

    在上图中,我们可以看到,有两个非常重要的类,一个是AuthenticatingRealm,还有一个是AuthorizingRealm,我们现在看一下上图中主要类的源码

Realm

  1. Realm接口我们已经分析过了,这里就不看了,其里面获取用户信息的方法为org.apache.shiro.realm.Realm#getAuthenticationInfo

CachingRealm

  1. CachingRealm,该类比Realm多了三个能自定义的属性,还有几个方法
java 复制代码
public abstract class CachingRealm implements Realm, Nameable, CacheManagerAware, LogoutAware {
  // 此realm的名称    
    private String name;
    // 是否为此realm启用缓存
    private boolean cachingEnabled;
    // 缓存管理器
    private CacheManager cacheManager;
    protected void afterCacheManagerSet() {}
    protected void clearCache(PrincipalCollection principals) {}
    protected void doClearCache(PrincipalCollection principals) {}
}

// 缓存管理器就一个方法,通过var1(理解为key)获取Cache对象,我们可以自己实现,比如定义一个RedisCacheManager,还需要实现Cache接口
public interface CacheManager {
    <K, V> Cache<K, V> getCache(String var1) throws CacheException;
}

如果要为我们的realm增加缓存支持的话,可以在配置中进行指定

java 复制代码
@PostConstruct
public void init() {
    // AdminReam继承自AuthorizingRealm
    // 想要开启AuthenticationInfo缓存,必须要设置下面这几个属性
    adminRealm.setAuthenticationCachingEnabled(true);
    adminRealm.setCachingEnabled(true);
    adminRealm.setCacheManager(CacheManager);
    // 设置AuthenticationInfo对象缓存的名字,比如redis中的key
    adminRealm.setAuthenticationCacheName("shiro:AuthenticationInfoCache:Name");
}

@Bean("securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 这里后续可以增加多个realm
    Collection<Realm> realmCollection = new ArrayList<>();
    realmCollection.add(adminRealm);
    securityManager.setRealms(realmCollection);
    return securityManager;
}

AuthenticatingRealm

  1. AuthenticatingRealm:在此类中,我们可以配置凭据匹配器CredentialsMatcher,是否开启身份验证缓存authenticationCachingEnabled,还有身份验证缓存的名字authenticationCacheName,还有几个重要的方法,主要源码如下
java 复制代码
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable {
  // 凭据匹配器,也就是如何进行密码验证
    private CredentialsMatcher credentialsMatcher;
    private Cache<Object, AuthenticationInfo> authenticationCache;
    // 是否开启身份验证缓存
    private boolean authenticationCachingEnabled;
    private String authenticationCacheName;

    // 判断是否能从缓存中获取AuthenticationInfo对象,判断的方法是isAuthenticationCachingEnabled(),逻辑是1.开启身份验证缓存authenticationCachingEnabled(true),2.为realm设置了cachingEnabled(true)
    private Cache<Object, AuthenticationInfo> getAvailableAuthenticationCache() {
        Cache<Object, AuthenticationInfo> cache = getAuthenticationCache();
        boolean authcCachingEnabled = isAuthenticationCachingEnabled();
        if (cache == null && authcCachingEnabled) {
            cache = getAuthenticationCacheLazy();
        }
        return cache;
    }

    // 懒加载缓存
    private Cache<Object, AuthenticationInfo> getAuthenticationCacheLazy() {
        if (this.authenticationCache == null) {
            // 获取缓存管理器
            CacheManager cacheManager = getCacheManager();
            if (cacheManager != null) {
                // 获取key
                String cacheName = getAuthenticationCacheName();
                // 从缓存管理器中获取缓存对象
                this.authenticationCache = cacheManager.getCache(cacheName);
            }
        }
        return this.authenticationCache;
    }

    // 从缓存中获取当前用户的身份信息
    private AuthenticationInfo getCachedAuthenticationInfo(AuthenticationToken token) {
        AuthenticationInfo info = null;
      // 从缓存获取
        Cache<Object, AuthenticationInfo> cache = getAvailableAuthenticationCache();
        if (cache != null && token != null) {
            // 获取用户信息
            Object key = getAuthenticationCacheKey(token);
            info = cache.get(key);
        }
        return info;
    }

    // 获取用户信息,此方法非常重要
    public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
      // 1. 先从缓存中获取用户身份信息
        AuthenticationInfo info = getCachedAuthenticationInfo(token);
        if (info == null) {
            // 如果缓存中没有,则使用我们自己的逻辑去获取,比如从MySQL,或者是本地等等,doGetAuthenticationInfo()是抽象方法
            info = doGetAuthenticationInfo(token);
        }
        if (info != null) {
            // 能获取到用户信息的话,就进行用户密码的验证
            assertCredentialsMatch(token, info);
        }
        return info;
    }

    // 判断用户凭据(密码)是否正确
    protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
        // 获取密码匹配器,正常业务中,需要自定义,然后在Shiro配置中,对realm进行设置
        CredentialsMatcher cm = getCredentialsMatcher();
        if (cm != null) {
            // 执行密码验证
            if (!cm.doCredentialsMatch(token, info)) {}
        }
    }
  // 如何获取用户信息,由子类去自己实现
    protected abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
}

在上面这个类中,最重要的方法莫过于org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo,我们可以理解为,他是我们从realm中获取用户信息的入口方法

AuthorizingRealm

  1. AuthorizingRealm:此类很重要,我们看该类的继承和实现关系,就可以看到,它实现了Authorizer,所以就有hasRole()等方法,还继承自AuthenticaticatingRealm,该类中,有5个属性我们可以进行设置,authorizationCachingEnabled是否开启授权缓存,authorizationCacheName授权缓存的名字,permissionResolver权限解析器,permissionRoleResolver角色解析器,这个

注意了,能够从缓存中获取AuthorizationInfo(用户权限角色信息),其逻辑和AuthenticatingRealm是一样的,你必须开启设置authorizationCachingEnabledcachingEnabled两个字段的值为true,才能开启

从realm中获取用户权限信息的逻辑和从realm中获取用户身份信息的逻辑是差不多的,其方法是org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo,我们可以看一下其源码

java 复制代码
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
    AuthorizationInfo info = null;
    // 1. 从缓存中获取
    Cache<Object, AuthorizationInfo> cache = getAvailableAuthorizationCache();
    if (cache != null) {
        // 2. 从缓存中根据用户名获取用户权限信息
        Object key = getAuthorizationCacheKey(principals);
        info = cache.get(key);
    }
    if (info == null) {
        // 3. 缓存中没有,则从数据库或者是本地获取,需要用户自己实现,doGetAuthorizationInfo()是抽象方法
        info = doGetAuthorizationInfo(principals);
        if (info != null && cache != null) {
            // 4. 如果启用了缓存,则放入缓存中
            Object key = getAuthorizationCacheKey(principals);
            cache.put(key, info);
        }
    }
    return info;
}

最后,如果你需要自定义一个realm,我推荐大家继承AuthorizingRealm,因为这个类,既能获取用户身份信息,又能获取用户权限信息,关于subject.hasRole()等鉴权相关的分析,我放在下一节,这一节就主要看身份验证

ModularRealmAuthenticator

  1. 现在我们已经看完了所需要了解的源码了,回到我们之前的开始的部分org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate方法

    java 复制代码
    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);
        }
    }
    protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
        if (!realm.supports(token)) {
            // ..........
            throw new UnsupportedTokenException(msg);
        }
        AuthenticationInfo info = realm.getAuthenticationInfo(token);
        if (info == null) {
            // ........
            throw new UnknownAccountException(msg);
        }
        return info;
    }
    
    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
        AuthenticationStrategy strategy = getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        for (Realm realm : realms) {
            try {
                aggregate = strategy.beforeAttempt(realm, token, aggregate);
            } catch (ShortCircuitIterationException shortCircuitSignal) {
                // Break from continuing with subsequnet realms on receiving 
                // short circuit signal from strategy
                break;
            }
            if (realm.supports(token)) {
                AuthenticationInfo info = null;
                Throwable t = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (Throwable throwable) {
                   
                }
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
            }
        }
        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }

    如果当前的securityManager中只存在一个realm的话,那么会走doSingleRealmAuthentication(Realm realm, AuthenticationToken token)流程,在该方法中,就是从我们自定义的realm中通过用户名获取到用户身份信息,而且我们可以看到,它首先是先执行realm.supports(token),只有此realm支持此AuthenticationToken参数,其才会进入到getAuthenticationInfo,这部分比较简单,我们重点看一下doMultiRealmAuthentication(realms, authenticationToken)方法

SecurityUtils.getSubject().hasRole()

执行鉴权流程,它的源码我们基本上已经在SecurityUtils.getSubject().login()中分析过了,这里就不重复了,我就只说一下调用流程,还有部分类的源码分析。

  1. org.apache.shiro.subject.support.DelegatingSubject#hasRole

  2. 下一个流程,根据你配置的Authenticator来走

    1. 如果你直接配置的认证器是AuthorizingRealm类型,那么下一步会进入你自己的那个org.apache.shiro.realm.AuthorizingRealm#hasRole(org.apache.shiro.subject.PrincipalCollection, java.lang.String)方法

      java 复制代码
      @Bean("securityManager")
      public DefaultWebSecurityManager defaultWebSecurityManager() {
          DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
          securityManager.setAuthenticator(Authenticator);
          return securityManager;
      }
    2. 因为上面1的情况只针对于SecurityManager下只存在于一个Realm的情况,更多时候,我们业务中,一般都会有多个Realm,像这种情况的话,我们就需要将认证器设置成ModularRealmAuthorizer类型,下一步执行的是org.apache.shiro.authz.ModularRealmAuthorizer#hasRole

      java 复制代码
      @Bean("securityManager")
      public DefaultWebSecurityManager defaultWebSecurityManager() {
          DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
          // 设置设置验证器,无论设置什么,最终都会执行我们的Realm
          securityManager.setAuthenticator(modularRealmAuthenticator());
          return securityManager;
      }
      
      /**
       * 系统自带的Realm管理,主要针对多realm
       */
      @Bean
      public ModularRealmAuthenticator modularRealmAuthenticator() {
          ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
          FirstSuccessfulStrategy firstSuccessfulStrategy = new FirstSuccessfulStrategy();
          firstSuccessfulStrategy.setStopAfterFirstSuccess(true);
          modularRealmAuthenticator.setAuthenticationStrategy(firstSuccessfulStrategy);
          modularRealmAuthenticator.setAuthenticationListeners();
          modularRealmAuthenticator.setRealms();
          return modularRealmAuthenticator;
      }

      而且其流程就是,遍历每一个realm,如果该realmAuthorizingRealm类型的话,那么就调用org.apache.shiro.realm.AuthorizingRealm#hasRole(org.apache.shiro.subject.PrincipalCollection, java.lang.String),只要有一个realm能够调用hasRole()后,返回true,就表示鉴权成功,并不像多Realm情况下,login()登录那么复杂,需要设计到策略。

    java 复制代码
    public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
        AuthorizationInfo info = getAuthorizationInfo(principal);
        return hasRole(roleIdentifier, info);
    }

hasRole()

后面的如何获取用户的权限信息,我上面已经分析过了,我们现在来分析一下hasRole()这个方法

java 复制代码
protected boolean hasRole(String roleIdentifier, AuthorizationInfo info) {
    return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
}

其逻辑也是很好理解,就是从AuthorizationInfo中获取角色列表,判断用户的所有角色中,是否包含了roleIdentifier

hasAllRoles()

java 复制代码
protected boolean[] hasRoles(List<String> roleIdentifiers, AuthorizationInfo info) {
    boolean[] result;
    if (roleIdentifiers != null && !roleIdentifiers.isEmpty()) {
        int size = roleIdentifiers.size();
        result = new boolean[size];
        int i = 0;
        for (String roleName : roleIdentifiers) {
            result[i++] = hasRole(roleName, info);
        }
    } else {
        result = new boolean[0];
    }
    return result;
}

最终都是调用hasRole()方法

isPermitted()

该方法位置在org.apache.shiro.realm.AuthorizingRealm#isPermitted(org.apache.shiro.subject.PrincipalCollection, java.lang.String),源码如下

java 复制代码
// 1. 将permission字符串转换成Permission类型
public boolean isPermitted(PrincipalCollection principals, String permission) {
    Permission p = getPermissionResolver().resolvePermission(permission);
    return isPermitted(principals, p);
}

// 2. 获取用户权限信息
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
    AuthorizationInfo info = getAuthorizationInfo(principals);
    return isPermitted(permission, info);
}

// 3. 鉴权
protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
    Collection<Permission> perms = getPermissions(info);
    if (perms != null && !perms.isEmpty()) {
        for (Permission perm : perms) {
            if (perm.implies(permission)) {
                return true;
            }
        }
    }
    return false;
}

对于第一步来说,首先需要先将传入的permission字符串,转换为Permission对象,转换逻辑就是从AuthorizingRealm中获取权限解析器,我们可以自定义一个,然后调用他的resolvePermission()方法,只需要最终返回一个Permission对象就行

java 复制代码
public class MyPermissionResolver implements PermissionResolver {
    @Override
    public Permission resolvePermission(String permissionString) {
        log.info("正在将{} permissionStr转换成Permission对象", permissionString);
        UserPermission permission = new UserPermission();
        permission.setPermissionStr(permissionString);
        return permission;
    }
}

Subject.hasRole()和SecurityManager.hasRole()方法区别

直接上源码

java 复制代码
// Subject.hasRole()
Subject subject = SecurityUtils.getSubject();
subject.hasRole("");

public boolean hasRole(String roleIdentifier) {
    return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
}
java 复制代码
// SecurityManager.hasRole()
SecurityManager securityManager = SecurityUtils.getSecurityManager();
securityManager.hasRole(PrincipalCollection subjectPrincipal, String roleIdentifier);

通过看源码,我们可以看到,Subject.hasRole()比SecurityManager.hasRole()多了一步hasPrincipals(),这个方法的作用就是从session中获取当前subject的PrincipalCollection信息,也就是如果你关闭了Session存储并且你调用Subject.hasRole(),那么你永远不会进入到真正的securityManager.hasRole()中去,因为关闭session存储后,调用subject.login()登录成功之后,并不会将PrincipalCollection保存到session中去,也就是说,如果你使用jwt或者是其他无状态token,那么你在调用shiro框架的hasRole()时,不要使用SecurityUtils.getSubject().hasRole(),而使用SecurityUtils.getSecurityManager().hasRole(),这里不注意看,是个坑,我就在这个坑里debug了好长时间

过滤器

Shiro中还有一个重要的组件就是过滤器(我不知道为何网上有一些会有一些翻译成拦截器,反正说的都是同一个东西),该拦截器的位置是org.apache.shiro.web.servlet.AbstractFilter,该类的继承关系如下

关于这些过滤器,我画了一张图,帮助大家理解每一个过滤器他的一个区别,新增哪些方法。

但是在开发过程中,我自定义的过滤器只继承了AccessControlFilter这个过滤器,你们也可以再往下继承,从上图中,最终的一个执行流程来看的画(假设我自定义过滤器继承自AccessControlFilter),那么其简略的执行顺序就是isAccessAllowed -> onAccessDenied,而且我们可以在AccessControlFilter类中看到其onPreHandle()方法的源码为:

java 复制代码
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

那么我们自定义的过滤器,对于请求接口鉴权这一块,是不是可以直接在isAccessAllowed()去实现了,如果鉴权成功,就返回true,如果鉴权失败的话,返回false或者更推荐大家直接抛出一个Shiro异常,方便后期对Shiro的异常进行统一处理。

统一异常处理

对于统一异常处理,我不知道有没有人和我一样,跳进了使用Spring Boot进行统一异常处理的坑,无论在isAccessAllowed()方法中抛出什么异常,都不会在@RestControllerAdvice被拦截,这部分的源码,我没去了解过,大家感兴趣的可以去源码中看看,最后我才想起来JavaWeb中的ServletResponse类,其可以调用response.getWriter()方法将字符串写出。

但是如果在我们自定义自定义的过滤器中,在需要抛出异常,或者是权限不足等地方,每次都调用response.getWriter().write()进行处理的话,是可以实现部分的"统一异常处理",但是对于Shiro框架中抛出的异常,我们还是没办法,这样也很不优雅,我们再来看AdviceFilter的源码:

java 复制代码
public abstract class AdviceFilter extends OncePerRequestFilter {
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        return true;
    }
    
    @SuppressWarnings({"UnusedDeclaration"})
    protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
    }
    
    @SuppressWarnings({"UnusedDeclaration"})
    public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
    }
    
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        Exception exception = null;
        try {
            boolean continueChain = preHandle(request, response);
            if (continueChain) {
                executeChain(request, response, chain);
            }
            postHandle(request, response);
        } catch (Exception e) {
            exception = e;
        } finally {
            cleanup(request, response, exception);
        }
    }

    protected void cleanup(ServletRequest request, ServletResponse response, Exception existing)
            throws ServletException, IOException {
        Exception exception = existing;
        try {
            afterCompletion(request, response, exception);
        } catch (Exception e) {
            if (exception == null) {
                exception = e;
            }
        }
        if (exception != null) {
            if (exception instanceof ServletException) {
                throw (ServletException) exception;
            } else if (exception instanceof IOException) {
                throw (IOException) exception;
            } else {
                throw new ServletException(exception);
            }
        }
    }
}

我们重点看doFilterInternal()方法和cleanup()方法,doFilterInternal()方法也算是我们Shiro中的入口方法,注意看该方法中的try-catch-finally块,我们可以看到,在Shiro过滤器的preHandle()postHandle()中产生的异常,最后都会在该方法中被捕获,并且最终会交给cleanup()方法进行处理,并且该方法参数中的Exception对象如果不为null的话,那么Shiro会直接将该异常抛出,那么如果我们在

实战

源码我们已经基本上分析过了,那么现在就开启Spring Boot、JWT、Shiro的整合,因为我项目使用的是微服务,考虑到一些业务上的区别(每个模块的鉴权可能不同),在Shiro这块,我是定义成一个Starter,哪些模块需要做鉴权,就自行引入这个starter,进行一些简单的配置,就可以了。自定义配置包括,自定义多Realm的策略,自定义Realm等。

在开始之前,我先说一下,整个项目的一个鉴权逻辑,权限部分使用RBAC模型,数据库中存放了能访问requestMethod:Uri接口的所有角色信息,在鉴权这块的话,首先是从jwt中获取用户的角色信息userRoles,然后获取当前请求的restful风格的请求路径,从数据库或者是缓存中,获取能访问该请求路径的所有角色roles,遍历roles,如果userRoles中有一个和roles中的某个角色相同,则表示用户拥有访问该接口的权限。

shiro-spring-boot-starter

其目录结构如下:

java 复制代码
├─src
│  └─main
│      ├─java
│      │  └─xyz
│      │      └─xcye
│      │          ├─authorizer
│      │          │      JwtModularRealmAuthorizer.java // 自定义ModularRealmAuthorizer
│      │          │
│      │          ├─config
│      │          │      ShiroAutoConfig.java // Shiro配置类
│      │          │
│      │          ├─entity
│      │          │      AuroraShiroProperties.java // 配置字段
│      │          │      JwtTokenAuthenticationToken.java 
│      │          │      JwtTokenAuthorizationInfo.java
│      │          │      UserInfo.java
│      │          │
│      │          ├─enums
│      │          │      RegexEnum.java
│      │          │
│      │          ├─factory
│      │          │      AuroraJwtSubjectFactory.java
│      │          │
│      │          ├─filter
│      │          │      AuroraHttpFilter.java
│      │          │
│      │          ├─realm
│      │          │      AbstractAuthenticationRealm.java
│      │          │
│      │          └─utils
│      │                  JsonUtil.java
│      │                  JwtUtil.java
│      │
│      └─resources
│          │  application.yaml
│          │
│          └─META-INF
│                  spring.factories

下面我就开始贴出每个文件的代码,有一些文件会解释为什么这么做。

authorizer

java 复制代码
public class JwtModularRealmAuthorizer extends ModularRealmAuthorizer {

    private static final Logger logger = LogManager.getLogger(JwtModularRealmAuthorizer.class.getName());

    @Override
    public boolean hasAllRoles(PrincipalCollection principals, Collection<String> roleIdentifiers) {
        assertRealmsConfigured();
        List<AbstractAuthenticationRealm> authorizingRealmList = new ArrayList<>();
        for (Realm realm : getRealms()) {
            if (realm instanceof AbstractAuthenticationRealm) {
                authorizingRealmList.add((AbstractAuthenticationRealm) realm);
            } else {
                if (logger.isDebugEnabled()) {
                    logger.warn("从shiro中获取到 {} ,其并不是 {} 类型,将被忽略", realm.getName(), AbstractAuthenticationRealm.class.getName());
                }
            }
        }
        for (AbstractAuthenticationRealm realm : authorizingRealmList) {
            AuthorizationInfo info = realm.getAuthorizationInfo(principals);
            if (realm.hasAllRoles(principals, info.getRoles())) {
                return true;
            }
        }
        return false;
    }
}

此类主要是为了重写hasAllRoles()方法,因为正常业务中,每个用户可能会存在多个角色,而且我们数据库中,就存放了能访问某条接口的所有角色信息,jwt也存在用户角色,但是Shiro框架中的hasRole和hasAllRoles等方法,都是进行一个角色一个角色的验证,每进行一次角色的验证,都会从数据库或者缓存中查询权限数据,和我们系统的逻辑不同,所以这里就需要重写hasAllRoles()方法,Collection<String> roleIdentifiers中存放了从jwt中获取的用户角色列表,在我们自己的AbstractAuthenticationRealm中,就只需要再次重写hasAllRoles方法,就可以一次性的对用户拥有的多个角色进行鉴权。

ShiroAutoConfig

java 复制代码
@EnableConfigurationProperties({AuroraShiroProperties.class})
@Configuration
public class ShiroAutoConfig {

    private static final Logger logger = LogManager.getLogger(ShiroAutoConfig.class.getName());
    @Autowired
    private List<AbstractAuthenticationRealm> authenticationRealmList;

    @Autowired
    private AuthenticationStrategy authenticationStrategy;

    @Autowired
    private AuroraHttpFilter auroraHttpFilter;

    @Autowired
    private AuroraJwtSubjectFactory auroraJwtSubjectFactory;

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @PostConstruct
    public void init() {
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        // 设置验证器,无论设置什么,最终都会执行我们的Realm
        securityManager.setAuthenticator(modularRealmAuthenticator());
        securityManager.setAuthorizer(modularRealmAuthorizer());

        // 这里后续可以增加多个realm
        if (authenticationRealmList == null || authenticationRealmList.isEmpty()) {
            throw new RuntimeException("当前容器中没有AbstractAuthenticationRealm类型的Bean");
        }
        List<Realm> realmList = new ArrayList<>(authenticationRealmList);
        securityManager.setRealms(realmList);

        // 解决多realm
        // securityManager.setAuthenticator();

        // 设置自己的AuthenticationInfo缓存管理器
        // securityManager.setCacheManager(myCacheManager);

        // 设置自己的rememberMe管理器,源码在org.apache.shiro.mgt.DefaultSecurityManager.resolvePrincipals
        // securityManager.setRememberMeManager();

        // 设置session管理器
        securityManager.setSessionManager(sessionManager);

        // 关闭shiro自带的subjectDao
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        // 关闭sessionStorageEnabled
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);

        subjectDAO.setSessionStorageEvaluator(evaluator);

        securityManager.setSubjectDAO(subjectDAO);

        securityManager.setSubjectFactory(auroraJwtSubjectFactory);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 将所有请求都通过AuroraHttpFilter
        Map<String, String> map = new HashMap<>();
        map.put("/**", "auroraFilter");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);

        // 配置自定义的BearerHttpAuthenticationFilter
        shiroFilterFactoryBean.getFilters().put("auroraFilter", auroraHttpFilter);

        return shiroFilterFactoryBean;
    }

    // 开启注解代理(默认好像已经开启,可以不要)
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * 系统自带的Realm管理,主要针对多realm
     */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator() {
        ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
        if (authenticationStrategy != null) {
            modularRealmAuthenticator.setAuthenticationStrategy(authenticationStrategy);
        }else {
            // 使用FirstSuccessfulStrategy
            FirstSuccessfulStrategy firstSuccessfulStrategy = new FirstSuccessfulStrategy();
            firstSuccessfulStrategy.setStopAfterFirstSuccess(true);
            modularRealmAuthenticator.setAuthenticationStrategy(firstSuccessfulStrategy);
            logger.warn("你未设置AuthenticationStrategy,将使用 {}", firstSuccessfulStrategy.getClass().getSimpleName());
        }
        return modularRealmAuthenticator;
    }

    @Bean
    public ModularRealmAuthorizer modularRealmAuthorizer() {
        return new JwtModularRealmAuthorizer();
    }

    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(false);
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }
}

AuroraShiroProperties

java 复制代码
@Data
@ConfigurationProperties(prefix = AuroraShiroProperties.AURORA_SHIRO_PREFIX)
public class AuroraShiroProperties {
    public static final String AURORA_SHIRO_PREFIX = "aurora.shiro";

    /**
     * 登录地址
     */
    private String loginUrl;

    /**
     * 登录请求方法
     */
    private String loginRequestMethod;

    /**
     * restful风格的白名单列表,此列表中的url在拦截器中将被忽略
     */
    private List<String> restfulWhiteUriList;

    /**
     * redis中存储角色权限关系的key值
     */
    private String redisRolePermissionCacheName;

    /**
     * redis中存储用户权限关系的key值
     */
    private String redisUserPermissionCacheName;

    /**
     * 是否过滤静态文件
     */
    private Boolean ignoreStaticFiles;

    /**
     * 超级管理员的角色名
     */
    private String superAdministratorRoleName;

    /**
     * 在该模块下,哪些角色是作为管理员存在  TODO 后期为了便于维护,可以使用字典进行维护
     */
    private List<String> administratorRoleNameList;
}

JwtTokenAuthenticationToken

java 复制代码
/**
 * @author xcye
 * @description 执行登录时候,传入subject.login()参数中的对象
 * @date 2023-07-24 14:04:27
 */

@Data
public class JwtTokenAuthenticationToken implements AuthenticationToken {
    // 账户名
    private String account;
    // 账户密码
    private String password;

    public JwtTokenAuthenticationToken(String account, String password) {
        this.account = account;
        this.password = password;
    }

    public JwtTokenAuthenticationToken() {
    }

    @Override
    public Object getPrincipal() {
        return this.account;
    }

    @Override
    public Object getCredentials() {
        return this.password;
    }
}

JwtTokenAuthorizationInfo

java 复制代码
public class JwtTokenAuthorizationInfo implements AuthorizationInfo {

    @Setter
    private Collection<String> roleList;

    @Setter
    private Collection<String> stringPermissions;

    @Setter
    private Collection<Permission> permissions;

    @Override
    public Collection<String> getRoles() {
        return roleList;
    }

    @Override
    public Collection<String> getStringPermissions() {
        return stringPermissions;
    }

    @Override
    public Collection<Permission> getObjectPermissions() {
        return permissions;
    }
}

UserInfo

java 复制代码
/**
 * @author xcye
 * @description 生成jwtToken以及解析jwtToken时,使用到的用户信息
 * @date 2023-07-24 11:34:23
 */

@Data
public class UserInfo {

    /**
     * 用户id
     */
    private String userId;

    /**
     * 用户账户名
     */
    private String account;

    /**
     * 用户昵称
     */
    private String nickName;

    /**
     * 用户角色
     */
    private List<String> roleList;

    /**
     * 用户标识 后端自己在字典中维护
     */
    private Integer userTag;

    /**
     * 是否是管理员
     */
    private Boolean isAdministrator;
}

RegexEnum

java 复制代码
/**
 * 正则表达式的枚举,存放所有需要的正则表达式
 *
 * @author qsyyke
 */

public enum RegexEnum {
    REST_FUL_PATH("^(GET|DELETE|POST|PUT):/[*a-z0-9A-Z/_-]*");

    /**
     * 正则表达式
     */
    private final String regex;

    private RegexEnum(String regex) {
        this.regex = regex;
    }

    public String getRegex() {
        return regex;
    }
}

AuroraJwtSubjectFactory

java 复制代码
@Component
public class AuroraJwtSubjectFactory extends DefaultWebSubjectFactory {
    @Override
    public Subject createSubject(SubjectContext context) {
        // 不创建session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }

}

AuroraHttpFilter

java 复制代码
/**
 * @author xcye
 * @description 这是shiro的拦截器
 * @date 2023-07-24 13:37:11
 */

@Component
public class AuroraHttpFilter extends AccessControlFilter {

    private static final Logger logger = LogManager.getLogger(AuroraHttpFilter.class.getName());

    @Autowired
    private AuroraShiroProperties auroraShiroProperties;

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        String requestMethod = httpServletRequest.getMethod();
        String requestURI = httpServletRequest.getRequestURI();

        // 如果是option方法,跳过
        if ("OPTIONS".equalsIgnoreCase(requestMethod)) {
            return true;
        }

        // 将请求方法和uri构造成形如 GET:/shiro/login的形式
        String restFulUri = requestMethod + ":" + requestURI;
        logger.info("{} 请求进入", restFulUri);

        // 如果过滤静态文件,则直接跳过
        if (Objects.equals(auroraShiroProperties.getIgnoreStaticFiles(), Boolean.TRUE)) {
            List<String> staticUriList = Arrays.asList("**:/**/*.html", "**:/**/*.js", "**:/**/*.css", "**:/**/*.ico");
            for (String staticUri : staticUriList) {
                if (pathMatcher.matches(staticUri, restFulUri)) {
                    return true;
                }
            }
        }

        // 判断当前请求是否在白名单中
        for (String whiteUri : auroraShiroProperties.getRestfulWhiteUriList()) {
            if (pathMatcher.matches(whiteUri, restFulUri)) {
                return true;
            }
        }

        // 执行到这里,说明不是白名单uri,需要进行身份验证,从请求头中获取token
        String authorizationToken = httpServletRequest.getHeader(HttpConstant.AUTHORIZATION_HEADER);
        UserInfo userInfo = null;
        if (!StringUtils.hasLength(authorizationToken) || (userInfo = JwtUtil.parseJWT(authorizationToken)) == null) {
            throw new ExpiredCredentialsException("登录状态失效");
        }

        // 执行到这里,说明token有效 验证用户是否拥有访问此uri的权限
        SimplePrincipalCollection principalCollection = new SimplePrincipalCollection();
        principalCollection.add(userInfo.getAccount(), principalCollection.getClass().getName());
        boolean hasRoleStatus = SecurityUtils.getSecurityManager().hasAllRoles(principalCollection, userInfo.getRoleList());
        if (!hasRoleStatus) {
            throw new UnauthorizedException("权限不足");
        }
        return true;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        return false;
    }

    @Override
    protected void cleanup(ServletRequest request, ServletResponse response, Exception existing) throws ServletException, IOException {
        if (existing != null) {
            logger.error(existing.getMessage());
            if (existing instanceof ExpiredCredentialsException) {
                handleAccessDenied(response, ResultEnum.IDINVALID.getCode(), ResultEnum.IDINVALID.getMessage(), existing);
            } else if (existing instanceof AccountException) {
                handleAccessDenied(response, ResultEnum.PASSWORDNOTEMPTY.getCode(), existing.getMessage(), existing);
            } else if (existing instanceof AuthorizationException) {
                handleAccessDenied(response, ResultEnum.PERMISSIONDENIED.getCode(), existing.getMessage(), existing);
            } else if (existing instanceof ShiroException) {
                handleAccessDenied(response, ResultEnum.PROGRAMERROR.getCode(), ResultEnum.PROGRAMERROR.getMessage(), existing);
            }
        }
        super.cleanup(request, response, existing);
    }

    private void handleAccessDenied(ServletResponse response, int code, String message, Exception exception) {
        exception = null;
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setStatus(HttpServletResponse.SC_OK);
        R r = R.failure(code, message);
        String resultStr = ConvertObjectUtils.jsonToString(r);
        PrintWriter writer = null;
        try {
            response.setCharacterEncoding("UTF-8");
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            writer = response.getWriter();
            writer.write(resultStr);
        } catch (IOException ex) {
            logger.error(ex.getMessage());
            throw new RuntimeException(ex.getMessage());
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
}

cleanup方法中中,如果我们调用super.cleanup(request, response, existing)时,传入父类方法的existing异常不为null的话,在父类中,也还是会抛出这个异常,就会出现500或者其他的白页。如果这个异常为null的话,Shiro就不会进行处理,但是在该方法中,我们只需要对shiro相关的异常进行处理,如果不是shiro产生的异常,我们还是需要shiro正常抛出,方便框架统一进行处理,上面的cleanup(),我们只是对shiro相关的异常进行处理。

AbstractAuthenticationRealm

java 复制代码
@Component
public abstract class AbstractAuthenticationRealm extends AuthorizingRealm {

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtTokenAuthenticationToken;
    }

    /**
     * @param principal       the application-specific subject/user identifier.
     * @param roleIdentifiers 能访问此Method:uri的角色集合,并不是用户的角色集合,用户所拥有的角色集合是通过jwt进行获取的
     * @return true表示验证用户角色成功
     */
    @Override
    public boolean hasAllRoles(PrincipalCollection principal, Collection<String> roleIdentifiers) {
        return judgePermission(principal, roleIdentifiers);
    }

    public AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
        return super.getAuthorizationInfo(principals);
    }

    /**
     * 判断某个用户是否含有roleIdentifiers中的角色信息,在任意地方调用subject.haveAllRole()方法将执行此方法
     *
     * @param principal       含有用户账户名的对象
     * @param roleIdentifiers 含有用户角色
     * @return true表示验证用户角色成功
     */
    protected abstract boolean judgePermission(PrincipalCollection principal, Collection<String> roleIdentifiers);
}

在该类中,我重写了hasAllRoles()方法,调用SecurityManager.hasAllRoles()方法时,能够走我们自己的处理逻辑。

JwtUtil

java 复制代码
/**
 * @author xcye
 * @description 和jwt相关的工具类,生成jwt以及解析jwt
 * @date 2023-07-24 11:28:15
 */

public class JwtUtil {

    private static final Logger logger = LogManager.getLogger(JwtUtil.class.getName());

    // token过期时间 一年
    private static final long EXPIRE_TIME = 1000 * 60 * 60 * 24;
    private static final String signature = "自己设置";

    /**
     * 根据用户信息,生成jwtToken
     *
     * @param userInfo 包含角色,账户等的用户信息,nickName,roleList可以为null
     * @return jwtToken字符串
     */
    public static String sign(UserInfo userInfo) {
        // 过期时间
        Date expirationTime = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(signature);
        Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

        JwtBuilder builder = Jwts.builder().setId(userInfo.getUserId())
                .claim("account", userInfo.getAccount())
                .claim("roleList", userInfo.getRoleList())
                .claim("userId", userInfo.getUserId())
                .claim("nickName", userInfo.getNickName())
                .claim("userTag", userInfo.getUserTag())
                .claim("isAdministrator", userInfo.getIsAdministrator())
                .setSubject("user")
                .setExpiration(expirationTime)
                .signWith(signatureAlgorithm, signingKey);
        return builder.compact();
    }

    /**
     * 从jwt中获取用户信息
     *
     * @param jwt Jwt字符串
     * @return 用户信息,解析失败,返回null
     */
    public static UserInfo parseJWT(String jwt) {
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(signature))
                    .parseClaimsJws(jwt)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
        UserInfo userInfo = new UserInfo();
        userInfo.setUserId(claims.getId());
        userInfo.setAccount(claims.get("account", String.class));
        userInfo.setNickName(claims.get("nickName", String.class));
        userInfo.setRoleList(claims.get("roleList", List.class));
        userInfo.setUserTag(claims.get("userTag", Integer.class));
        userInfo.setIsAdministrator(claims.get("isAdministrator", Boolean.class));
        return userInfo;
    }

    /**
     * 是否失效
     *
     * @param expiration
     * @return
     */
    public static boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }

    public static void main(String[] args) {
        UserInfo userInfo = new UserInfo();
        userInfo.setRoleList(Arrays.asList("coder", "程序"));
        userInfo.setUserId("c51aa33ed1df4ef88f8299969b64ab37");
        userInfo.setAccount("xcye");
        userInfo.setIsAdministrator(false);
        userInfo.setUserTag(2);
        userInfo.setNickName("xcyeye");
        String sign = sign(userInfo);
        System.out.println(sign);
        System.out.println(parseJWT(sign));
    }

    /**
     * 从token中获取UserInfo
     *
     * @return
     */
    public static UserInfo getUserinfoByToken() {
        HttpServletRequest request = HttpUtils.getCurrentHttpServletRequest();
        if (request == null) {
            throw new AuroraUserException("从当前线程中获取不到请求信息");
        }
        String tokenHeader = request.getHeader(HttpConstant.AUTHORIZATION_HEADER);
        if (!StringUtils.hasLength(tokenHeader)) {
            throw new AuroraUserException("当前请求头中没有key为 " + HttpConstant.AUTHORIZATION_HEADER + "的value值");
        }

        UserInfo userInfo = JwtUtil.parseJWT(tokenHeader);
        if (userInfo == null) {
            throw new AuroraUserException("token失效");
        }
        return userInfo;
    }

    public static String getUserIdByToken() {
        UserInfo userinfo = null;
        try {
            userinfo = getUserinfoByToken();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return userinfo.getUserId();
    }

    /**
     * 获取当前用户的角色集合
     *
     * @return
     */
    public static List<String> getCurrentUserRoleList() {
        UserInfo userinfo = null;
        try {
            userinfo = getUserinfoByToken();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        return userinfo.getRoleList();
    }

    /**
     * 当前登录用户是否是超级管理员
     *
     * @param superRole 超级管理员的角色名
     * @return
     */
    public static boolean isSuperAdministrator(String superRole) {
        if (!StringUtils.hasLength(superRole)) {
            return false;
        }
        List<String> currentUserRoleList = getCurrentUserRoleList();
        if (currentUserRoleList == null) {
            return false;
        }
        return currentUserRoleList.contains(superRole);
    }

    /**
     * 当前登录用户是否是管理员
     *
     * @param administratorRoleNameList 在该模块下,哪些角色是作为管理员存在
     * @return
     */
    public static boolean isAdministrator(List<String> administratorRoleNameList) {
        List<String> currentUserRoleList = getCurrentUserRoleList();
        if (currentUserRoleList == null) {
            return false;
        }
        for (String administratorRoleName : administratorRoleNameList) {
            for (String currentUserRoleName : currentUserRoleList) {
                if (administratorRoleName.equals(currentUserRoleName)) {
                    return true;
                }
            }
        }
        return false;
    }
}

shiro-spring-boot-starter的代码就已经写完了,下面我贴上aurora-admin-boot的代码。

aurora-admin-boot

AuroraServerShiroConfig

java 复制代码
@Configuration
public class AuroraServerShiroConfig {

    @Autowired
    private UserAdminRealm userAdminRealm;

    @Autowired
    private AdminCredentialsMatcher adminCredentialsMatcher;

    @PostConstruct
    public void init() {
        // 配置密码验证器
        userAdminRealm.setCredentialsMatcher(adminCredentialsMatcher);
    }
}

UserAdminRealm

java 复制代码
@Slf4j
@Component
public class UserAdminRealm extends AbstractAuthenticationRealm {

    @Autowired
    private UserAdminService userAdminService;

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private UserPermissionExtService userPermissionExtService;

    @Autowired
    private AuroraShiroProperties auroraShiroProperties;

    /**
     *
     * @param principal 包含账户名的对象
     * @param roleIdentifiers 用户自己拥有的角色
     * @return
     */
    @Override
    protected boolean judgePermission(PrincipalCollection principal, Collection<String> roleIdentifiers) {
        UserInfo userinfo = JwtUtil.getUserinfoByToken();
        String account = userinfo.getAccount();
        // 1. 从doGetAuthorizationInfo中获取能访问此method:xxx请求的角色doGetAuthorizationInfo#getRoles就行
        List<String> currentUserRoleList = userinfo.getRoleList();
        if (currentUserRoleList == null || currentUserRoleList.isEmpty()) {
            log.error("用户{}没有分配任何角色", account);
            throw new AuthorizationException(ResultEnum.PERMISSIONDENIED.getMessage());
        }
        // 2. 和roleIdentifiers中的角色进行比较,只要roleIdentifiers有一个和
        for (String userRoleName : currentUserRoleList) {
            for (String allowedRoleName : roleIdentifiers) {
                if (userRoleName.equals(allowedRoleName)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 返回对象中的getRoles值,必须是在该系统中,能访问此METHOD:requestUri的所有权限
     * @param principals the primary identifying principals of the AuthorizationInfo that should be retrieved.
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String account = (String) principals.iterator().next();
        if (!StringUtils.hasLength(account)) {
            throw new UnknownAccountException("账户或者密码不能空");
        }
        // 1. 请求当前请求,构造成restful风格
        String restfulUri = HttpUtils.getRestfulUri();

        // 2. 从redis或者是数据库中获取能访问该url的所有角色
        return getJwtTokenAuthorizationInfo(account, restfulUri);
    }

    /**
     * 无论此用户存不存在,都不需要进行非空判断,如果框架得到一个null[AuthenticationInfo]对象,那么会抛出UnknownAccountException
     * @param token the authentication token containing the user's principal and credentials.
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        JwtTokenAuthenticationToken jwtTokenAuthenticationToken = null;
        if (token instanceof JwtTokenAuthenticationToken) {
            jwtTokenAuthenticationToken = (JwtTokenAuthenticationToken) token;
        }

        if (jwtTokenAuthenticationToken == null) {
            throw new UnknownAccountException("账户或者密码错误");
        }

        String account = jwtTokenAuthenticationToken.getAccount();
        String password = jwtTokenAuthenticationToken.getPassword();

        if (!StringUtils.hasLength(account) || !StringUtils.hasLength(password)) {
            throw new UnknownAccountException("账户或者密码错误");
        }

        LambdaQueryWrapper<UserAdmin> wrapper = Wrappers.lambdaQuery();
        wrapper.eq(UserAdmin::getAccount, account);
        // 1. 从token中获取用户名和密码
        UserAdmin userAdminInfo = userAdminService.getOne(wrapper);
        if (userAdminInfo == null) {
            throw new UnknownAccountException("用户不存在");
        }
        return new SimpleAuthenticationInfo(userAdminInfo.getAccount(), userAdminInfo.getPassword(), this.getName());
    }

    private JwtTokenAuthorizationInfo getJwtTokenAuthorizationInfo(String account, String restfulUri) {
        if (!StringUtils.hasLength(auroraShiroProperties.getRedisRolePermissionCacheName())) {
            // 从数据库加载
            return getJwtTokenAuthorizationInfoFromDb(account, restfulUri);
        }else {
            // 从redis进行加载 逻辑自己实现
        }
    }

    private Set<String> getRoleSet(UserPermissionRelationDTO userPermissionRelationDTO, String account, String restfulUri) {
        return 返回用户的角色集合;
    }

    private JwtTokenAuthorizationInfo getJwtTokenAuthorizationInfoFromDb(String account, String restfulUri) {
        String userId = null;
        try {
            userId = JwtUtil.getUserIdByToken();
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
        // 业务逻辑自己实现,只要最终返回用户的角色权限信息就行
    }
}

application.yaml

yaml 复制代码
aurora:
    shiro:
      restfulWhiteUriList:
        - POST:/user/login
        - POST:/user/logout
        - GET:/swagger-resources
        - GET:/v2/api-docs
      redisUserPermissionCacheName: "aurora:blog:rolePermission"
      redisRolePermissionCacheName: "aurora:blog:userPermission"
      ignoreStaticFiles: true
      superAdministratorRoleName: root
      administratorRoleNameList:
        - admin
相关推荐
KK溜了溜了2 小时前
JAVA-springboot log日志
java·spring boot·logback
我命由我123453 小时前
Spring Boot 项目集成 Redis 问题:RedisTemplate 多余空格问题
java·开发语言·spring boot·redis·后端·java-ee·intellij-idea
面朝大海,春不暖,花不开3 小时前
Spring Boot消息系统开发指南
java·spring boot·后端
hshpy3 小时前
setting up Activiti BPMN Workflow Engine with Spring Boot
数据库·spring boot·后端
jay神3 小时前
基于Springboot的宠物领养系统
java·spring boot·后端·宠物·软件设计与开发
不知几秋4 小时前
Spring Boot
java·前端·spring boot
howard20055 小时前
5.4.2 Spring Boot整合Redis
spring boot·整合redis
TracyCoder1235 小时前
接口限频算法:漏桶算法、令牌桶算法、滑动窗口算法
spring boot·spring·限流
饮长安千年月5 小时前
JavaSec-SpringBoot框架
java·spring boot·后端·计算机网络·安全·web安全·网络安全
考虑考虑6 小时前
Jpa中的@ManyToMany实现增删
spring boot·后端·spring