1. 目标
前面一直讨论的是只有一个Reaml的场景,Shiro是可以管理多个Realm的。那么什么场景下,我们需要定义多个Realm,以及Shiro框架是如何管理多个Realm的,他们是如何工作的。本章将会解释上面的问题,最后会配置前面章节中的 SystemAccountRealm
用来做用户名,密码认证, ApiAuthenticationRealm
用来做 api接口访问认证。
2. 多Reaml的场景
在多个 Realm
的场景中,通常是因为存在不同的安全数据源或需要不同的验证策略。以下是一些需要多个 Realm
的示例场景:
2.1 多数据源
系统中存在不同类型的用户群体,如普通用户、管理员、合作伙伴等,它们各自拥有独立的认证方式和数据源。例如,普通用户通过用户名/密码登录,管理员使用数字证书验证身份,而合作伙伴可能通过 API 密钥进行认证。
使用多个 Realm:为每种用户类型创建一个对应的 Realm,如 UsernamePasswordRealm
用于处理普通用户的登录,CertificateRealm
用于管理员的身份验证,以及 ApiKeyRealm
用于合作伙伴的API访问控制。每个 Realm 从各自的用户数据源(如数据库、API密钥存储)中获取和验证用户凭据。
2.2 多策略身份验证
系统要求用户在登录时提供不止一种身份验证信息,如除了输入密码外,还需要通过短信验证码、硬件令牌(如U2F、TOTP)或生物特征(指纹、面部识别)进行二次验证。
使用多个 Realm:可以创建一个主 Realm(如 PasswordRealm
)处理基础的用户名/密码认证,再添加一个或多个辅助 Realm(如 SmsCodeRealm
、HardwareTokenRealm
)处理第二因素的验证。Shiro会按照配置的顺序或策略依次调用各个 Realm 进行认证,只有所有 Realm 都成功验证后,用户才被认为已通过多因素认证。
通过配置多个 Realm
并使用 Shiro 的策略组合逻辑(如 AtLeastOneSuccessfulStrategy
、FirstSuccessfulStrategy
等),可以实现这种复杂的验证逻辑。
2.3 集成外部系统
系统需要与现有的第三方认证服务(如OAuth2、SAML、CAS等)或企业级目录服务(如Active Directory、OpenLDAP)进行集成,允许用户使用外部系统的凭证登录。
使用多个 Realm:配置一个 OAuth2Realm
、SamlRealm
或 LdapRealm
与相应的外部服务对接,同时保留内部的 DatabaseRealm
或其他自定义 Realm 供本地用户使用。这样,用户可以选择使用系统内置账户或通过单点登录(SSO)使用外部系统的身份进行认证。
2.4 不同的授权需求
有的时候,在一个系统中会存在不同的授权需求,比如:
基于角色的授权:一个Realm 处理基于用户角色的授权,另一个Realm 处理基于权限的授权。
外部服务授权:一个Realm 处理内部资源的授权,另一个Realm 处理外部服务的授权,比如使用REST API。
2.5 遗留系统的整合
当集成遗留系统时,这些系统可能使用不同的身份验证机制和数据源。通过为每个遗留系统配置一个 Realm
,可以在一个统一的 Shiro 配置中管理所有身份验证逻辑。比如:
新系统可能采用了新的认证授权机制(如OAuth 2.0、JWT),而旧系统仍依赖传统的用户名/密码认证。为确保平滑过渡,可以在新系统中配置两个Realm:一个用于处理新系统的OAuth/JWT认证,另一个用于兼容旧系统的数据库认证。这样,无论是新老用户都能顺利登录并获得相应的权限。
3. 多Realm工作原理
在 Shiro 中,可以配置一个或多个Realm,并指定它们的顺序。当需要进行身份验证或授权时,Shiro 将按照配置的顺序依次查询这些Realm,直到找到合适的Realm 来处理请求。
3.1 Realm配置
我们可以定义多个Realm的配置,比如:
@Configuration
@Slf4j
public class ShiroConfiguration {
@Bean
public Realm apiKeyRealm() {
ApiAuthenticationRealm realm = new ApiAuthenticationRealm();
realm.setCachingEnabled(true);
realm.setAuthenticationCachingEnabled(true);
// 认证缓存的名字,不设置也可以,默认由
realm.setAuthenticationCacheName("shiro:authentication:apiKeyCache");
return realm;
}
@Bean
public Realm userPasswordRealm() {
SystemAccountRealm realm = new SystemAccountRealm();
realm.setCachingEnabled(true);
realm.setAuthenticationCachingEnabled(true);
// 认证缓存的名字,不设置也可以,默认由
realm.setAuthenticationCacheName("shiro:authentication:userPasswordCache");
return realm;
}
...
}
3.2 SecurityManager与Realm关联
SecurityManager 是 Shiro 的核心组件,负责协调整个安全框架的行为。配置时,将所有定义的 Realm 注册到 SecurityManager 中。在shiro-spring-boot-web-starter
的自动配置中,会将定义的多个Realm配置给 SecurityManager:
自动配置ShiroWebAutoConfiguration 源码:
@Configuration
@AutoConfigureBefore(ShiroAutoConfiguration.class)
@AutoConfigureAfter(ShiroWebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnProperty(name = "shiro.web.enabled", matchIfMissing = true)
public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
...
// 会到spring容器中查找所有的Realm,然后"注入" 到这个方法上
@Bean
@ConditionalOnMissingBean
@Override
protected SessionsSecurityManager securityManager(List<Realm> realms) {
return super.securityManager(realms);
}
...
}
//ShiroWebAutoConfiguration 的父类
public class AbstractShiroConfiguration {
...
protected SessionsSecurityManager securityManager(List<Realm> realms) {
SessionsSecurityManager securityManager = createSecurityManager();
// 重要: 关键角色之一: Authenticator(认证器)
securityManager.setAuthenticator(authenticator());
// 重要: 关键角色之二: Authorizer(授权器)
securityManager.setAuthorizer(authorizer());
// 重要:关键角色之三,Realm.在SecurityManager中,用集合来保存多个Realm
securityManager.setRealms(realms);
securityManager.setSessionManager(sessionManager());
securityManager.setEventBus(eventBus);
if (cacheManager != null) {
securityManager.setCacheManager(cacheManager);
}
return securityManager;
}
...
// 认证器策略
protected AuthenticationStrategy authenticationStrategy() {
return new AtLeastOneSuccessfulStrategy();
}
// 认证器
protected Authenticator authenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(authenticationStrategy());
return authenticator;
}
// 授权器
protected Authorizer authorizer() {
ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();
if (permissionResolver != null) {
authorizer.setPermissionResolver(permissionResolver);
}
if (rolePermissionResolver != null) {
authorizer.setRolePermissionResolver(rolePermissionResolver);
}
return authorizer;
}
}
可以看到,实际的认证器和授权器是 ModularRealmAuthenticator 和 ModularRealmAuthorizer
3.2.1 Authenticator(认证器)
Authenticator
负责对用户提交的认证信息(如用户名、密码、API密钥等)进行验证,判断用户身份是否合法,即是否为系统所认可的用户。它的工作流程如下:
- 接收和解析:接收客户端提交的
AuthenticationToken
(认证令牌),该令牌封装了用户提供的身份标识和认证凭据。在前面的例子中,AuthenticationToken
是在过滤器中被创建的,然后它就被交给了SecurityManager
,SecurityManager
调度认证器,同时传给他AuthenticationToken
来进行认证 - 身份验证:根据令牌信息,调用关联的 Realm(安全数据源)进行实际的认证操作。Realm 从数据源中查找与令牌匹配的用户记录,并验证凭据的有效性。
- 结果判定:根据 Realm 返回的认证结果,决定是否认证成功。如果所有关联的 Realm 中至少有一个成功验证了用户身份,通常认为认证成功(具体取决于配置的认证策略)。成功时,
Authenticator
会创建或更新Subject
(当前用户主体)的身份信息,并将其绑定到当前会话中。
简而言之:Authenticator
的主要任务是验证用户身份,确保只有合法用户能够访问受保护的资源或服务。
认证器策略:
AuthenticationStrategy
(认证策略)则是Authenticator
中的一个关键组件,用于定义如何处理多个Realm
(领域)的认证结果。主要作用是在多个Realm
参与认证时,决定如何合并和解释这些Realm
的认证结果。当配置了多个Realm
时,每个Realm
都可能返回自己的认证结果,而AuthenticationStrategy
则负责将这些结果整合成一个最终的认证结果。它是这样工作的:
- 发起认证:用户提交
AuthenticationToken
(如用户名/密码对),Authenticator
开始认证流程。 - 遍历 Realm:
Authenticator
使用配置的AuthenticationStrategy
,按照一定的顺序或策略遍历已注册的各个 Realm。对于每个 Realm,Authenticator
传递AuthenticationToken
给 Realm,让其尝试进行认证。 - 收集结果:每个 Realm 完成认证后,返回一个
AuthenticationInfo
(认证信息,如用户详情和凭据)或抛出异常(表示认证失败)。Authenticator
将这些结果收集起来,传递给AuthenticationStrategy
。 - 策略评估:
AuthenticationStrategy
根据收集到的 Realm 认证结果,依据预设策略进行评估。常见的策略有:- At least one successful strategy(至少一个成功):只要有至少一个 Realm 认证成功,整个认证过程就算成功。这是最常用的策略,适用于多因素认证或多来源认证的情况,只要一个因素或来源通过即可。
- All successful strategy(全部成功):所有 Realm 必须全部认证成功,整个认证过程才算成功。适用于需要极高安全性的场景,所有认证途径都必须验证无误。
- First successful strategy(首个成功):一旦遇到第一个认证成功的 Realm,立即停止后续 Realm 的认证,并认定整个认证过程成功。适用于希望尽快结束认证过程,或者后续 Realm 认证成本较高的情况。
- 决策反馈:根据策略评估的结果,
AuthenticationStrategy
告知Authenticator
认证是否成功。若成功,Authenticator
将认证成功的AuthenticationInfo
与当前Subject
绑定;若失败,Authenticator
可能抛出相应的异常或返回错误信息。
上面的源代码中可以看到,默认使用的是 ModularRealmAuthenticator
和 AtLeastOneSuccessfulStrategy
实例说明:
假设系统配置了三个 Realm:ApiAuthenticationRealm
(API认证)、SystemAccountRealm
(管理后台用户认证)、WxRealm
(微信客户端认证)采用 At least one successful strategy
。
- 用户提交
AuthenticationToken
。 Authenticator
首先调用ApiAuthenticationRealm
进行认证,结果失败。Authenticator
继续调用SystemAccountRealm
,用户通过 SystemAccountRealm验证成功。Authenticator
收到成功结果,不再尝试WxRealm
,因为已经有一个 Realm 认证成功。AuthenticationStrategy
根据策略判断认证成功,因为至少有一个 Realm(SystemAccountRealm
)成功。Authenticator
将SystemAccountRealm
返回的AuthenticationInfo
与当前Subject
绑定,用户登录成功。
源码分析:
先看看继承关系:
在Authenticator接口中定义了方法:
public interface Authenticator {
/**
* 这个方法会抛出一下几个异常:
* @see ExpiredCredentialsException 凭证过期
* @see IncorrectCredentialsException 凭证错误
* @see ExcessiveAttemptsException 尝试次数过多
* @see LockedAccountException 账号被锁定
* @see ConcurrentAccessException 并发访问异常
* @see UnknownAccountException 账号错误异常
*/
AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
throws AuthenticationException;
}
我们知道Subject的登录操作实际上是委托给了SecurityManager来完成的:
public class DefaultWebSecurityManager extends DefaultSecurityManager implements WebSecurityManager{
...
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
...
info = authenticate(token);
...
Subject loggedIn = createSubject(token, info, subject);
onSuccessfulLogin(token, info, loggedIn);
return loggedIn;
}
...
// 这个方法实际上是定义在父类 AuthenticatingSecurityManager 中。这里为了方便理解,就写在了
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
}
接着看 ModularRealmAuthenticator 类是如何工作的:
public class ModularRealmAuthenticator extends AbstractAuthenticator {
...
private Collection<Realm> realms;
private AuthenticationStrategy authenticationStrategy;
...
// 这个方法定义在父类 AbstractAuthenticator中,这里为了看起来方便就写在这里
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException{
...
info = doAuthenticate(token);
...
}
// 执行认证的逻辑
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
assertRealmsConfigured();
// realm的集合
Collection<Realm> realms = getRealms();
if (realms.size() == 1) { // 如果是一个realm,执行doSingleRealmAuthentication
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else { // 如果存在多个 realm 执行 doMultiRealmAuthentication
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
// 单个realm认证
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) { // 判定这个realm是否支持这个token,不支持抛出 UnsupportedTokenException 异常
...
}
// 从 realm 中获取 AuthenticationInfo,即认证信息
AuthenticationInfo info = realm.getAuthenticationInfo(token);
...
return info;
}
// 真正多个realm认证逻辑
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
// 认证策略
AuthenticationStrategy strategy = getAuthenticationStrategy();
// 认证之前,在实际的FirstSuccessfulStrategy中,返回的是 null
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
...
// 遍历所有的realm
for (Realm realm : realms) {
try {
// 策略中如果抛出 ShortCircuitIterationException 异常则终止遍历
aggregate = strategy.beforeAttempt(realm, token, aggregate);
} catch (ShortCircuitIterationException shortCircuitSignal) {
break;
}
// 判断这个realm是否支持当前的token
if (realm.supports(token)) {
...
// realm 认证
info = realm.getAuthenticationInfo(token);
...
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
} else { //token不匹配则跳过
LOGGER.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token);
}
}
// 所有的realm都遍历完成之后,在这里决策这个是否通过
aggregate = strategy.afterAllAttempts(token, aggregate);
return aggregate;
}
}
3.2.2 Authorizer(授权器)
Authorizer
负责确定已认证用户(即拥有有效身份的 Subject
)是否有权限执行特定的操作或访问特定的资源。它基于预先定义的权限规则和用户角色来做出授权决策。它的主要任务:
- 权限查询:根据
Subject
的身份信息(如用户ID、角色、权限等),查询关联的 Realm 获取用户的权限数据。这可能包括用户直接拥有的权限,以及通过角色间接继承的权限。 - 权限检查:对
Subject
进行具体的权限检查。当应用程序需要判断用户是否有权执行某个操作(如访问某个URL、执行特定的数据库操作)时,会向Authorizer
提交一个权限验证请求,如“用户是否具有read:document
权限”。Authorizer
根据查询到的权限数据判断用户是否具备所需权限。 - 角色与权限关系管理:某些 Authorizer 实现可能还支持角色与权限之间的动态关联管理,如基于 RBAC(Role-Based Access Control,基于角色的访问控制)模型进行授权。
简而言之:Authorizer
的主要任务是根据用户身份及其关联的权限信息,判断用户是否有权执行特定操作,从而控制对系统资源的访问权限。
源码就不分析了,感兴趣的可以自行分析,入口点从 DefaultWebSecurityManager
继承的 checkXXX
或者hasXXX
方法开始。
3.3 多 Realm 与 Subject 的交互
在实际运行时,Subject 无需关心底层有多少个 Realm。它通过 SecurityManager 提供的接口进行认证、授权操作。SecurityManager 负责根据配置和策略,透明地与各个 Realm 交互,确保正确的 Realm 被用于处理特定的用户请求。
3.4 会话管理与多 Realm
Shiro 的会话管理功能同样适用于多 Realm 环境。无论用户通过哪个 Realm 登录,Shiro 都会创建一个唯一的 Session,用于跟踪用户的会话状态。后续的会话相关操作(如检查会话有效性、更新会话属性、销毁会话)都由 SecurityManager 统一管理,不受具体 Realm 数量的影响。
3.5 小结
总结来说,Apache Shiro 通过 SecurityManager 将多个 Realm 组织在一起,根据预设的策略协调它们在认证、授权过程中的行为。这种设计使得系统能够轻松应对多样化的安全需求,同时保持内部逻辑的清晰与简洁。开发者只需关注每个 Realm 的具体实现,而无需关心 Realm 间如何协同工作。
4. 案例
本案例将会配置前面章节中的 SystemAccountRealm
用来做用户名,密码认证, ApiAuthenticationRealm
用来做 api接口访问认证,让应用同时支持两种认证方式。
4.1 确定认证策略
现在思考一下用户名,密码认证的方式SystemAccountRealm
和 ApiAuthenticationRealm
采用什么策略?很显然它们之间没有关联性,使用默认的AtLeastOneSuccessfulStrategy
策略。因为 SystemAccountRealm支持的token是 UsernamePasswordToken
, 而 ApiAuthenticationRealm支持的Token是ApiAuthenticationToken
,每个Realm对应的Token是不一样,也没有关联关系,所以认证的时候,只需要匹配一个即可。
4.2 AuthenticationToken的创建
前面的案例中,我们定义了两个Filter,ApiAuthenticationFilter
和 AuthenticationFilter
, 这两个Filter中都创建了自己的Token:
// api认证的filter
public class ApiAuthenticationFilter extends AuthenticatingFilter {
...
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = WebUtils.toHttp(request);
String accessKey = req.getHeader("X-Access-Key");
String accessTimestamp = req.getHeader("X-Access-Timestamp");
String accessSign = req.getHeader("X-Access-Sign");
String accessAppId = req.getHeader("X-Access-AppId");
ContentCachingRequestWrapper cachedRequestWrapper = (ContentCachingRequestWrapper) request;
String requestBody = StreamUtils.copyToString(cachedRequestWrapper.getInputStream(), Charset.forName("UTF-8"));
return new ApiAuthenticationToken(accessKey, accessTimestamp, accessSign, accessAppId, requestBody);
}
...
}
// 用户名,密码认证的filter
public class AuthenticationFilter extends org.apache.shiro.web.filter.authc.FormAuthenticationFilter{
...
// 这个方法没有在本类中定义,而是在 org.apache.shiro.web.filter.authc.AuthenticatingFilter 这个父类中定义的
protected AuthenticationToken createToken(String username, String password,
boolean rememberMe, String host) {
return new UsernamePasswordToken(username, password, rememberMe, host);
}
...
}
实际应用中,一次只能创建一种类型的Token,那过滤器链的顺序就比较重要了。这里有两种思路:
- 提供一个公共的Filter,让所有的请求都通过这个Filter,客户端在提交数据的时候,在请求头上加上标识,公共的Filter获取到这个标识后,创建对应的Token。
- 每种认证方式都有一个Filter,用URL匹配模式与这个Filter关联,每个Filter内创建自己的
AuthenticationToken
.
第一种方式需要在客户端请求头上加标识,只有一个Filter。如果后续再添加一个认证方式,就需要修改这个公共的Filter。而第二种方式遇到这种需求只需要新创建一个Filter,配置的时候,匹配一个URL即可,不需要修改原来的类。所以这里使用第2中方式。
如果采用第二种方式,原有Filter代码不用做任何更改,只需要修改配置,就可以适配两种认证方式。
4.3 配置
以下的方法代码,都在项目的 ShiroConfiguration
这个类中
-
SecurityManager
安全管理器 : 使用ShiroWebAutoConfiguration
这个自动配置,它配置好了DefaultWebSecurityManager
我们无需自己配置 -
authenticator
认证器: 使用默认,无需配置 -
authorizer
授权器: 使用默认,无需配置 -
realm
: 需要使用我们自己的配置@Bean public Realm apiKeyRealm() { ApiAuthenticationRealm realm = new ApiAuthenticationRealm(); realm.setCachingEnabled(true); realm.setAuthenticationCachingEnabled(true); // 认证缓存的名字,不设置也可以,默认由 realm.setAuthenticationCacheName("shiro:authentication:apiKeyCache"); return realm; } @Bean public Realm userPasswordRealm() { SystemAccountRealm realm = new SystemAccountRealm(); realm.setCachingEnabled(true); realm.setAuthenticationCachingEnabled(true); // 认证缓存的名字,不设置也可以,默认由 realm.setAuthenticationCacheName("shiro:authentication:userPasswordCache"); return realm; }
-
shiro
CacheManager
缓存管理器: 我们要将认证和授权信息缓存到Redis中,所以要自己配置。前面代码已经写好了,直接配置.@Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisSerializer<String> stringSerializer = RedisSerializer.string(); // 设置key的序列化器 redisTemplate.setKeySerializer(stringSerializer); // 设置 Hash 结构中 key 的序列化器 redisTemplate.setHashKeySerializer(stringSerializer); return new ShiroRedisCacheManager(redisTemplate); }
-
sessionManager
会话管理器:因为已经禁用cookie了,前面已经改写好了,使用我们自己写的sessionManager. 同时要将session缓存到redis中,所以也要配置sessionDAO
// 配置SessionDAO @Bean public SessionDAO shiroRedisSessionDAO(RedisTemplate redisTemplate, CacheManager cacheManager) { ShiroRedisSessionDAO sessionDAO = new ShiroRedisSessionDAO(redisTemplate, "shiro:session"); // 活跃session缓存的名字 sessionDAO.setActiveSessionsCacheName("shiro:active:session"); sessionDAO.setCacheManager(cacheManager); return sessionDAO; } // sessionManager配置 @Bean public SessionManager sessionManager( SessionFactory sessionFactory, SessionDAO sessionDAO) { //自定义的SessionManager,已经禁用了Cookie AccessTokenWebSessionManager webSessionManager = new AccessTokenWebSessionManager(); // 自动配置中已经配置了sessionFactory 直接注入进来 webSessionManager.setSessionFactory(sessionFactory); // 使用自定义的ShiroRedisSessionDAO webSessionManager.setSessionDAO(sessionDAO); return webSessionManager; }
-
shiroFilterFactoryBean
拦截器相关配置。定义shiroFilterFactoryBean
的时候,需要为它配置:-
securityManager
-
自定义的Filter。它是一个Map结构,过滤器名称和过滤器对象之间的映射
-
URL与过滤器名称和请求URL之间的关系。即什么样的URL对应哪一个或者哪几个过滤器。
可以为一个URL配置多个过滤器名称,名称之间使用"," 分割,这样一个请求就可以通过一个过滤器链
/** * 自定义filter. * apiAuthc->ApiAuthenticationFilter * sysAuthc ->AuthenticationFilter * @return */ private Map<String, Filter> getCustomerShiroFilter() { // API 认证过滤器 ApiAuthenticationFilter apiAuthcFilter = new ApiAuthenticationFilter(); // 用户名,密码认证过滤器 AuthenticationFilter sysAuthcFilter = new AuthenticationFilter(); //需要指定登录地址 sysAuthcFilter.setLoginUrl("/sys/login"); Map<String, Filter> filters = new HashMap<>(); filters.put("apiAuthc", apiAuthcFilter); filters.put("sysAuthc", sysAuthcFilter); return filters; } /** * URL与filter之间的关系 * * @return */ private ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); chainDefinition.addPathDefinition("/api/**", "apiAuthc"); chainDefinition.addPathDefinition("/sys/**", "sysAuthc"); return chainDefinition; } /** * ShiroFilterFactoryBean 重要配置,为他配置过滤器URL映射关系和自定义的过滤器。 **/ @Bean protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean(); filterFactoryBean.setSecurityManager(securityManager); filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap()); filterFactoryBean.setFilters(getCustomerShiroFilter()); return filterFactoryBean; }
-
4.4 改造Controller
controller使用 “/api” 和 “/sys” 进行分类:
4.5 测试
-
用户名密码登录
/sys/login
请求报文:
POST /sys/login HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: Apifox/1.0.0 (https://apifox.com) Accept: */* Host: 127.0.0.1:8080 Connection: keep-alive Content-Type: application/x-www-form-urlencoded username=administrator&password=admin
响应报文:
{ "name": "SystemAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt=55ae2b2c63ddd6d4763e0c57bda9078e)", "accessToken": "70555c4b-7191-41fd-a596-71462564c8da", "message": "登录成功" }
缓存:
-
退出登录
/sys/logout
请求报文:刚才返回的accessToken加入到
X-Access-Token
请求头中POST /sys/logout HTTP/1.1 Host: 127.0.0.1:8080 X-Access-Token: 70555c4b-7191-41fd-a596-71462564c8da User-Agent: Apifox/1.0.0 (https://apifox.com) Accept: */* Host: 127.0.0.1:8080 Connection: keep-alive
响应报文:
{ "name": "SystemAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt=55ae2b2c63ddd6d4763e0c57bda9078e)", "message": "退出登录成功" }
退出登录后,session缓存被清理掉了:
-
访问主页
/sys
请求报文: 刚才返回的accessToken加入到请求头中,这个
X-Access-Token
已经被清理掉了,所以应该是进制访问GET /sys HTTP/1.1 Host: 127.0.0.1:8080 X-Access-Token: 70555c4b-7191-41fd-a596-71462564c8da User-Agent: Apifox/1.0.0 (https://apifox.com) Accept: */* Host: 127.0.0.1:8080 Connection: keep-alive
响应报文:
{ "code": 401, "msg": "未登录或登录已过期" }
-
登录后获取正确的 accessToken 再次请求,访问主页
/sys
请求报文:
GET /sys HTTP/1.1 Host: 127.0.0.1:8080 X-Access-Token: 806db936-61f1-4030-9f5c-fcf71bbc7c38 User-Agent: Apifox/1.0.0 (https://apifox.com) Accept: */* Host: 127.0.0.1:8080 Connection: keep-alive
响应报文: 此时可以正常访问
{ "name": "SystemAccount(account=administrator, pwdEncrypt=0b188436fd5c434e3b8ed05cfe7c107250c1ff0ac034fad089db0f017ac3cacb, salt=55ae2b2c63ddd6d4763e0c57bda9078e)", "sessionKeys": "[org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY, org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY]" }
-
API请求,访问:
/api/employees
添加一个员工请求报文:
POST /api/employees HTTP/1.1 Host: 127.0.0.1:8080 X-Access-Key: db0f017ac3cacb X-Access-Timestamp: 1711853625738 X-Access-Sign: d9afbd8009dec57cbe01bde8fda51246e6163fc0800a987d2ea9ce729e556b16 X-Access-AppId: 123456 User-Agent: Apifox/1.0.0 (https://apifox.com) Content-Type: application/json Accept: */* Host: 127.0.0.1:8080 Connection: keep-alive {"name": "张三","gender": "男"}
响应报文:
{ "message": "创建员工成功", "data": { "name": "张三", "gender": "男" }, "code": "0000" }
5. 总结
- Shiro支持使用多个Realm进行认证,是由 认证器来完成工作的。默认使用的认证器是
ModularRealmAuthenticator
,每个认证器需要为它配置认证策略,默认使用的是AtLeastOneSuccessfulStrategy
. 其工作流程为:- 用户发出URL请求
- 被Shiro过滤器拦截
- 过滤器创建
AuthenticationToken
, 后交给securityManager
,securityManager
再将token
交给认证器,认证器根据策略遍历realm
,调用realm的认证方法,最后根据策略来判断是否认证成功。
- Shiro支持使用多个Realm进行授权,是有授权器来完成工作的。默认使用的授权器是
ModularRealmAuthorizer
- 每一个过滤器创建一个特定的token,交给对应的realm,过滤器与匹配的URL相关联,这样就可以实现多种认证方式。
代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 8_springboot_shiro_jwt_多端认证鉴权_多Reaml管理.