zl程序教程

您现在的位置是:首页 >  后端

当前栏目

SpringBoot 与Shiro 整合系列(三)多Realm验证和认证策略

SpringBoot认证 系列 策略 验证 整合 shiro realm
2023-09-11 14:20:19 时间

系列(三)讲述 SpringBoot整合Shiro 实现多Realm验证以及认证策略


shiro是一个很好的登陆以及权限管理框架,但是默认是单realm单数据表,但是很多业务需求单realm是很难实现登陆以及权限管理的功能(比如业务中用户分布在不同的数据表),这个时候就需要使用多个Realm。

一、使用场景

存在这样一种场景,我们可能会把安全数据放到不同的数据库,比方说 mysql里有,oracle里也有,mysql里面的加密算法是MD5,oracle里面的加密算法是SHA1。这个时候我们进行用户认证时,就需要同时访问这两个数据库,就需要多个Realm。如果有多个Realm的话,还需要涉及到认证策略的问题。

二、多Realm验证

多重认证,主要的类是ModularRealmAuthenticator他有两个需要配置的属性,一个是Collection(用于存储Realm),另一个是AuthenticationStrategy(用于存储验证的策略 )

通过查看源码可以看到 ModularRealmAuthenticator.class 中的 doAuthenticate方法:

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {//一个realm
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {//多个realm
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

从上面doAuthenticate方法中可以看到:

如果有一个Realm 使用的是:

  • doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);

如果有多个Realm 使用的是:

  • doMultiRealmAuthentication(realms, authenticationToken);

所以我们可以配置多个Realm 给到 ModularRealmAuthenticator 这个bean,将ModularRealmAuthenticator 单独配置为一个bean,将这个bean 配置给SecurityManager。

配置流程:

1.在原有代码的基础上,添加第二个Realm SecondRealm.java

SecondRealm.java 中的 加密算法 为 SHA1

package com.koncord.shiro;


import org.apache.shiro.SecurityUtils;
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.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import com.koncord.model.User;
import com.koncord.service.UserService;

/**
 * 自定义 Realm
 * @author Administrator
 *
 */
public class SecondRealm extends AuthorizingRealm{

	@Autowired
	private UserService userService;
	
	/**
	 * 执行授权逻辑
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("SecondRealm 执行授权逻辑");
		//给资源进行授权
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		//添加资源的授权字符串
		//info.addStringPermission("user:add");
		
		//导数据库查询当前登录用户的授权字符串
		//获取当前登录用户
		Subject subject=SecurityUtils.getSubject();
		User user = (User) subject.getPrincipal(); 
		
		User sbUser=userService.findUserByName(user.getName());
		
		info.addStringPermission(sbUser.getPerms());
		return info;
	}

	/**
	 * 执行认证逻辑
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("SecondRealm 执行认证逻辑");
		
		//编写shiro 判断逻辑,判断用户名和密码
		//1.判断用户名
		UsernamePasswordToken userToken=(UsernamePasswordToken) token;
		//根据用户名获取用户信息
		User user=userService.findUserByName(userToken.getUsername());
				
		if(user == null){
			//用户不存在
			return null;//shiro底层会抛出 UNknowAccountException
		}
		//2.判断密码     第一个参数:需要返回给 subject.login方法的数据   第二个参数:数据库密码       第三个参数:realm的名字
//		return new SimpleAuthenticationInfo(user, user.getPassword(), "");
		
		//根据用户的情况,来构建AuthenticationInfo对像并返回,通常使用的实现类是SimpleAuthenticationInfo
		//以下信息是从数据库获取的
		//1).principal:认证的实体类信息。可以是username,也可以是数据表对应的用户的体类对象
		Object principal=user;
		//2).credentials:密码(数据库获取的用户的密码)
		Object credentials=user.getPassword();
		//3).realmName:当前realm对象的name,调用父类的getName()方法即可
		String realmName=getName();
		//4).盐值
		ByteSource credentialsSalt = ByteSource.Util.bytes(user.getName());//盐值 要唯一
		return new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
	}

	public static void main(String[] args) {
		String hashAlgorithmName="SHA1";//加密算法(与配置文件中的一致)
		String credentials="111111";//密码
		ByteSource salt=ByteSource.Util.bytes("zhangsan");//盐值
//		ByteSource salt=null;//盐值
		int hashIterations=1024;//加密次数(与配置文件中的一致)
		System.out.println(new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations));
	}
}

第一个自定义Realm的代码:

package com.koncord.shiro;


import org.apache.shiro.SecurityUtils;
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.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import com.koncord.model.User;
import com.koncord.service.UserService;

/**
 * 自定义 Realm
 * @author Administrator
 *
 */
public class UserRealm extends AuthorizingRealm{

	@Autowired
	private UserService userService;
	
	/**
	 * 执行授权逻辑
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		System.out.println("UserRealm 执行授权逻辑");
		//给资源进行授权
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
		//添加资源的授权字符串
		//info.addStringPermission("user:add");
		
		//导数据库查询当前登录用户的授权字符串
		//获取当前登录用户
		Subject subject=SecurityUtils.getSubject();
		User user = (User) subject.getPrincipal(); 
		
		User sbUser=userService.findUserByName(user.getName());
		
		info.addStringPermission(sbUser.getPerms());
		return info;
	}

	/**
	 * 执行认证逻辑
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		System.out.println("UserRealm 执行认证逻辑");
		
		//编写shiro 判断逻辑,判断用户名和密码
		//1.判断用户名
		UsernamePasswordToken userToken=(UsernamePasswordToken) token;
		//根据用户名获取用户信息
		User user=userService.findUserByName(userToken.getUsername());
				
		if(user == null){
			//用户不存在
			return null;//shiro底层会抛出 UNknowAccountException
		}
		//2.判断密码     第一个参数:需要返回给 subject.login方法的数据   第二个参数:数据库密码       第三个参数:realm的名字
//		return new SimpleAuthenticationInfo(user, user.getPassword(), "");
		
		//根据用户的情况,来构建AuthenticationInfo对像并返回,通常使用的实现类是SimpleAuthenticationInfo
		//以下信息是从数据库获取的
		//1).principal:认证的实体类信息。可以是username,也可以是数据表对应的用户的体类对象
		Object principal=user;
		//2).credentials:密码(数据库获取的用户的密码)
		Object credentials=user.getPassword();
		//3).realmName:当前realm对象的name,调用父类的getName()方法即可
		String realmName=getName();
		//4).盐值
		ByteSource credentialsSalt = ByteSource.Util.bytes(user.getName());//盐值 要唯一
		return new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
	}

	public static void main(String[] args) {
		String hashAlgorithmName="MD5";//加密算法(与配置文件中的一致)
		String credentials="111111";//密码
		ByteSource salt=ByteSource.Util.bytes("admin");//盐值
//		ByteSource salt=null;//盐值
		int hashIterations=1024;//加密次数(与配置文件中的一致)
		System.out.println(new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations));
	}
}

2.修改ShiroConfig 配置类

2.1 创建第2个Realm:

	/**
	 * 创建第二个Realm
	 */
	@Bean
	public SecondRealm secondRealm(){
		SecondRealm secondRealm = new SecondRealm();
		//设置 凭证匹配器
		secondRealm.setCredentialsMatcher(hashedCredentialsMatcherSHA());
		return secondRealm;
	}
	/**
	 * 配置 凭证匹配器 ,加密算法为 SHA1
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcherSHA(){
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		//设置加密算法
		hashedCredentialsMatcher.setHashAlgorithmName("SHA1");
		//设置加密次数,比如两次,相当于SHA1(SHA1())
		hashedCredentialsMatcher.setHashIterations(1024);
		return hashedCredentialsMatcher;
	}

2.2 修改securityManager方法,添加多realms:

	/**
	 * 2.创建 DefaultWebSecurityManager
	 */
	@Bean(name="securityManager")
	public DefaultWebSecurityManager securityManager(@Qualifier("userRealm")UserRealm userRealm){
		DefaultWebSecurityManager securityManager =new DefaultWebSecurityManager();
//		//关联realm
//		securityManager.setRealm(userRealm);
		//添加多realms
		List<Realm> realms =new ArrayList<Realm>();
		realms.add(userRealm);
		realms.add(secondRealm());
		securityManager.setRealms(realms);
		return securityManager;
	}

ShiroConfig 配置类完整代码:

package com.koncord.shiro;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;

/**
 * Shiro配置类
 * @author Administrator
 *
 */
@Configuration
public class ShiroConfig {

	/**
	 * 3.创建ShiroFilterFactoryBean
	 */
	@Bean
	public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
		ShiroFilterFactoryBean shiroFilterFactoryBean =new ShiroFilterFactoryBean();
		
		//设置安全管理器
		shiroFilterFactoryBean.setSecurityManager(securityManager);
		
		//添加Shiro内置过滤器
		/**
		 * Shiro内置过滤器,可以实现权限相关的拦截
		 *    常用的过滤器:
		 *       anon:无需认证(登录)可以访问
		 *       authc:必须认证才可以访问
		 *       user:如果使用rememberMe的功能可以直接访问
		 *       perms:该资源必须得到资源权限才可以访问
		 *       role:该资源必须得到角色权限才可以访问
		 */
		Map<String, String> filterMap = new LinkedHashMap<String, String>();
//		filterMap.put("/add", "authc");
//		filterMap.put("/update", "authc");
		filterMap.put("/testThymeleaf", "anon");
		filterMap.put("/login", "anon");
		
		//授权过滤器
		//注意:当前授权拦截后,shiro会自动跳转到未授权页面
		filterMap.put("/add", "perms[user:add]");
		filterMap.put("/update", "perms[user:update]");
		
		filterMap.put("/*", "authc");
		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
		
		//设置登录跳转链接
		shiroFilterFactoryBean.setLoginUrl("/toLogin");
		//设置未授权提示页面
		shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
		return shiroFilterFactoryBean;
	}
	
	/**
	 * 2.创建 DefaultWebSecurityManager
	 */
	@Bean(name="securityManager")
	public DefaultWebSecurityManager securityManager(@Qualifier("userRealm")UserRealm userRealm){
		DefaultWebSecurityManager securityManager =new DefaultWebSecurityManager();
//		//关联realm
//		securityManager.setRealm(userRealm);
		//添加多realms
		List<Realm> realms =new ArrayList<Realm>();
		realms.add(userRealm);
		realms.add(secondRealm());
		securityManager.setRealms(realms);
		return securityManager;
	}
	
	/**
	 * 1.创建Realm
	 */
	@Bean
	public UserRealm userRealm(){
		UserRealm userRealm = new UserRealm();
		//设置 凭证匹配器
		userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
		return userRealm;
	}
	
	/**
	 * 创建第二个Realm
	 */
	@Bean
	public SecondRealm secondRealm(){
		SecondRealm secondRealm = new SecondRealm();
		//设置 凭证匹配器
		secondRealm.setCredentialsMatcher(hashedCredentialsMatcherSHA());
		return secondRealm;
	}
	
	/**
	 * 配置ShiroDialect,用于thymeleaf和shiro标签配合使用
	 */
	@Bean
	public ShiroDialect getShiroDialect(){
		return new ShiroDialect();
	}
	
	/**
	 * 配置 凭证匹配器 (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了,
	 * 所以我们需要修改下doGetAuthenticationInfo中的代码;)
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcher(){
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		//设置加密算法
		hashedCredentialsMatcher.setHashAlgorithmName("MD5");
		//设置加密次数,比如两次,相当于md5(md5())
		hashedCredentialsMatcher.setHashIterations(1024);
		return hashedCredentialsMatcher;
	}
	
	/**
	 * 配置 凭证匹配器 ,加密算法为 SHA1
	 */
	@Bean
	public HashedCredentialsMatcher hashedCredentialsMatcherSHA(){
		HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
		//设置加密算法
		hashedCredentialsMatcher.setHashAlgorithmName("SHA1");
		//设置加密次数,比如两次,相当于SHA1(SHA1())
		hashedCredentialsMatcher.setHashIterations(1024);
		return hashedCredentialsMatcher;
	}
}

这样流程就配置完成了!

3.测试

打断点,进行debug调试,看下运行效果
(1)在UsernamePasswordToken.class 的
在这里插入图片描述

处打断点,会看到断点停两次。
(2)在ModularRealmAuthenticator.class的
在这里插入图片描述
处,会看到 Realm的个数。
(3)由于两种都使用的HashedCredentialsMatcher 时的两种算法:
在这里插入图片描述
在这里插入图片描述
测试成功!!!

三、认证策略

如果有多个Realm,怎样才能认证成功,这就我们所谓的认证策略。

1.概念

(1)当一个应用程序配置了两个或两个以上的Realm 时,ModularRealmAuthenticator 依靠内部的AuthenticationStrategy 组件来判定认证的成功或失败。

AuthenticationStrategy是一个无状态的组件,它在身份验证尝试中被询问4 次(这4 次交互所需的任何必要的状态将被作为方法参数):

  1. 在任何Realm 被调用之前被询问;
  2. 在一个单独的Realm 的getAuthenticationInfo 方法被调用之前立即被询问;
  3. 在一个单独的Realm 的getAuthenticationInfo 方法被调用之后立即被询问;
  4. 在所有的Realm 被调用后询问。

(2)认证策略的另外一项工作就是聚合所有Realm的结果信息封装至一个AuthenticationInfo实例中,并将此信息返回,以此作为Subject的身份信息。

2.认证策略的使用

认证策略主要使用的是 AuthenticationStrategy 接口。
在这里插入图片描述
这个接口有三个实现类(认证策略的实现):

策略意义
AllSuccessfulStrategy所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了
AtLeastOneSuccessfulStrategy(默认)只要有一个Realm验证成功即可,和FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息
FirstSuccessfulStrategy只要有一个 Realm 验证成功即可只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;

ModularRealmAuthenticator内置的认证策略默认实现是AtLeastOneSuccessfulStrategy 方式,因为这种方式也是被广泛使用的一种认证策略。
在这里插入图片描述

2.1 默认使用

验证下AtLeastOneSuccessfulStrategy:通过源码的方法验证
①为了看效果,修改下 SecondRealm的认证消息的返回值

SimpleAuthenticationInfo info= new SimpleAuthenticationInfo("SecondRealmName", credentials, credentialsSalt, realmName);

②debug方式启动项目,输入 用户名和密码,点击submit,进入如下断点,可以看到 认证策略:
在这里插入图片描述
默认AtLeastOneSuccessfulStrategy策略下,有一个Realm认证成功就可以了。就算另一个Realm认证失败也没关系。

2.1 切换认证策略

(1)如何切换认证策略?
比如:切换成 AllSuccessfulStrategy 即所有认证策略都通过了,才算认证成功。

答:可以看出 认证策略是ModularRealmAuthenticator 类的一个属性 authenticationStrategy,即在ShiroConfig配置类中添加配置:

	/**
	 * 系统自带的Realm管理,主要针对多realm
	 */
	@Bean
	public ModularRealmAuthenticator modularRealmAuthenticator(){
		ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
		//设置认证策略
		modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
		return modularRealmAuthenticator;
	}

并在securityManager方法中设置这个modularRealmAuthenticator()方法:
在这里插入图片描述

即可。

(2)验证AllSuccessfulStrategy:
打断点,debug重启项目,进行验证:发现第二个Realm报异常
在这里插入图片描述

登录失败
在这里插入图片描述
上一篇:SpringBoot 与Shiro 整合系列(二)实现MD5盐值加密

延伸:
springboot整合shiro实现多realm不同数据表登陆
Spring Boot 集成Shiro的多realm配置