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

news2024/11/25 3:04:55

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管理.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1561711.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL中MHA故障排查

文章目录 MySQL故障排查MySQL主从环境常见故障1、故障一1.1 故障现象1.2 报错原因1.3 解决方法 2、故障二2.1 故障现象2.2 报错原因2.3 解决方法 3、故障三3.1 故障现象3.2 报错原因3.3 解决方法 4、故障四4.1 故障现象4.2 问题分析4.3 解决方法 5、故障五5.1 故障现象5.2 报错…

neo4j使用详解(六、cypher即时时间函数语法——最全参考)

Neo4j系列导航&#xff1a; neo4j及简单实践 cypher语法基础 cypher插入语法 cypher插入语法 cypher查询语法 cypher通用语法 cypher函数语法 6.时间函数-即时类型 表示具体的时刻的时间类型函数 6.1.date函数 年-月-日时间函数&#xff1a; yyyy-mm-dd 6.1.1.获取date da…

深度学习500问——Chapter05: 卷积神经网络(CNN)(3)

文章目录 5.14 理解转置卷积与棋盘效应 5.14.1 标准卷积 5.14.2 转置卷积 5.15 卷积神经网络的参数设置 5.16 提高卷积神经网络的泛化能力 5.17 卷积神经网络在不同领域的应用 5.17 .1 联系 5.17.2 区别 5.14 理解转置卷积与棋盘效应 5.14.1 标准卷积 在理解转置卷积之前&…

从学习海底捞到学习巴奴,中国餐饮带洋快餐重归“产品主义”

俗话说“民以食为天”&#xff0c;吃饭一向是国人的头等大事&#xff0c;餐饮业也是经济的强劲助推力。新世纪以来&#xff0c;餐饮业不断讲述着热辣滚烫的商业故事。 2006年&#xff0c;拥有“必胜客”、“肯德基”等品牌的餐饮巨头百胜集团&#xff0c;组织两百多名区域经理…

代码随想录算法训练营第24天|理论基础 |77. 组合

理论基础 jia其实在讲解二叉树的时候&#xff0c;就给大家介绍过回溯&#xff0c;这次正式开启回溯算法&#xff0c;大家可以先看视频&#xff0c;对回溯算法有一个整体的了解。 题目链接/文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;带你学透回溯算法&#xff08;理…

Windows安装禅道系统结合Cpolar实现公网访问内网BUG管理服务

文章目录 前言1. 本地安装配置BUG管理系统2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射本地服务3. 测试公网远程访问4. 配置固定二级子域名4.1 保留一个二级子域名5.1 配置二级子域名6. 使用固定二级子域名远程 前言 BUG管理软件,作为软件测试工程师的必备工具之一。在…

竞技之道-打造成功竞技游戏的实战指南【文末送书】

文章目录 理解竞技游戏的本质游戏力&#xff1a;竞技游戏设计实战教程【文末送书】 在当今数字化时代&#xff0c;游戏已经不再是一种单纯的娱乐方式&#xff0c;而是成为了一门具有巨大商业潜力的产业。特别是竞技游戏&#xff0c;它们引领着全球数十亿玩家的潮流&#xff0c;…

引用,内联函数,auto函数,指针nullptr

一&#xff1a;引用 1.1 该文章的引用是对上一篇引用的进行补充和完善 按理来说&#xff0c;double可以隐式转换为int&#xff0c;那起别名的时候为什么不可以类型转换呢&#xff1f; 那是因为&#xff0c;在类型转换的时候&#xff0c;会创建一个临时变量&#xff0c;让后再…

基于kalman的单目标追踪,以及demo测试(Python and C++)

一.卡尔曼滤波简单介绍 我们可以在任何含有不确定信息的动态系统中的使用卡尔曼滤波&#xff0c;对系统的下一步动作做出有根据的猜测。猜测的依据是预测值和观测值&#xff0c;首先我们认为预测值和观测值都符合高斯分布且包含误差&#xff0c;然后我们预设预测值的误差Q和观测…

OMNet项目1 —— Linux环境配置

项目环境搭建&#xff0c;软件版本Ubuntu16&#xff0c;OMNet5.0 Linux配置环境步骤 安装VMWare虚拟机16.25&#xff08;个人号养老版本&#xff09;下载ISO镜像文件Ubuntu16 链接&#xff1a;https://pan.baidu.com/s/1SETyn6t4qIUfli1uRRgm3w?pwdf4ua 提取码&#xff1a;f…

软件设计师25--逻辑结构设计

软件设计师25--逻辑结构设计 考点1&#xff1a;关系模式相关概念数据模型关系模型相关概念完整性约束 考点2&#xff1a;E-R图转换关系模式逻辑结构设计 - E-R模型转关系模式E - R图转关系模式 考点1&#xff1a;关系模式相关概念 数据模型 层次模型网状模型关系模型面向对象…

第四百三十六回

文章目录 1. 概念介绍2. 思路与方法2.1 实现思路2.2 实现方法 3. 示例代码4. 内容总结 我们在上一章回中介绍了"不同平台上换行的问题"相关的内容&#xff0c;本章回中将介绍如何在页面上显示蒙板层.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我…

ElMessageBox.confirm中内容换行

ElMessageBox.confirm(导入结果&#xff1a;<br/>成功导入${res.successCount}条数据&#xff0c;导入失败${res.errorList.length}条数据。<br/>${str},"提示",{confirmButtonText: "确定",cancelButtonText: "取消",type: "w…

云原生应用(5)之Dockerfile精讲及新型容器镜像构建技术

一、容器与容器镜像之间的关系 说到Docker管理的容器不得不说容器镜像&#xff0c;主要因为容器镜像是容器模板&#xff0c;通过容器镜像我们才能快速创建容器。 如下图所示&#xff1a; Docker Daemon通过容器镜像创建容器。 二、容器镜像分类 操作系统类 CentOS Ubuntu 在…

38.HarmonyOS鸿蒙系统 App(ArkUI)堆叠布局结合弹性布局

层叠布局用于在屏幕上预留一块区域来显示组件中的元素&#xff0c;提供元素可以重叠的布局。层叠布局通过Stack容器组件实现位置的固定定位与层叠&#xff0c;容器中的子元素&#xff08;子组件&#xff09;依次入栈&#xff0c;后一个子元素覆盖前一个子元素&#xff0c;子元素…

Spring源码分析(BeanDefinition)

文章目录 Spring源码分析&#xff08;BeanDefinition&#xff09;一、概述1、BeanDefinition 的理解2、BeanDefinition 接口3、BeanDefinition 的实现4、BeanDefinitionHolder 类 二、BeanDefinition 的加载1、reader 的获取1&#xff09;registerAnnotationConfigProcessors2&…

浅谈高阶智能驾驶-NOA领航辅助的技术与发展

浅谈高阶智能驾驶-NOA领航辅助的技术与发展 附赠自动驾驶学习资料和量产经验&#xff1a;链接 2019年在国内首次试驾特斯拉NOA领航辅助驾驶的时候&#xff0c;当时兴奋的觉得未来已来;2020年在试驾蔚来NOP领航辅助驾驶的时候&#xff0c;顿时不敢小看国内新势力了;现在如果哪家…

第十八章 算法

一、介绍 1.1 什么是算法 算法&#xff08;Algorithm&#xff09;是指解题方案的准确而完整的描述&#xff0c;是一系列解决问题的清晰指令&#xff0c;算法代表着用系统的方法描述解决问题的策略机制。也就是说&#xff0c;能够对一定规范的输入&#xff0c;在有限时间内获…

kubernetes之实战进阶篇

目录 一、搭建kubenetes集群 1.1、搭建方案选择 1.2、软硬件准备 1.2.1、硬件要求: 1.2.2、软件要求 1.3、安装步骤 1.3.1、初始化操作(三个节点都要执行一遍) 1.3.2、部署kubernetes master节点(控制面板) 1.3.3、node节点加入k8s集群 1.3.4、部署CNI网络插件 1.3.…

【经典算法】LeetCode14:最长公共前缀(Java/C/Python3实现含注释说明,Easy)

最长公共前缀 题目思路及实现方式一&#xff1a;横向扫描思路代码实现Java版本C语言版本Python3版本 复杂度分析 方式二&#xff1a;纵向扫描思路代码实现Java版本C语言版本Python3版本 复杂度分析 方式三&#xff1a;分治思路代码实现Java版本C语言版本Python3版本 复杂度分析…