shiro多realm整合的spring-boot案例剖析
概述:shiro认证的流程主要是通过securityManager调用login(Subject subject, AuthenticationToken token)方法,实际上委托的是Authenticator(认证器进行认证),默认情况下使用的是
ModularRealmAuthenticator认证器,如果我们只有1个realm,最终的认证操作就是调用这个realm,如果我们有多个realm呢?这个时候就需要我们对ModularRealmAuthenticator认证器进行扩展。
一、下面是subject.login()执行的代码调用片段
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
上面是DelegatingSubject的login方法
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = authenticate(token);
} catch (AuthenticationException ae) {
}
}
上面是DefaultSecurityManager的login方法
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
实际调用的是AuthenticatingSecurityManager的authenticate方法, 这个this.authenticator就是ModularRealmAuthenticator。
二、ModularRealmAuthenticator默认的多reaml是按什么策略进行处理的呢?
1、authenticate方法的公共处理,实际上调用的是doAuthenticate(token)方法。
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = doAuthenticate(token); // 实际主要的认证方法
if (info == null) { // 没有返回info就抛出auth异常
throw new AuthenticationException(msg);
}
} catch (Throwable t) {
AuthenticationException ae = null;
if (t instanceof AuthenticationException) {
ae = (AuthenticationException) t;
}
if (ae == null) {
ae = new AuthenticationException(msg, t);
} // 任何认证过程异常包装成auth异常进行抛出
throw ae;
}
return info;
}
2、我们看看doAuthenticate的默认实现,根据配置了realms的个数来判断是调用单个还是doMultiRealmAuthentication()方法
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);
}
}
3、在说doMultiRealmAuthentication方法前,我们可以在ModularRealmAuthenticator类里找到new的AuthenticationStrategy策略。这个策略决定了doMultiRealmAuthentication的实现方式。我们先看看AbstractAuthenticationStrategy的默认实现。
public abstract class AbstractAuthenticationStrategy implements AuthenticationStrategy {
// 全部realm认证前--创建一个空info
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
return new SimpleAuthenticationInfo();
}
// 单个realm认证前--直接返回
public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
return aggregate;
}
// 单个realm认证前--将上一个和此次的info进行合并
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;
}
// 全部认证后--直接返回
public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
return aggregate;
}
}
它有三个实现子类。
FirstSuccessfulStrategy: 是只要有1个认证成功,认证信息返回第一个认证信息
原理: 步骤1( 见下面的代码 )所有认证返回null, 步骤3,第一次info不为null,返回info赋值给aggregate,此后一直返回aggregate。
public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token){
return null; // 返回null,保证合并方法执行时,认证后第一次info不为null,将info返回。
}
protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
if (aggregate != null && isEmpty(aggregate.getPrincipals())) { // 当info赋值给aggregate后,持续返回aggregate。
return aggregate;
}
return info != null ? info : aggregate; // 认证后第一次info不为null,就返回info,merge()方法即afterAttempt方法返回。
}
AtLeastOneSuccessfulStrategy:在firstSuccessfulStrategy基础上,会初始创建aggregate,并且步骤3是默认合并info和aggregate。
AllSuccessfulStrategy:所有都必须认证成功,原理即:afterAttempt会对每一个realm的结果进行校验。
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy strategy = getAuthenticationStrategy();
AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token); // 1默认实现 new SimpleAuthenticationInfo();
for (Realm realm : realms) {
aggregate = strategy.beforeAttempt(realm, token, aggregate); // 2默认实现 return aggregate;
if (realm.supports(token)) {
AuthenticationInfo info = null;
Throwable t = null;
try {
// 单个realm认证
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
}
aggregate = strategy.afterAttempt(realm, token, info, aggregate, t); // 3默认实现 合并aggregate到info
}
}
aggregate = strategy.afterAllAttempts(token, aggregate); // 4默认实现 return aggregate;
return aggregate;
}
三、案例:
1、案例流程概述:
配置三个realm,其中realmOne,realmTwo,是认证usernamepasswordToken,tokenRealm是认证jwtToken,即如果我调用username、password的login,会执行realmOne
realmTwo的认证,如果我使用自定义的Token那么就是使用了tokenRealm。
2、pojo、dao、token等
public class User {
private String username;
private String password;
private String role;
public class JWTToken implements AuthenticationToken {
private static final long serialVersionUID = -329196857201301194L;
private String token;
public JWTToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
public class JwtUtil {
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
/**
* 过期时间5分钟
*/
private static final long EXPIRE_TIME = 5 * 60 * 1000;
/**
* 生成签名,5min后过期
* @param username 用户名
* @param secret 用户的密码
* @return 加密的token
*/
public static String sign(String username, String secret) {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}
/**
* 校验token是否正确
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
try {
DecodedJWT jwt = verifier.verify(token);
} catch (JWTVerificationException e) {
return false;
}
return true;
}
/**
* 获得token中的信息:无需secret解密也能获得
*/
public static String getUsername(String token) {
return getClaim(token,"username");
}
3 、三个realm
/**
* 自定义realmOne
*/
public class UserOneRealm extends AuthorizingRealm{
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
private UserDao userDao = new UserDao();
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(user.getRole());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("UserOneRealm调用");
String username = (String) token.getPrincipal();
User user = this.userDao.findUserByUsername(username);
if (user == null) {
throw new UnknownAccountException("账号不存在");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
return info;
}
}
/**
* 自定义realmTwo
* 验证用户名密码的基础上,并且验证了role是否为admin,才判定是否登陆成功
*/
public class UserTwoRealm extends AuthorizingRealm{
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
private UserDao userDao = new UserDao();
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(user.getRole());
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("UserTwoRealm调用");
String username = (String) token.getPrincipal();
User user = this.userDao.findUserByUsername(username);
if (user == null) {
throw new UnknownAccountException("账号不存在");
}
String role = user.getRole();
if (role == null || !role.equals("admin")) {
throw new AuthenticationException("此用户不是管理员,不能在此reaml登陆");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
return info;
}
}
/**
* token规则的realm
*/
public class TokenUserRealm extends AuthorizingRealm{
private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);
private UserDao userDao = new UserDao();
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole(user.getRole());
return info;
}
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof AuthenticationToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
log.info("TokenUserRealm调用");
String tokenStr = (String) token.getCredentials();
String username = JwtUtil.getUsername(tokenStr);
if (username == null) {
throw new AuthenticationException("token invalid");
}
User user = userDao.findUserByUsername(username);
if (user == null) {
throw new AuthenticationException("用户");
}
if (!JwtUtil.verify(tokenStr, username, user.getPassword())) {
throw new AuthenticationException("Token认证失败");
}
// 没有配置加密,token中的getCredentials()不进行加密,即是tokenStr
return new SimpleAuthenticationInfo(user, tokenStr, "my_realm");
}
}
4、ModularRealmAuthenticator的扩展MyModularRealmAuthenticator
/**
* 为了使代码清晰易懂,采用重写doAuthenticate的方式,自己设置根据不同的token实现调用不同的realm。
* 备注:doMultiRealmAuthentication本身能够根据realm.supports(token),来决定使用那个realm。
*/
public class MyModularRealmAuthenticator extends ModularRealmAuthenticator{
/**
* user相关的realm是下面的类
*/
private static final Class<?>[] USER_REAM_CLASSES = new Class[]{UserOneRealm.class, UserTwoRealm.class};
/**
* token相关的realm是下面的类
*/
private static final Class<?> TOKEN_REAML_CLASSES = TokenUserRealm.class;
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
assertRealmsConfigured();
Collection<Realm> realms = getRealms();
if (realms.size() == 1) {
return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
} else {
Collection<Realm> newRealms = new ArrayList<>();
if (authenticationToken instanceof JWTToken) {
JWTToken jwtToken = (JWTToken) authenticationToken;
for (Realm realm : realms) {
if (realm.getClass() == TOKEN_REAML_CLASSES) {
newRealms.add(realm);
}
}
if (newRealms.size() == 1) {
return doSingleRealmAuthentication(newRealms.iterator().next(), jwtToken);
}
} else {
for (Realm realm : realms) {
if (arrContains(USER_REAM_CLASSES, realm.getClass())) {
newRealms.add(realm);
}
}
if (newRealms.size() == 1) {
return doSingleRealmAuthentication(newRealms.iterator().next(), authenticationToken);
} else {
return doMultiRealmAuthentication(newRealms, authenticationToken);
}
}
return doMultiRealmAuthentication(realms, authenticationToken);
}
}
/**
* 数组是否包含
*/
private <T> boolean arrContains(T[] arr,T clazz) {
for (T t : arr) {
if (t == clazz) {
return true;
}
}
return false;
}
}
5、配置类和测试使用
配置类
@Configuration
public class ShiroConfig {
/**
* 第一个realm
*/
@Bean
public Realm realmOne() {
UserOneRealm userOneRealm = new UserOneRealm();
userOneRealm.setCredentialsMatcher(hashedCredentialsMatcher());
userOneRealm.setCachingEnabled(false);
return userOneRealm;
}
/**
* 第二个realm
*/
@Bean
public Realm realmTwo() {
UserTwoRealm userTwoRealm = new UserTwoRealm();
userTwoRealm.setCredentialsMatcher(hashedCredentialsMatcher());
userTwoRealm.setCachingEnabled(false);
return userTwoRealm;
}
/**
* token的realm
*/
@Bean
public Realm tokenRealm() {
TokenUserRealm userTwoRealm = new TokenUserRealm();
return userTwoRealm;
}
/**
* 自定义认证器
*/
@Bean
public Authenticator modularRealmAuthenticator() {
MyModularRealmAuthenticator modularRealmAuthenticator = new MyModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return modularRealmAuthenticator;
}
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
return hashedCredentialsMatcher;
}
@Bean(name = "sessionManager")
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setGlobalSessionTimeout(40 * 60 * 1000);
sessionManager.setDeleteInvalidSessions(true);
sessionManager.setSessionIdCookieEnabled(true);
return sessionManager;
}
@Bean
public EhCacheManager ehCacheManager(CacheManager cacheManager) {
EhCacheManager em = new EhCacheManager();
em.setCacheManager(cacheManager);
return em;
}
@Bean
public SecurityManager securityManager(EhCacheManager ehCacheManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setAuthenticator(modularRealmAuthenticator());
List<Realm> realms = new ArrayList<>();
realms.add(realmTwo());
realms.add(realmOne());
realms.add(tokenRealm());
securityManager.setRealms(realms);
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(ehCacheManager);
return securityManager;
}
}
测试类
/**
* 多realm测试
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class TestDemo {
@Autowired
private org.apache.shiro.mgt.SecurityManager securityManager;
/**
* 1、此时配置的多realm验证策略是AtLeastOneSuccessfulStrategy,即,只要有1个符合就通过,返回合并后所有认证信息。
*/
@Test
public void test1() {
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
// UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
UsernamePasswordToken token = new UsernamePasswordToken("guest", "123456");
subject.login(token);
}
/**
* 2、改变多realm验证策略FirstSuccessfulStrategy,也是只有一个符合即可,但是只返回第一个的认证信息。
* realms.add(realmTwo());
* realms.add(realmOne());
*/
@Test
public void test2() {
DefaultWebSecurityManager securityManager2 = (DefaultWebSecurityManager) securityManager;
MyModularRealmAuthenticator authenticator = (MyModularRealmAuthenticator) securityManager2.getAuthenticator();
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
SecurityUtils.setSecurityManager(securityManager);
UsernamePasswordToken token = new UsernamePasswordToken("guest", "123456"); // ok
Subject subject = SecurityUtils.getSubject();
subject.login(token);
}
/**
* 3、改变多realm验证策略FirstSuccessfulStrategy,必须全部符合realm。
*/
@Test
public void test3() {
DefaultWebSecurityManager securityManager2 = (DefaultWebSecurityManager) securityManager;
MyModularRealmAuthenticator authenticator = (MyModularRealmAuthenticator) securityManager2.getAuthenticator();
authenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
SecurityUtils.setSecurityManager(securityManager);
// UsernamePasswordToken token = new UsernamePasswordToken("guest",
// "123456"); // 报错
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
Subject subject = SecurityUtils.getSubject();
subject.login(token);
}
/**
* 4、验证jwtToken的token,会走TokenUserRealm的验证。
*/
@Test
public void test4() {
String username = "admin";
String secret = "e10adc3949ba59abbe56e057f20f883e"; // 123456的MD5加密
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTA5NjAwMDQsInVzZXJuYW1lIjoiYWRtaW4ifQ.-wpGMWqPweVg6TeUj_JsvA0YJ3MX9QgApNQB_vL0Fcs
String tokenstr = JwtUtil.sign(username, secret);
// System.out.println(token);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
JWTToken token = new JWTToken(tokenstr);
subject.login(token);
}
}
完毕!