Apache Shiro多Realm认证过程,亲测可用!

背景

如果项目中使用了shiro进行认证及授权,那么,大多数情况下,项目都只有一个数据源,也就是指向一个同数据库。

在一个中、大型项目中,也有可能存在多个数据源的情况。这里的多数据源指的是人员、组织机构、权限角色等数据在一个项目中可能使用不同的数据库来存储。而不是指的业务数据库。

举个例子:企业的OA系统一般又称为协同运营系统,协调运营平台一般要求和各个子系统建立完整的数据通道,避免产生信息孤岛,这就要求平台必须要有统一认证的能力。然而,在平台建设初期,尤其是各个子系统(子公司)无法提供完整的人员和组织机构的条件下,建立同一套认证数据源简直是难上加难。

所以,为了便于OA平台统一进行认证,我们在后端项目中就要集成多个组织机构、人员的数据源,即,一个项目中就会存在多套子系统人员和组织机构的数据,那么,一套认证过程和一个数据源是一一对应的,多套认证过程就会对应多套数据源。

shiro认证过程

在实现当前应用场景之前,我们先来了解一下shiro的认证过程,以及单realm的实现方式:

在以前传统的单体项目中,还没有使用类似shiro或sa-token的认证框架,我记得都是我们开发人员自己去封装这些方法,通常来说,传递页面的用户名和密码数据,传到后台按照一定的规则生成token信息,并保存到session中,当请求接口时,会调用后台的过滤器或拦截器进行认证。在认证的过程中要从数据库取相关的信息进行校验和判断用户是否合法。

同样的和以往的实现方式类似,shiro框架只是在我们刚刚说的认证逻辑中进行了封装,其包含以下几个元素信息:

  • Principal :简单理解就是用户名

  • Credentials:简单来说就是用户密码

  • Subject:翻译过来为主体信息,它可以理解为用户。专业来讲,Subject只是应用程序User的特定于安全性的“视图”。实际上也可以叫它“User”,因为这样“才有意义”,因为太多的应用程序已经有了自己的User类/框架的现有api,而叫Subject的原因在于shiro不想与User发生冲突。一般shiro使用Subject来标识当前用户的身份,使用方式:Subject currentUser = SecurityUtils.getSubject();

  • Realm 和数据源(数据库)一一对应。提供安全数据源,认证时会从realm中获取用户、权限、角色相关的数据来和用户输入的信息进行比对。后面会详细介绍。

好了,现在我们来看看如何使用shiro进行认证,认证过程和传统的自定义认证过程有什么区别。

1、首先,当前端收集好用户名密码并传递给后台的过滤后,在过滤器的逻辑中将用户名(userName )和密码(password)通过一定的规则生成token,比如shiro官网提供的生成token的api,如下

UsernamePasswordToken token = new UsernamePasswordToken(username, password);

当然我们也可以使用JWT-token 的方式来生成token,此token为AuthenticationToken的实例。

2、将token作为参数传递给shiro获取Subject信息,说是获取,实际上,在内容进行了判断,如果为空则会重新创建Subject主体信息。

Subject currentUser = SecurityUtils.getSubject();

这样就给当前用户生成了一个全局唯一的主体对象,它是存在一个单独的线程上下文环境中的。我们可以看看其中的源码,其中 DelegatingSubject 是Subject 接口的实现类。

public DelegatingSubject(PrincipalCollection principals, boolean authenticated, String host, Session session, boolean sessionCreationEnabled, SecurityManager securityManager) {
if (securityManager == null) {
throw new IllegalArgumentException("SecurityManager argument cannot be null.");
} else {
this.securityManager = securityManager;
this.principals = principals;
this.authenticated = authenticated;
this.host = host;
if (session != null) {
this.session = this.decorate(session);
}

this.sessionCreationEnabled = sessionCreationEnabled;
}
}

3、生成Subject后则进行登录认证,简单来说,这一步就是拿前端收集的用户密码对应的token,然后将token(A)信息传递给shiro进行认证,具体的认证过程就用到了Realm(从Realm中获取真实的用户数据B),将A和B进行比较,一致则认为登录成功,否则为失败。

currentUser.login(token);

登录认证的源码比较多,这里简单描述一下。在shiro中有个安全管理器叫做SecurityManager,它主要用来进行认证过程的管理,其子类RealmSecurityManager 用来管理realm 。而AuthenticatingSecurityManager 用来对Realm进行认证过程的管理。

好了,现在知道3者的关系后,我们就来看看AuthenticatingSecurityManager 定义了什么,在AuthenticatingSecurityManager 中定义了一个认证器,如下:

private Authenticator authenticator = new ModularRealmAuthenticator();

通过实现这个ModularRealmAuthenticator 认证器,我们可以定义自己的认证器,这也是shiro集成过程所要求的。

使用认证器认证的过程,使用了Realm来从数据库获取当前用户的真实数据。那么,这个realm到底是在什么时候定义的?Realm是如何实现的?

Realm

在传统的项目中,我们通常直接查询数据库来和前端传来的用户名密码进行对比验证,而shiro则定义了一个类似DAO层的API来供开发者定义自己的查询数据库的逻辑,这个就是Realm的作用,说到这里,我们就可以清楚的理解了Realm的作用。而这个Realm又分为单Realm 和 多Realm。由于是查询数据库的逻辑,所以一个Realm对应了一个数据源。

从这个意义上讲,Realm本质上是一个特定于安全性的DAO:它封装数据源的连接细节,并根据需要将相关数据提供给Shiro。在配置Shiro时,必须指定至少一个用于身份验证和/或授权的Realm。SecurityManager可以配置多个realm,但至少需要一个。

Apache Shiro多Realm认证过程,亲测可用!

下面我们来看看Realm如何来使用:

我们先来定义一个CustomerRealm ,让它来继承AuthorizingRealm,其中包括两个需要实现的方法

public class CustomerRealm extends AuthorizingRealm {

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//从数据库获取用户信息
//从数据库中获取密码(redis中获取token)
//两个参数分别为用户信息和密码信息,传给shiro(secuitymanager)进行登录认证。
// 不应该直接传递传来的token,应该传递数据库查询的token,否则在realm认证过程中永远都是成功的。)
        return new SimpleAuthenticationInfo(userAuthority, token, getName());
}
}

而这个CustomerRealm 的调用恰恰就是刚刚currentUser.login(token); 代码块调用的。

在前面我们说过,我们需要定义自己的认证器,因为在currentUser.login(token); 时,其内部就是调用了我们自己的认证器进行认证的,而在认证器中又获取到了刚刚的CustomerRealm来完成认证的过程。

自定义认证器如下:

public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {

private static final Logger logger = LogManager.getLogger(CustomModularRealmAuthenticator.class);

@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("------------------------CustomModularRealmAuthenticator");

assertRealmsConfigured();
Collection<Realm> realms = getRealms();

//获取自定义token
JWTToken jwtToken = (JWTToken) authenticationToken;
//获取登录类型
String loginType = jwtToken.getLoginType() ;

//根据登录类型获取当前要执行的realm
Collection<Realm> authRealms = new ArrayList<>();
for(Realm realm:realms){
// realm.getName() 是全类名
if(realm.getClass().getSimpleName().startsWith(loginType)){
authRealms.add(realm);
}
}
if (authRealms.size() == 1) {
return doSingleRealmAuthentication(authRealms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(authRealms, authenticationToken);
}

}
}

如上代码,在Collection<Realm> realms = getRealms(); 代码块中获取到realm,然后将realm传递给doSingleRealmAuthentication :

public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
if (info == null) {
info = this.doGetAuthenticationInfo(token);
log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info);
if (token != null && info != null) {
this.cacheAuthenticationInfoIfPossible(token, info);
}
} else {
log.debug("Using cached authentication info [{}] to perform credentials matching.", info);
}

if (info != null) {
this.assertCredentialsMatch(token, info);
} else {
log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token);
}

return info;
}

重点看这个代码块 info =this.doGetAuthenticationInfo(token); 这里就直接调用到了刚刚自定义的CustomerRealm中的doGetAuthenticationInfo方法了,从而返回认证信息。

Realm是在何时初始化的?

我们可以定义一个配置类来初始化自定义认证器和CustomerRealm的实例,如下:

@Configuration
public class ShiroConfig {

/**
* 配置realm
*/

@Bean
public CustomerRealm customerRealm(){
return new CustomerRealm();
}


/**
* 配置平台自定义认证器
*/

@Bean
public CustomModularRealmAuthenticator customModularRealmAuthenticator(){
CustomModularRealmAuthenticator customModularRealmAuthenticator = new CustomModularRealmAuthenticator();
return customModularRealmAuthenticator;
}

@Bean("securityManager")
public DefaultWebSecurityManager getManager() {

//定义securityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

//使用自定义的认证器
CustomModularRealmAuthenticator customModularRealmAuthenticator = new CustomModularRealmAuthenticator();
securityManager.setAuthenticator(customModularRealmAuthenticator);

//使用自定义的授权器
CustomModularRealmAuthorizer customModularRealmAuthorizer =new CustomModularRealmAuthorizer();
securityManager.setAuthorizer(customModularRealmAuthorizer);

// securityManager配置realm
Collection<Realm> realms = new ArrayList<>();
realms.add(CustomerRealm());
securityManager.setRealms(realms);

/*
* 关闭shiro自带的session,详情见文档
*/

DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);

return securityManager;
}
}

ok,到此我们基本清楚了shiro的单Realm的认证过程,那么如果对于开头提出的背景,我们该如何实现呢?

多Realm认证过程

在单realm认证过程中项目的目录是这样的:

Apache Shiro多Realm认证过程,亲测可用!

那么多Realm其实就是这样的:

Apache Shiro多Realm认证过程,亲测可用!

多Realm的认证过程只需要改如下几个地方:

1、初始化配置类,有多少个Realm就加入多少个

2、自定义多个Realm实例

3、通过for循环的方式按每个Realm的顺序执行

配置类如下:

@Configuration
public class ShiroConfig {

/**
* 第一个realm
*/

@Bean
public ARealm aRealm(){
return new ARealm();
}

/**
* 第二个Realm
*/

@Bean
public BRealm bRealm(){
return new BRealm();
}

/**
* 配置平台自定义认证器
*/

@Bean
public CustomModularRealmAuthenticator customModularRealmAuthenticator(){
CustomModularRealmAuthenticator customModularRealmAuthenticator = new CustomModularRealmAuthenticator();
return customModularRealmAuthenticator;
}

@Bean("securityManager")
public DefaultWebSecurityManager getManager() {

//定义securityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

//使用自定义的认证器
CustomModularRealmAuthenticator customModularRealmAuthenticator = new CustomModularRealmAuthenticator();
securityManager.setAuthenticator(customModularRealmAuthenticator);

//使用自定义的授权器
CustomModularRealmAuthorizer customModularRealmAuthorizer =new CustomModularRealmAuthorizer();
securityManager.setAuthorizer(customModularRealmAuthorizer);

// securityManager配置多realm
Collection<Realm> realms = new ArrayList<>();
realms.add(ARealm());
realms.add(BRealm());
securityManager.setRealms(realms);

/*
* 关闭shiro自带的session
*/

DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);

return securityManager;
}

定义ARealm 和 BRealm 。这里同上面的CustomerRealm配置方式,分别配置查询数据库即可,这里不做说明。

/**
* @author :ni
* @date :2023/2/20 11:10 上午
* @description:自定义认证器,主要用于处理和区分不同(多)平台的认证逻辑。
*/

public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {

private static final Logger logger = LogManager.getLogger(CustomModularRealmAuthenticator.class);

@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("------------------------CustomModularRealmAuthenticator");

assertRealmsConfigured();
Collection<Realm> realms = getRealms();

//获取自定义token
JWTToken jwtToken = (JWTToken) authenticationToken;
//获取登录类型
String loginType = jwtToken.getLoginType() ;

// loginType = loginType + "Realm";
//根据登录类型获取当前要执行的realm
Collection<Realm> authRealms = new ArrayList<>();
for(Realm realm:realms){
// 前端若没传loginType,暂时赋值loginType默认值。
if (StringUtils.isNullOrEmpty(loginType)) {
loginType = UserConstants.Default_LOGIN_TYPE;
}
// realm.getName() 是全类名
if(realm.getClass().getSimpleName().startsWith(loginType)){
authRealms.add(realm);

}
}
if (authRealms.size() == 1) {
return doSingleRealmAuthentication(authRealms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(authRealms, authenticationToken);
}

}

从认证器的代码可以看出,如果有两个数据源,那么就对应两个realm,如果前端想区分每一次登录走不同的realm,比如:用户A在OA系统登录走的是ARealm,用户B在OA系统登录走的是BRealm, 那么这种情况该如何区分走不同的realm呢?其实很简单,就是在认证器中定义一个loginType 用来区分是谁登录,该走哪个Realm(该调用哪个数据库获取用户信息),那么这个loginType 就可以通过前端传递过来,然后当走到认证器时就自动区分开该走哪个realm了。通过上述代码来看,多realm还是走的单realm。

下面针对开头的背景实现给大家提供一个方案图,其实现为:使用shiro + jwt +redis 进行认证。

Apache Shiro多Realm认证过程,亲测可用!

总结

本文主要从shiro源码的基础上分析了基本认证过程,并详细说明了使用shiro的核心步骤,重点说明了realm的运作过程,以及从实际的应用背景出发,描述了多realm的实现方案。以上流程图及方案代码亲测可用,感谢大家的阅读。

原文始发于微信公众号(小核桃编程):Apache Shiro多Realm认证过程,亲测可用!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/215961.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!