【万字长文微服务整合Shiro+Jwt,源码分析鉴权实战
前言
Shiro是什么,我这里就不多介绍了,全网也有好多是介绍Spring Boot
、Shiro
、Jwt
整合的教程,我想要写这篇文章,是因为有好多的内容,基本上都是类似的,自己想要的效果,我并没有找到合适的解决方案。
整合之后,我想要的一个效果是:
支持 RBAC
通过JWT生成token,做到无状态 能够动态的根据用户角色对当前请求路径进行鉴权,而不是使用Shiro提供的注解,把角色,权限等信息写死 对Shiro的异常进行统一捕获和处理
源码分析
我之前并没有学过Shiro,现在工作中又需要做权限这一块,就快速的学了一下Shiro,后面的分析,我因为是应届生,可能还存在很多考虑不周到的地方和Bug,欢迎各位大佬指出,我也再完善一下。如果你要学Shiro,并不想看官方文档的话,可以试试这篇教程,我个人感觉是非常仔细的,推荐搭配源码一起看。
组件介绍
想要上手Shiro,我们必须要明白Shiro中的几个概念:
Subject
(主题):当前“用户”
是谁,这个“用户”并不是我们现实世界中的人类,你可以这样理解,向Shiro发起操作的就是一个“用户”
,比如一个网络请求,他就是一个Subject。
// 从当前线程上下文中获取一个subject Subject subject = SecurityUtils.getSubject(); subject.hasRole("admin");// 此subject是否有admin角色 subject.hasRole(xxx); subject.isPermitted(xxx);
SecurityManager
(安全管理器):管理系统中所有"Subject"
,是Shiro中的核心类,该类下面有很多的方法,但是这些方法基本上和Subject对象中的方法是一样的,假如我们获取到Subject,想要验证此Subject是否有admin
这个角色,调用subject.hasRole("admin")
,但是其最终执行的是securityManager.hasRole(getPrincipals(), "admin")
// 获取安全管理器 SecurityManager securityManager = SecurityUtils.getSecurityManager(); securityManager.hasRole(xxx, xxx);
Authenticator
(身份认证):验证用户身份,该类只有一个方法authenticate()
,该方法需要返回一个AuthenticationInfo
对象,此对象中就包含用户的用户名和密码(数据库中存储的密码,并不是表单中密码)
CredentialsMatcher
(凭证匹配器):也可以叫做密码匹配器,调用subject.login()
方法,从Authenticator
中获取到用户名,密码,在CredentialsMatcher
中进行密码的比较。
Authorizer
(授权):进行鉴权操作,验证某个用户是否具有某某权限/角色
boolean hasRole(PrincipalCollection subjectPrincipal, String roleIdentifier); boolean isPermitted(PrincipalCollection subjectPrincipal, Permission permission); ...
Realm
:Shiro和数据之间的桥梁,我们的用户数据可以存放在本地、数据库、Redis、第三方等等,Shiro他并不关心,也不需要知道它所需要的数据是以什么方式存储,它关心的仅仅是,我(Shiro)需要用户的账号和密码,或者用户的权限信息,你就必须给我。他们之间就是通过Realm
来进行数据通信的,在一个Security Manager
,可以有多个Realm
,对于存在多个Realm
的情况,我们可以定义策略(AuthenticationStrategy
),来决定如何处理。
Session Management
(Session管理器):如果使用Jwt,这个基本上用不到,主要作用就是保存session
Cache
(缓存):也就是获取用户信息或者权限信息等,可以从缓存中获取,Shiro中的缓存全部都是org.apache.shiro.cache.Cache
对象
上面就是我们需要了解的一些概念,下面我将对上面这些内容进行源码分析。源码分析的时候,我们只需要关注两个方法,分别是
SecurityUtils.getSubject().login()
和SecurityUtils.getSubject().hasRole()
(使用hasRole举例,其他的方法一样)
调用SecurityUtils.getSubject().login()
调用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
我们进入 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
进入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
继续往下,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**/); return modularRealmAuthenticator; }
notifySuccess(token, info)
方法完了,我们继续回到流程中的info = doAuthenticate(token)
,最终会调用org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate
,请注意了,ModularRealmAuthenticator
这个类非常重要,他是一个支持多Realm
的认证器,正常我们在SecurityManager
中都将Authenticator
设置为ModularRealmAuthenticator
,他的doAuthenticate
方法为
protected Collection getRealms() { return this.realms; } protected void assertRealmsConfigured() throws IllegalStateException { Collection realms = getRealms(); if (CollectionUtils.isEmpty(realms)) { throw new IllegalStateException(msg); } } protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
可以看到,在doAuthenticate()
方法中,首先会判断当前认证器中有没有Realm
对象,因为之前说过了,Shiro想要知道用户信息,就必须要有Realm这个中间件,就算数据存储在本地的也不行,我们设置Realm
可以在SecurityManager
和ModularRealmAuthenticator
中都可以设置,效果是一样的。
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
对象,就是调用realm
的getAuthenticationInfo
进行获取,我们点击进入该方法,在讲解该getAuthenticationInfo方法之前,我再说一下Realm
我们目前已经了解到Realm
是Shiro和数据之间的桥梁,对于一个安全框架来说,获取用户信息和用户权限是必不可少的,但是在Realm
接口中,我们只看到了获取用户信息的方法,也就是org.apache.shiro.realm.Realm#getAuthenticationInfo
,但是并没有获取用户权限信息的方法,这个是因为获取用户的权限信息是在Realm
的实现类中去完成的,我们看一下Realm
接口的子类关系图
在上图中,我们可以看到,有两个非常重要的类,一个是AuthenticatingRealm
,还有一个是AuthorizingRealm
,我们现在看一下上图中主要类的源码
Realm
Realm
接口我们已经分析过了,这里就不看了,其里面获取用户信息的方法为org.apache.shiro.realm.Realm#getAuthenticationInfo
CachingRealm
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 { Cache 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 realmCollection = new ArrayList(); realmCollection.add(adminRealm); securityManager.setRealms(realmCollection); return securityManager; }
AuthenticatingRealm
AuthenticatingRealm
:在此类中,我们可以配置凭据匹配器CredentialsMatcher
,是否开启身份验证缓存authenticationCachingEnabled
,还有身份验证缓存的名字authenticationCacheName
,还有几个重要的方法,主要源码如下public abstract class AuthenticatingRealm extends CachingRealm implements Initializable { // 凭据匹配器,也就是如何进行密码验证 private CredentialsMatcher credentialsMatcher; private Cache authenticationCache; // 是否开启身份验证缓存 private boolean authenticationCachingEnabled; private String authenticationCacheName; // 判断是否能从缓存中获取AuthenticationInfo对象,判断的方法是isAuthenticationCachingEnabled(),逻辑是1.开启身份验证缓存authenticationCachingEnabled(true),2.为realm设置了cachingEnabled(true) private Cache getAvailableAuthenticationCache() { Cache cache = getAuthenticationCache(); boolean authcCachingEnabled = isAuthenticationCachingEnabled(); if (cache == null && authcCachingEnabled) { cache = getAuthenticationCacheLazy(); } return cache; } // 懒加载缓存 private Cache 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 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
AuthorizingRealm
:此类很重要,我们看该类的继承和实现关系,就可以看到,它实现了Authorizer
,所以就有hasRole()
等方法,还继承自AuthenticaticatingRealm
,该类中,有5个属性我们可以进行设置,authorizationCachingEnabled
是否开启授权缓存,authorizationCacheName
授权缓存的名字,permissionResolver
权限解析器,permissionRoleResolver
角色解析器,这个注意了,能够从缓存中获取
AuthorizationInfo
(用户权限角色信息),其逻辑和AuthenticatingRealm
是一样的,你必须开启设置authorizationCachingEnabled
和cachingEnabled
两个字段的值为true,才能开启
从realm中获取用户权限信息的逻辑和从realm中获取用户身份信息的逻辑是差不多的,其方法是org.apache.shiro.realm.AuthorizingRealm#getAuthorizationInfo
,我们可以看一下其源码
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) { AuthorizationInfo info = null; // 1. 从缓存中获取 Cache 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
现在我们已经看完了所需要了解的源码了,回到我们之前的开始的部分org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate
方法
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection 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 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()
中分析过了,这里就不重复了,我就只说一下调用流程,还有部分类的源码分析。
org.apache.shiro.subject.support.DelegatingSubject#hasRole
下一个流程,根据你配置的Authenticator
来走
如果你直接配置的认证器是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; }
因为上面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
,如果该realm
是AuthorizingRealm
类型的话,那么就调用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 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 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
,该类的继承关系如下
关于这些过滤器,我画了一张图,帮助大家理解每一个过滤器他的一个区别,新增哪些方法。
但是在开发过程中,我自定义的过滤器只继承了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 roleIdentifiers) { assertRealmsConfigured(); List 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 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 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 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 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 restfulWhiteUriList; /** * redis中存储角色权限关系的key值 */ private String redisRolePermissionCacheName; /** * redis中存储用户权限关系的key值 */ private String redisUserPermissionCacheName; /** * 是否过滤静态文件 */ private Boolean ignoreStaticFiles; /** * 超级管理员的角色名 */ private String superAdministratorRoleName; /** * 在该模块下,哪些角色是作为管理员存在 TODO 后期为了便于维护,可以使用字典进行维护 */ private List 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 roleList; @Setter private Collection stringPermissions; @Setter private Collection permissions; @Override public Collection getRoles() { return roleList; } @Override public Collection getStringPermissions() { return stringPermissions; } @Override public Collection 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 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 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 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 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 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 currentUserRoleList = getCurrentUserRoleList(); if (currentUserRoleList == null) { return false; } return currentUserRoleList.contains(superRole); } /** * 当前登录用户是否是管理员 * * @param administratorRoleNameList 在该模块下,哪些角色是作为管理员存在 * @return */ public static boolean isAdministrator(List administratorRoleNameList) { List 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 roleIdentifiers) { UserInfo userinfo = JwtUtil.getUserinfoByToken(); String account = userinfo.getAccount(); // 1. 从doGetAuthorizationInfo中获取能访问此method:xxx请求的角色doGetAuthorizationInfo#getRoles就行 List 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 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 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