8_springboot_shiro_jwt_多端认证鉴权_多Reaml管理

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(如 SmsCodeRealmHardwareTokenRealm)处理第二因素的验证。Shiro会按照配置的顺序或策略依次调用各个 Realm 进行认证,只有所有 Realm 都成功验证后,用户才被认为已通过多因素认证。

通过配置多个 Realm 并使用 Shiro 的策略组合逻辑(如 AtLeastOneSuccessfulStrategyFirstSuccessfulStrategy 等),可以实现这种复杂的验证逻辑。

2.3 集成外部系统

系统需要与现有的第三方认证服务(如OAuth2、SAML、CAS等)或企业级目录服务(如Active Directory、OpenLDAP)进行集成,允许用户使用外部系统的凭证登录。

使用多个 Realm:配置一个 OAuth2RealmSamlRealmLdapRealm 与相应的外部服务对接,同时保留内部的 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;
    }
}

可以看到,实际的认证器和授权器是 ModularRealmAuthenticatorModularRealmAuthorizer

3.2.1 Authenticator(认证器)

Authenticator 负责对用户提交的认证信息(如用户名、密码、API密钥等)进行验证,判断用户身份是否合法,即是否为系统所认可的用户。它的工作流程如下:

  1. 接收和解析:接收客户端提交的 AuthenticationToken(认证令牌),该令牌封装了用户提供的身份标识和认证凭据。在前面的例子中,AuthenticationToken 是在过滤器中被创建的,然后它就被交给了 SecurityManager ,SecurityManager调度认证器,同时传给他AuthenticationToken 来进行认证
  2. 身份验证:根据令牌信息,调用关联的 Realm(安全数据源)进行实际的认证操作。Realm 从数据源中查找与令牌匹配的用户记录,并验证凭据的有效性。
  3. 结果判定:根据 Realm 返回的认证结果,决定是否认证成功。如果所有关联的 Realm 中至少有一个成功验证了用户身份,通常认为认证成功(具体取决于配置的认证策略)。成功时,Authenticator 会创建或更新 Subject(当前用户主体)的身份信息,并将其绑定到当前会话中。

简而言之Authenticator 的主要任务是验证用户身份,确保只有合法用户能够访问受保护的资源或服务。

认证器策略:

AuthenticationStrategy(认证策略)则是Authenticator中的一个关键组件,用于定义如何处理多个Realm(领域)的认证结果。主要作用是在多个Realm参与认证时,决定如何合并和解释这些Realm的认证结果。当配置了多个Realm时,每个Realm都可能返回自己的认证结果,而AuthenticationStrategy则负责将这些结果整合成一个最终的认证结果。它是这样工作的:

  1. 发起认证:用户提交 AuthenticationToken(如用户名/密码对),Authenticator 开始认证流程。
  2. 遍历 RealmAuthenticator 使用配置的 AuthenticationStrategy,按照一定的顺序或策略遍历已注册的各个 Realm。对于每个 Realm,Authenticator 传递 AuthenticationToken 给 Realm,让其尝试进行认证。
  3. 收集结果:每个 Realm 完成认证后,返回一个 AuthenticationInfo(认证信息,如用户详情和凭据)或抛出异常(表示认证失败)。Authenticator 将这些结果收集起来,传递给 AuthenticationStrategy
  4. 策略评估AuthenticationStrategy 根据收集到的 Realm 认证结果,依据预设策略进行评估。常见的策略有:
    • At least one successful strategy(至少一个成功):只要有至少一个 Realm 认证成功,整个认证过程就算成功。这是最常用的策略,适用于多因素认证或多来源认证的情况,只要一个因素或来源通过即可。
    • All successful strategy(全部成功):所有 Realm 必须全部认证成功,整个认证过程才算成功。适用于需要极高安全性的场景,所有认证途径都必须验证无误。
    • First successful strategy(首个成功):一旦遇到第一个认证成功的 Realm,立即停止后续 Realm 的认证,并认定整个认证过程成功。适用于希望尽快结束认证过程,或者后续 Realm 认证成本较高的情况。
    • 决策反馈:根据策略评估的结果,AuthenticationStrategy 告知 Authenticator 认证是否成功。若成功,Authenticator 将认证成功的 AuthenticationInfo 与当前 Subject 绑定;若失败,Authenticator 可能抛出相应的异常或返回错误信息。

上面的源代码中可以看到,默认使用的是 ModularRealmAuthenticatorAtLeastOneSuccessfulStrategy

实例说明:

假设系统配置了三个 Realm:ApiAuthenticationRealm(API认证)、SystemAccountRealm(管理后台用户认证)、WxRealm(微信客户端认证)采用 At least one successful strategy

  1. 用户提交 AuthenticationToken
  2. Authenticator 首先调用 ApiAuthenticationRealm 进行认证,结果失败。
  3. Authenticator 继续调用 SystemAccountRealm,用户通过 SystemAccountRealm验证成功。
  4. Authenticator 收到成功结果,不再尝试 WxRealm,因为已经有一个 Realm 认证成功。
  5. AuthenticationStrategy 根据策略判断认证成功,因为至少有一个 Realm(SystemAccountRealm)成功。
  6. AuthenticatorSystemAccountRealm 返回的 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 确定认证策略

现在思考一下用户名,密码认证的方式SystemAccountRealmApiAuthenticationRealm 采用什么策略?很显然它们之间没有关联性,使用默认的AtLeastOneSuccessfulStrategy 策略。因为 SystemAccountRealm支持的token是 UsernamePasswordToken, 而 ApiAuthenticationRealm支持的Token是ApiAuthenticationToken ,每个Realm对应的Token是不一样,也没有关联关系,所以认证的时候,只需要匹配一个即可。

4.2 AuthenticationToken的创建

前面的案例中,我们定义了两个Filter,ApiAuthenticationFilterAuthenticationFilter, 这两个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,那过滤器链的顺序就比较重要了。这里有两种思路:

  1. 提供一个公共的Filter,让所有的请求都通过这个Filter,客户端在提交数据的时候,在请求头上加上标识,公共的Filter获取到这个标识后,创建对应的Token。
  2. 每种认证方式都有一个Filter,用URL匹配模式与这个Filter关联,每个Filter内创建自己的AuthenticationToken .

第一种方式需要在客户端请求头上加标识,只有一个Filter。如果后续再添加一个认证方式,就需要修改这个公共的Filter。而第二种方式遇到这种需求只需要新创建一个Filter,配置的时候,匹配一个URL即可,不需要修改原来的类。所以这里使用第2中方式。

如果采用第二种方式,原有Filter代码不用做任何更改,只需要修改配置,就可以适配两种认证方式。

4.3 配置

以下的方法代码,都在项目的 ShiroConfiguration 这个类中

  1. SecurityManager 安全管理器 : 使用ShiroWebAutoConfiguration 这个自动配置,它配置好了 DefaultWebSecurityManager 我们无需自己配置

  2. authenticator 认证器: 使用默认,无需配置

  3. authorizer 授权器: 使用默认,无需配置

  4. 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;
        }
    
  5. 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);
        }
    
  6. 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;
        }
    
  7. 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. 总结

  1. Shiro支持使用多个Realm进行认证,是由 认证器来完成工作的。默认使用的认证器是 ModularRealmAuthenticator,每个认证器需要为它配置认证策略,默认使用的是AtLeastOneSuccessfulStrategy. 其工作流程为:
    1. 用户发出URL请求
    2. 被Shiro过滤器拦截
    3. 过滤器创建AuthenticationToken, 后交给 securityManager, securityManager 再将token 交给认证器,认证器根据策略遍历realm ,调用realm的认证方法,最后根据策略来判断是否认证成功。
  2. Shiro支持使用多个Realm进行授权,是有授权器来完成工作的。默认使用的授权器是 ModularRealmAuthorizer
  3. 每一个过滤器创建一个特定的token,交给对应的realm,过滤器与匹配的URL相关联,这样就可以实现多种认证方式。

代码仓库 https://github.com/kaiwill/shiro-jwt , 本节代码在 8_springboot_shiro_jwt_多端认证鉴权_多Reaml管理.

相关推荐

  1. K8S RBAC

    2024-04-02 13:14:04       13 阅读
  2. springboot项目jwt认证(企业级实现方案)

    2024-04-02 13:14:04       18 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-02 13:14:04       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-02 13:14:04       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-02 13:14:04       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-02 13:14:04       20 阅读

热门阅读

  1. go-zero整合单机版Redis并实现增删改查

    2024-04-02 13:14:04       16 阅读
  2. 438 找到字符串中所有字母异味词

    2024-04-02 13:14:04       12 阅读
  3. springcloud基本使用四(Feign远程调用)

    2024-04-02 13:14:04       14 阅读
  4. 为什么型类型信息可以通过匿名内部类来保存

    2024-04-02 13:14:04       14 阅读
  5. 2404C++,C++ADL扩展库

    2024-04-02 13:14:04       15 阅读
  6. width:100%与width:auto区别

    2024-04-02 13:14:04       14 阅读
  7. mysql常见故障及mysql优化

    2024-04-02 13:14:04       17 阅读
  8. python之匿名函数

    2024-04-02 13:14:04       12 阅读
  9. 钉钉“牵手”微信,互联网“拆墙”大步迈进

    2024-04-02 13:14:04       14 阅读
  10. 技术与安全的交织

    2024-04-02 13:14:04       12 阅读
  11. mysql定时任务

    2024-04-02 13:14:04       14 阅读
  12. 递归解决图的深度遍历

    2024-04-02 13:14:04       15 阅读