写在前面

本文基于上一篇文章《 Shiro + Springboot + JWT 的整合》延续,示例项目也是在上篇文章的基础上扩展而来的。

一个系统一般都具有多种用户类型,如管理员、普通用户和运营者账号等,这些用户都存储在不同的表中。当然,通过对 Token 的内容进行条件判断,一个 Realm 是可以直接实现多个账号登录的,但是这样将导致不同用户的登录完全耦合在一起,这不是我们想看到的,所以有了多 Realm 的需求,并且 Shiro 是支持多 Realm 的。

1. 实现逻辑

1.1 分析

通过阅读 Shiro 部分源码可以发现,Shiro 是支持,但是 Shiro 的多 Realm 实现逻辑并不符合本文的场景,部分源码如下:

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    assertRealmsConfigured();
    Collection<Realm> realms = getRealms();
    if (realms.size() == 1) {
        return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
    } else {
        return doMultiRealmAuthentication(realms, authenticationToken);
    }
}

这是 Shiro 认证步骤选择 Realm 进行认证的实现,可以看到,如果有多个 Realm 将对每个都进认证,而我们希望的是管理员。用户等都只调用自己的 Realm,所以对这些地方的逻辑要稍微做些改动。

1.2 实现

主要改动包括三方面:

  1. TokenFilter 中将当前 Token 是属于那种用户类型的信息一同封装到 AuthenticationToken 记录下来。
  2. 认证步骤要实现自己调用自己的 Realm,需要复写 ModularRealmAuthenticator 类以下方法。
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException;
  1. 授权步骤要实现自己调用自己的 Realm,需要复写 ModularRealmAuthorizer 类以下方法。
public boolean isPermitted(PrincipalCollection principals, String permission);
public boolean isPermitted(PrincipalCollection principals, Permission permission);
public boolean hasRole(PrincipalCollection principals, String roleIdentifier);

2. 实现源码

本文章针对于已经如何简单运用 Shiro 的人,对于创建的多个 Realm 类、登录逻辑的修改等代码段此处就没有再贴出,详细内容可看项目源码,可直接运行。

示例项目已经上传到 Github 直接可用:https://github.com/nineya/framework-study/tree/v0.2.0/shiro-study

2.1 自定义 AuthenticationToken

自定义 AuthenticationToken,包含登录用户类型信息,并在 TokenFilter 中使用该类型。

package com.nineya.shiro.entity;

/**
 * @author 殇雪话诀别
 * 2021/2/17
 */
public enum LoginType {
    USER, MANAGE;
}

package com.nineya.shiro.entity;

import org.apache.shiro.authc.BearerToken;

/**
 * 继承实现一个自定义的带有登录类型的Token
 * @author 殇雪话诀别
 * 2021/2/17
 */
public class JwtToken extends BearerToken {
    private final LoginType loginType;

    public JwtToken(LoginType loginType, String token, String host) {
        super(token, host);
        this.loginType = loginType;
    }

    public LoginType getLoginType() {
        return loginType;
    }
}
    /**
     * 用户登录
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        LoginType loginType = null;
        String token = httpServletRequest.getHeader(AUTHORIZATION_HEADER);
        if (token != null) {
            loginType = LoginType.USER;
        } else {
            token = httpServletRequest.getHeader(MANAGE_AUTHORIZATION);
            if (token != null) {
                loginType = LoginType.MANAGE;
            }
        }
        JwtToken jwtToken = new JwtToken(loginType, token, request.getRemoteAddr());
        getSubject(request, response).login(jwtToken);
        return true;
    }

2.2 修改认证步骤实现

package com.nineya.shiro.config;

import com.nineya.shiro.entity.JwtToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;

import java.util.Collection;

/**
 * @author 殇雪话诀别
 * 2021/2/17
 */
public class StudyModularRealmAuthenticator extends ModularRealmAuthenticator {
    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 判断 Realm 是否为空
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        JwtToken jwtToken = (JwtToken) authenticationToken;
        String loginType = jwtToken.getLoginType().name();
        for (Realm realm : realms) {
            if (realm.getName().equals(loginType)) {
                return doSingleRealmAuthentication(realm, authenticationToken);
            }
        }
        return null;
    }
}

2.3 修改授权步骤实现

package com.nineya.shiro.config;

import org.apache.shiro.authz.Authorizer;
import org.apache.shiro.authz.ModularRealmAuthorizer;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.Set;

/**
 * @author 殇雪话诀别
 * 2021/2/18
 */
public class StudyModularRealmAuthorizer extends ModularRealmAuthorizer {
    @Override
    public boolean isPermitted(PrincipalCollection principals, String permission) {
        assertRealmsConfigured();
        Set<String> realmNames = principals.getRealmNames();
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)) continue;
            // 仅比较 realmName 对应得上的 realm
            if (realmNames.contains(realm.getName())) {
                return ((Authorizer) realm).isPermitted(principals, permission);
            }
        }
        return false;
    }

    @Override
    public boolean isPermitted(PrincipalCollection principals, Permission permission) {
        assertRealmsConfigured();
        Set<String> realmNames = principals.getRealmNames();
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)) continue;
            // 仅比较 realmName 对应得上的 realm
            if (realmNames.contains(realm.getName())) {
                return ((Authorizer) realm).isPermitted(principals, permission);
            }
        }
        return false;
    }

    @Override
    public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
        assertRealmsConfigured();
        Set<String> realmNames = principals.getRealmNames();
        for (Realm realm : getRealms()) {
            if (!(realm instanceof Authorizer)) continue;
            // 仅比较 realmName 对应得上的 realm
            if (realmNames.contains(realm.getName())) {
                return ((Authorizer) realm).hasRole(principals, roleIdentifier);
            }
        }
        return false;
    }
}

2.4 配置中运用

    /**
     * 权限管理,配置主要是Realm的管理认证,同时可以配置缓存管理等
     *
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(List<Realm> realms) {
        DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
        webSecurityManager.setAuthenticator(modularRealmAuthenticator());
        webSecurityManager.setAuthorizer(modularRealmAuthorizer());
        //realm管理,必须在两个modular之后,因为会对这两个对象进行设值
        webSecurityManager.setRealms(realms);
        return webSecurityManager;
    }

    /**
     * 针对多realm,用于认证阶段
     */
    @Bean
    public ModularRealmAuthenticator modularRealmAuthenticator() {
        //自己重写的ModularRealmAuthenticator
        StudyModularRealmAuthenticator modularRealmAuthenticator = new StudyModularRealmAuthenticator();
        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealmAuthenticator;
    }

    /**
     * 针对多realm,用于授权阶段
     * @return
     */
    @Bean
    public ModularRealmAuthorizer modularRealmAuthorizer() {
        return new StudyModularRealmAuthorizer();
    }