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

本文涉及的产品
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 介绍如何整合Spring Boot、Shiro和Jwt,以实现一个支持RBAC的无状态认证系统。通过生成JWT token,实现用户无状态登录,并能根据用户角色动态鉴权,而非使用Shiro提供的注解,将角色和权限信息硬编码。此外,文章还探讨了如何对Shiro的异常进行统一捕获和处理。作为应届生,笔者在学习Shiro的过程中进行了一些源码分析,尽管可能存在不足和Bug,但希望能为同样需要实现权限管理的开发者提供参考,并欢迎各位大佬指正完善。

前言

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

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

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

源码分析

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

组件介绍

image.png

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

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

    // 从当前线程上下文中获取一个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")

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

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

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

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

    image.png

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

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

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

调用SecurityUtils.getSubject().login()

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

    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

    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方法

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

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

    @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

    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,如果我们需要设置监听器,可以参照下面方式

    @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方法为

    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的情况,其源码如下

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

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

    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

    @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中获取到账户名和密码

    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

    image-20230729210658143.png

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

image.png

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

Realm

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

CachingRealm

  1. CachingRealm,该类比Realm多了三个能自定义的属性,还有几个方法
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增加缓存支持的话,可以在配置中进行指定

@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,还有几个重要的方法,主要源码如下
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,我们可以看一下其源码

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方法

    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)方法

      @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

      @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()登录那么复杂,需要设计到策略。

    public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
         
        AuthorizationInfo info = getAuthorizationInfo(principal);
        return hasRole(roleIdentifier, info);
    }
    

hasRole()

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

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

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

hasAllRoles()

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),源码如下

// 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对象就行

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()方法区别

直接上源码

// Subject.hasRole()
Subject subject = SecurityUtils.getSubject();
subject.hasRole("");

public boolean hasRole(String roleIdentifier) {
   
    return hasPrincipals() && securityManager.hasRole(getPrincipals(), roleIdentifier);
}
// 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,该类的继承关系如下

image.png

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

image.png

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

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的源码:

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

其目录结构如下:

├─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

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

@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

@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

/**
 * @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

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

/**
 * @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

/**
 * 正则表达式的枚举,存放所有需要的正则表达式
 *
 * @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

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

}

AuroraHttpFilter

/**
 * @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

@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

/**
 * @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

@Configuration
public class AuroraServerShiroConfig {
   

    @Autowired
    private UserAdminRealm userAdminRealm;

    @Autowired
    private AdminCredentialsMatcher adminCredentialsMatcher;

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

UserAdminRealm

@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

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
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
2月前
|
数据采集 JSON 算法
Python爬虫——基于JWT的模拟登录爬取实战
Python爬虫——基于JWT的模拟登录爬取实战
69 1
Python爬虫——基于JWT的模拟登录爬取实战
|
10天前
|
存储 监控 供应链
微服务拆分的 “坑”:实战复盘与避坑指南
本文回顾了从2~3人初创团队到百人技术团队的成长历程,重点讨论了从传统JSP到前后端分离+SpringCloud微服务架构的演变。通过实际案例,总结了微服务拆分过程中常见的两个问题:服务拆分边界不清晰和拆分粒度过细,并提出了优化方案,将11个微服务优化为6个,提高了系统的可维护性和扩展性。
28 0
|
1月前
|
JSON 安全 Go
Go语言中使用JWT鉴权、Token刷新完整示例,拿去直接用!
本文介绍了如何在 Go 语言中使用 Gin 框架实现 JWT 用户认证和安全保护。JWT(JSON Web Token)是一种轻量、高效的认证与授权解决方案,特别适合微服务架构。文章详细讲解了 JWT 的基本概念、结构以及如何在 Gin 中生成、解析和刷新 JWT。通过示例代码,展示了如何在实际项目中应用 JWT,确保用户身份验证和数据安全。完整代码可在 GitHub 仓库中查看。
224 1
|
1月前
|
运维 NoSQL Java
后端架构演进:微服务架构的优缺点与实战案例分析
【10月更文挑战第28天】本文探讨了微服务架构与单体架构的优缺点,并通过实战案例分析了微服务架构在实际应用中的表现。微服务架构具有高内聚、低耦合、独立部署等优势,但也面临分布式系统的复杂性和较高的运维成本。通过某电商平台的实际案例,展示了微服务架构在提升系统性能和团队协作效率方面的显著效果,同时也指出了其带来的挑战。
86 4
|
1月前
|
JavaScript NoSQL Java
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
CC-ADMIN后台简介一个基于 Spring Boot 2.1.3 、SpringBootMybatis plus、JWT、Shiro、Redis、Vue quasar 的前后端分离的后台管理系统
50 0
|
3月前
|
Dubbo Java 应用服务中间件
微服务框架Dubbo环境部署实战
微服务框架Dubbo环境部署的实战指南,涵盖了Dubbo的概述、服务部署、以及Dubbo web管理页面的部署,旨在指导读者如何搭建和使用Dubbo框架。
290 17
微服务框架Dubbo环境部署实战
|
3月前
|
运维 持续交付 API
深入理解并实践微服务架构:从理论到实战
深入理解并实践微服务架构:从理论到实战
156 3
|
3月前
|
运维 监控 持续交付
深入浅出:微服务架构的设计与实战
微服务,一个在软件开发领域如雷贯耳的名词,它代表着一种现代软件架构的风格。本文将通过浅显易懂的语言,带领读者从零开始了解微服务的概念、设计原则及其在实际项目中的运用。我们将一起探讨如何将一个庞大的单体应用拆分为灵活、独立、可扩展的微服务,并分享一些实践中的经验和技巧。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和深入的理解。
89 3
|
3月前
|
自然语言处理 Java 网络架构
解锁跨平台微服务新纪元:Micronaut与Kotlin联袂打造的多语言兼容服务——代码、教程、实战一次打包奉送!
【9月更文挑战第6天】Micronaut是一款轻量级、高性能的Java框架,适用于微服务开发。它支持Java、Groovy和Kotlin等多种语言,提供灵活的多语言开发环境。本文通过创建一个简单的多语言兼容服务,展示如何使用Micronaut及其注解驱动特性实现REST接口,并引入国际化支持。无论是个人项目还是企业应用,Micronaut都能提供高效、一致的开发体验,成为跨平台开发的利器。通过简单的配置和代码编写,即可实现多语言支持,展现其强大的跨平台优势。
60 3
|
4月前
|
消息中间件 缓存 Kafka
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)
go-zero微服务实战系列(八、如何处理每秒上万次的下单请求)