之前在研究认证授权的过程中,简单研究过可以有多个realm,下面研究其多个realm 多种认证鉴权方式以及使用。
1. 单Reaml 认证鉴权过程
0. realm 认证过程:
可以看出,其本身是一个授权器Authorizer。 其作为认证器使用是需要作为认证器 Authenticator 内部的成员属性调用。
1. 自定义Realm
import com.beust.jcommander.internal.Lists; import com.zd.bx.bean.user.User; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class CustomRealm extends AuthorizingRealm { private static final Logger log = LoggerFactory.getLogger(CustomRealm.class); /** * 鉴权 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // getPrimaryPrincipal获取到的是doGetAuthenticationInfo方法最后存进去的user对象 Object primaryPrincipal = principalCollection.getPrimaryPrincipal(); if (primaryPrincipal == null) { return null; } SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User currentUser = (User) primaryPrincipal; // 添加角色 authorizationInfo.addRoles(Lists.newArrayList("管理员")); // 添加权限 authorizationInfo.addStringPermissions(Lists.newArrayList("user:manage:*", "dept:manage:*")); log.debug("authorizationInfo roles: {}, permissions: {}", authorizationInfo.getRoles(), authorizationInfo.getStringPermissions()); return authorizationInfo; } /** * 认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (authenticationToken == null || !(authenticationToken instanceof UsernamePasswordToken)) { return null; } User user = new User(); user.setPassword("111222"); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName()); } @Override public boolean supports(AuthenticationToken token) { log.info("token: {}", token); return token != null && UsernamePasswordToken.class.isAssignableFrom(token.getClass()); } }
2. 注入到SecurityManager 中
// 权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中 securityManager.setRealms(Lists.newArrayList(new CustomRealm())); return securityManager; }
查看其 org.apache.shiro.mgt.RealmSecurityManager#setRealms:
public void setRealms(Collection<Realm> realms) { if (realms == null) { throw new IllegalArgumentException("Realms collection argument cannot be null."); } if (realms.isEmpty()) { throw new IllegalArgumentException("Realms collection argument cannot be empty."); } this.realms = realms; afterRealmsSet(); }
主要的操作包括:设置到SecutityManager 自己的属性内部; 调用 afterRealmsSet() 方法进行后续处理。调用到: org.apache.shiro.mgt.AuthenticatingSecurityManager#afterRealmsSet
protected void afterRealmsSet() { super.afterRealmsSet(); if (this.authenticator instanceof ModularRealmAuthenticator) { ((ModularRealmAuthenticator) this.authenticator).setRealms(getRealms()); } }
可以看到是调用父类方法,然后设置到 authenticator 认证器内部。org.apache.shiro.mgt.RealmSecurityManager#afterRealmsSet: 是设置到缓存器和发布事件
protected void afterRealmsSet() { applyCacheManagerToRealms(); applyEventBusToRealms(); }
3. 调用链查看:
(1) 认证方法 doGetAuthenticationInfo 认证方法调用链:
(2) 授权方法 doGetAuthornizationInfo() 方法调用链:
2. 多realm 认证认证过程
在一个普通的web 工程中,一个realm 针对usernamePasswordToken 验证方式足够使用。有的时候需要多种认证方式。 假设我们需要根据微信的uniquecode 进行认证。
1. 新增token
package com.zd.bx.config.shiro; import org.apache.shiro.authc.AuthenticationToken; public class WechatToken implements AuthenticationToken { private String wechatUniqueName; public WechatToken(String wechatUniqueName) { this.wechatUniqueName = wechatUniqueName; } @Override public Object getPrincipal() { return wechatUniqueName; } @Override public Object getCredentials() { return wechatUniqueName; } public String getWechatUniqueName() { return wechatUniqueName; } }
2. 新增第二种realm, 验证WechatToken
package com.zd.bx.config.shiro; import com.zd.bx.bean.user.User; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class WechatRealm extends AuthorizingRealm { private static final Logger log = LoggerFactory.getLogger(WechatRealm.class); @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * 认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { if (authenticationToken == null || !(authenticationToken instanceof WechatToken)) { return null; } WechatToken wechatToken = (WechatToken) authenticationToken; User user = new User(); user.setPassword(wechatToken.getWechatUniqueName()); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName()); } @Override public boolean supports(AuthenticationToken token) { log.info("token: {}", token); return token != null && token instanceof WechatToken; } }
3. 设置到SecurityManager 中
// 权限管理,配置主要是Realm的管理认证 @Bean public SecurityManager securityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 注意realm必须在设置完认证其之后设置, 或者在设置 authenticator 的时候直接设置realm。setRealms 方法会将realm 同时设置到 authenticator 认证器中 securityManager.setRealms(Lists.newArrayList(new CustomRealm(), new WechatRealm())); return securityManager; }
4. 新增第二种登录方式
@GetMapping("/login2")
public String login2() {
Subject subject = SecurityUtils.getSubject();
AuthenticationToken generateToken = new UsernamePasswordToken("zs", "111222");
subject.login(generateToken);
return "success";
}
@GetMapping("/login3")
public String login3() {
Subject subject = SecurityUtils.getSubject();
WechatToken wechatToken = new WechatToken("qiaozhi");
subject.login(wechatToken);
return "success";
}
5. shiro 配置放开登录地址
/** * 路径 -> 过滤器名称1[参数1,参数2,参数3...],过滤器名称2[参数1,参数2...]... * 自定义配置(前面是路径, 后面是具体的过滤器名称加参数,多个用逗号进行分割,过滤器参数也多个之间也是用逗号分割)) * 有的过滤器不需要参数,比如anon, authc, shiro 在解析的时候接默认解析一个数组为 [name, null] */ FILTER_CHAIN_DEFINITION_MAP.put("/test2", "anon"); // 测试地址 FILTER_CHAIN_DEFINITION_MAP.put("/login2", "anon"); // 登陆地址 FILTER_CHAIN_DEFINITION_MAP.put("/login3", "anon"); // 登陆地址 FILTER_CHAIN_DEFINITION_MAP.put("/user/**", "roles[系统管理员,用户管理员],perms[user:manager:*]"); FILTER_CHAIN_DEFINITION_MAP.put("/dept/**", "perms[dept:manage:*]"); FILTER_CHAIN_DEFINITION_MAP.put("/**", "authc"); // 所有资源都需要经过验证
6. 测试
访问 /login 和 /login3 都可以进行认证成功,则证明生效。
7. 原理查看
1. org.apache.shiro.authc.pam.ModularRealmAuthenticator#doAuthenticate 获取认证信息
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); } }
2. org.apache.shiro.authc.pam.ModularRealmAuthenticator#doMultiRealmAuthentication
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) { AuthenticationStrategy strategy = getAuthenticationStrategy(); AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); if (log.isTraceEnabled()) { log.trace("Iterating through {} realms for PAM authentication", realms.size()); } for (Realm realm : realms) { try { aggregate = strategy.beforeAttempt(realm, token, aggregate); } catch (ShortCircuitIterationException shortCircuitSignal) { // Break from continuing with subsequnet realms on receiving // short circuit signal from strategy break; } if (realm.supports(token)) { log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm); AuthenticationInfo info = null; Throwable t = null; try { info = realm.getAuthenticationInfo(token); } catch (Throwable throwable) { t = throwable; if (log.isDebugEnabled()) { String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:"; log.debug(msg, t); } } aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); } else { log.debug("Realm [{}] does not support token {}. Skipping realm.", realm, token); } } aggregate = strategy.afterAllAttempts(token, aggregate); return aggregate; }
可以看到核心逻辑是在这里。
1》 getAuthenticationStrategy() 获取认证策略, 默认是AtLeastOneSuccessfulStrategy 至少有一个成功策略,总共的策略有:
2》 strategy.beforeAllAttempts(realms, token); 调用到 org.apache.shiro.authc.pam.AbstractAuthenticationStrategy#beforeAllAttempts 创建了一个SimpleAuthenticationInfo 对象。
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException { return new SimpleAuthenticationInfo(); }
3》遍历Realm 进行处理:
(1) 如果支持调用org.apache.shiro.authc.pam.AbstractAuthenticationStrategy#beforeAttempt 进行处理之前逻辑:
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException { return aggregate; }
(2) 首先调用 realm.supports(token) 判断是否支持验证指定的token, 不支持直接进行下一个realm。也就是重复 3》 过程
(3) realm.getAuthenticationInfo(token) 获取认证信息,这里会调用到realm, 先从缓存获取,获取不到调用doGetAuthenticationInfo 方法
(4) 调用afterAttempt 重置 aggregate 对象。 会调用到:org.apache.shiro.authc.pam.AbstractAuthenticationStrategy#afterAttempt
public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException { AuthenticationInfo info; if (singleRealmInfo == null) { info = aggregateInfo; } else { if (aggregateInfo == null) { info = singleRealmInfo; } else { info = merge(singleRealmInfo, aggregateInfo); } } return info; }
这里实际就是调用 org.apache.shiro.authc.SimpleAuthenticationInfo#merge 合并两个 info。 实际就是将单个realm 获取到的认证信息合并到aggregate 属性中
4》 最后的realm 处理完之后调用 strategy.afterAllAttempts(token, aggregate);, 这里调用到 org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy#afterAllAttempts 重写了父类的方法:
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException { //we know if one or more were able to successfully authenticate if the aggregated account object does not //contain null or empty data: if (aggregate == null || isEmpty(aggregate.getPrincipals())) { throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " + "could not be authenticated by any configured realms. Please ensure that at least one realm can " + "authenticate these tokens."); } return aggregate; }
也就是验证认证是否成功,如果走完所有的realm 都不成功则抛出异常。
这里可以看到针对org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy 策略的多realm 认证的方式是: 遍历所有的realm, 如果其 supports 返回true, 也就是支持验证该token。 进行token 的认证, 认证完之后将认证的信息合并到一个统一的SimpleAuthenticationInfo 对象aggregate 内部。 如果最后的aggregate 为空,或者其内部的认证对象Principals 为空则抛出异常。
3. 多Realm 授权过程
1.授权过程会调用到: org.apache.shiro.authz.ModularRealmAuthorizer#hasRole
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) { assertRealmsConfigured(); for (Realm realm : getRealms()) { if (!(realm instanceof Authorizer)) continue; if (((Authorizer) realm).hasRole(principals, roleIdentifier)) { return true; } } return false; }
2. 这里实际是调用多个realm, 判断其是否包含指定的角色, 对于权限验证也是类似的机制。
4. 切换多Realm 的认证策略
上面看到默认的认证策略是 AtLeastOneSuccessfulStrategy, 也就是多个realm 轮询进行认证判断,根据其是否支持指定的token 进行认证处理,最后合并认证结果。 如果想改成所有的认证都必须成功,也就是将认证策略改为:AllSuccessfulStrategy。
默认的三种认证策略是:
1. 源码查看
(1) org.apache.shiro.authc.pam.AbstractAuthenticationStrategy
package org.apache.shiro.authc.pam; import org.apache.shiro.authc.*; import org.apache.shiro.realm.Realm; import java.util.Collection; /** * Abstract base implementation for Shiro's concrete <code>AuthenticationStrategy</code> * implementations. * * @since 0.9 */ public abstract class AbstractAuthenticationStrategy implements AuthenticationStrategy { /** * Simply returns <code>new {@link org.apache.shiro.authc.SimpleAuthenticationInfo SimpleAuthenticationInfo}();</code>, which supports * aggregating account data across realms. */ public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException { return new SimpleAuthenticationInfo(); } /** * Simply returns the <code>aggregate</code> method argument, without modification. */ public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException { return aggregate; } /** * Base implementation that will aggregate the specified <code>singleRealmInfo</code> into the * <code>aggregateInfo</code> and then returns the aggregate. Can be overridden by subclasses for custom behavior. */ public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException { AuthenticationInfo info; if (singleRealmInfo == null) { info = aggregateInfo; } else { if (aggregateInfo == null) { info = singleRealmInfo; } else { info = merge(singleRealmInfo, aggregateInfo); } } return info; } /** * Merges the specified <code>info</code> argument into the <code>aggregate</code> argument and then returns an * aggregate for continued use throughout the login process. * <p/> * This implementation merely checks to see if the specified <code>aggregate</code> argument is an instance of * {@link org.apache.shiro.authc.MergableAuthenticationInfo MergableAuthenticationInfo}, and if so, calls * <code>aggregate.merge(info)</code> If it is <em>not</em> an instance of * <code>MergableAuthenticationInfo</code>, an {@link IllegalArgumentException IllegalArgumentException} is thrown. * Can be overridden by subclasses for custom merging behavior if implementing the * {@link org.apache.shiro.authc.MergableAuthenticationInfo MergableAuthenticationInfo} is not desired for some reason. */ protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) { if( aggregate instanceof MergableAuthenticationInfo ) { ((MergableAuthenticationInfo)aggregate).merge(info); return aggregate; } else { throw new IllegalArgumentException( "Attempt to merge authentication info from multiple realms, but aggregate " + "AuthenticationInfo is not of type MergableAuthenticationInfo." ); } } /** * Simply returns the <code>aggregate</code> argument without modification. Can be overridden for custom behavior. */ public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException { return aggregate; } }
(2) AtLeastOneSuccessfulStrategy 主要重写了afterAllAttempts 验证是否认证成功,认证失败抛出异常
package org.apache.shiro.authc.pam; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.PrincipalCollection; /** * <tt>AuthenticationStrategy</tt> implementation that requires <em>at least one</em> configured realm to * successfully process the submitted <tt>AuthenticationToken</tt> during the log-in attempt. * <p/> * <p>This means any number of configured realms do not have to support the submitted log-in token, or they may * be unable to acquire <tt>AuthenticationInfo</tt> for the token, but as long as at least one can do both, this * Strategy implementation will allow the log-in process to be successful. * <p/> * <p>Note that this implementation will aggregate the account data from <em>all</em> successfully consulted * realms during the authentication attempt. If you want only the account data from the first successfully * consulted realm and want to ignore all subsequent realms, use the * {@link FirstSuccessfulStrategy FirstSuccessfulAuthenticationStrategy} instead. * * @see FirstSuccessfulStrategy FirstSuccessfulAuthenticationStrategy * @since 0.2 */ public class AtLeastOneSuccessfulStrategy extends AbstractAuthenticationStrategy { private static boolean isEmpty(PrincipalCollection pc) { return pc == null || pc.isEmpty(); } /** * Ensures that the <code>aggregate</code> method argument is not <code>null</code> and * <code>aggregate.{@link org.apache.shiro.authc.AuthenticationInfo#getPrincipals() getPrincipals()}</code> * is not <code>null</code>, and if either is <code>null</code>, throws an AuthenticationException to indicate * that none of the realms authenticated successfully. */ public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException { //we know if one or more were able to successfully authenticate if the aggregated account object does not //contain null or empty data: if (aggregate == null || isEmpty(aggregate.getPrincipals())) { throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " + "could not be authenticated by any configured realms. Please ensure that at least one realm can " + "authenticate these tokens."); } return aggregate; } }