Analysis of spring-boot case of shiro multi-realm

Analysis of spring-boot case of shiro multi-realm integration

    Overview: The shiro authentication process is mainly to call the login (Subject subject, AuthenticationToken token) method through the securityManager. In fact, it is the Authenticator (authenticator for authentication), which is used by default.

ModularRealmAuthenticator, if we only have 1 realm, the final authentication operation is to call this realm, what if we have multiple realm? At this time, we need to extend the ModularRealmAuthenticator.

1. The following is the code call snippet executed by subject.login()

    public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        Subject subject = securityManager.login(this, token);

The above is the login method of DelegatingSubject

    public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
        AuthenticationInfo info;
        try {
            info = authenticate(token);
        } catch (AuthenticationException ae) {
        
        }
    }

The above is the login method of DefaultSecurityManager

    public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }

What actually calls is the authenticate method of AuthenticatingSecurityManager, this.authenticator is ModularRealmAuthenticator.

2. What strategy does ModularRealmAuthenticator's default multiple reaml handle?

1. The public processing of the authenticate method actually calls the doAuthenticate(token) method.

 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. Let's take a look at the default implementation of doAuthenticate, and determine whether to call a single or doMultiRealmAuthentication() method according to the number of configured realms

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. Before talking about the doMultiRealmAuthentication method, we can find the new AuthenticationStrategy strategy in the ModularRealmAuthenticator class. This strategy determines the implementation of doMultiRealmAuthentication. Let's take a look at the default implementation of 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;
    }
}

It has three implementation subclasses.

FirstSuccessfulStrategy : As long as one authentication succeeds, the authentication information returns the first authentication information

Principle: Step 1 (see the code below) all authentications return null, Step 3, the first time info is not null, return info and assign it to aggregate, and always return aggregate after that.

   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 : Based on firstSuccessfulStrategy, aggregate will be initially created, and step 3 is to merge info and aggregate by default.

AllSuccessfulStrategy : All must be successfully authenticated. The principle is: afterAttempt will verify the results of each 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;
	}

Three, case:

1. Overview of the case process:

  Configure three realms, among which realmOne and realmTwo are authentication usernamepasswordToken, tokenRealm is authentication jwtToken, that is, if I call login with username and password, realmOne will be executed

For the authentication of realmTwo, if I use a custom Token, then tokenRealm is used.

2, pojo, dao, token, etc.

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. Three 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. MyModularRealmAuthenticator, an extension of ModularRealmAuthenticator

/**
 * 为了使代码清晰易懂,采用重写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 class and test use

Configuration class

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

Test class

/**
 * 多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);
	}
	
}

 

complete!

Guess you like

Origin blog.csdn.net/shuixiou1/article/details/112795418