shiro多realm的spring-boot案例剖析

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);
	}
	
}

完毕!

猜你喜欢

转载自blog.csdn.net/shuixiou1/article/details/112795418