Shiro简介及SpringBoot整合Shiro

此文章作为自己学习总结用。请各位看官多多指正留言或发邮件给我。

邮箱地址:[email protected]

Shiro简介

(仅仅是简介,只实现了用户登录认证、授权认证和用户权限缓存功能,可以满足小型项目的登录功能。如果想深入了解shiro,可以搜索《跟我学shiro》。)

Shiro架构:

1、Subject(org.apache.shiro.subject.Subject): 简称用户,但这个用户不一定是一个具体的人,和当前应用交互的任何东西都可以称为Subject;与Subject的所有交互都会委托给SecurityManager,SecurityManager是实际的执行者。

2、SecurityManager(org.apache.shiro.mgt.SecurityManager): SecurityManager是shiro的核心,协调shiro各个组件。

3、Authenticator(org.apache.shiro.authc.Authenticator): 认证器,登录控制。

4、Authorizer(org.apahce.shiro.authz.Authorizer): 授权器,决定subject能拥有什么样的角色或者权限。

5、SessionManager(org.apache.shiro.session.SessionManager): 创建和管理用户Session。

6、CacheManager(org.apache.shiro.cache.CacheManager): 缓存管理器,主要存储Session和权限数据。

7、Cryptography(org.apache.shiro.crypto): 安全加密工具,Shiro的api大幅度简化java api中繁琐的密码加密。

8、Realm(org.apache.shiro.realm.Realm): 相当于数据源,程序与安全数据的桥梁;负责用户认证和用户授权(可以配置多个realm)。


权限管理原理:

用户、角色、权限和资源的关系模型:

目的:基于资源的访问控制RBAC(ResourceBased Access Control)

通常企业开发中将资源和权限合并为一张权限表。

示例:


Type字段用于区分menu(菜单)和permission(权限)。

Percode权限代码。字符串通配符权限规则:“资源标识符:操作:对象实例ID”。

Parent_id_tree菜单层级关系。


简述Shiro身份验证、授权和拦截机制

身份验证流程:

1、首先会调用Subject.login(token)进行登录,会自动委托给SecurityManager(登录前必须通过SecurityUtils.setSecurityManager(SecurityManager)将SecurityManager设置到当前环境当中)。

2、SecurityManager负责验证逻辑,真正的验证工作会委托给Authenticator

3、Authenticator会把传入的token传入Realm,从Realm获取身份验证信息,验证失败返回AuthenticationException异常。若配置了多个Realm,将按照相应的顺序及策略进行验证。


授权流程:

Shiro的支持三种授权方式:

1、编程式,Subject.hasRole(roleCode),Subject.hasPermission(perCode)等。

2、注解式,@RequiresRoles(roleCode),@RequiresPermissions(perCode)(需要开启SpringMVC的AOP代理)。

3、JSP标签,

<shiro:hasRolename="roleCode">

<!— 有权限显示的标签 —>

</shiro:hasRole>

 

授权流程:

1、SecurityManager把真正的授权工作委托给Authorizer

2、接着Authorizer会通过PermissionResolver将权限字符串转换成相应的Permission实例。

3、授权之前会调用realm的doGetAuthorizationInfo方法获取当前Subject相应的角色/权限。

4、Authorizer会判断Realm的角色/权限是否和传入的匹配;如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断。


拦截机制:

Shiro拦截器类关系图:


1、org.apache.shiro.web.servlet.NameableFilter:给Filter起名字。

2、org.apache.shiro.web.servlet.OncePerRequestFilter:用于防止多次执行Filter;doFilter方法具体实现一次请求只会走一次拦截器链;另外会提供enable属性,表示否是开启拦截器实例,默认true。

3、org.apache.shiro.web.servlet.ShiroFilter:整个Shiro的入口,用于拦截需要验证的请求,并进行处理。

4、org.apache.shiro.web.servlet.AdviceFilter:提供了AOP风格的支持,通过preHandle和postHandle方法分别在执行拦截器链执行前后进行处理;afterCompletion属于postHandle的增强方法,无论是否有异常都会执行,一般进行资源清理的工作。

5、org.apache.shiro.web.filter.PathMatchingFilter:提供请求路径的匹配功能(pathsMatch方法),以及拦截器参数解析的功能(onPreHandle方法)。

6、org.apache.shiro.web.filter.AccessControlFilter:提供了访问控制的基础功能;比如是否允许访问(isAccessAllowed方法),访问拒绝时如何处理(onAccessDenied方法);AccessControlFilter还提供了基于表单的身份验证功能(isLoginRequest、saveRequestAndRedirectToLogin、saveRequest和redirectToLogin方法)。


内置拦截器:

authc:org.apache.shiro.web.filter.authc.FormAuthenticationFilter。基于表单的拦截器;需要登录才能访问。主要属性:用户名usernameParam,密码passwordParam,记住登录remremberMeParam,登录失败后错误信息failureKeyAttribute。

authcBasic:org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter。Basic HTTP身份验证拦截器。

logout:org.apache.shiro.web.filter.authc.LogoutFilter。退出拦截器。主要属性:redirectUrl:退出成功后重定向的地址。

user:org.apache.shiro.web.filter.authc.UserFilter。用户拦截器,记住我登录后可以访问的地址。

anon:org.apache.shiro.web.filter.authc.AnonymousFilter。匿名拦截器,即不需要登录即可访问的资源。一般用于过滤静态资源。

roles:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter。角色授权拦截器,验证用户是或否拥有角色。

perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter。权限授权拦截器,验证用户是否拥有权限。

port:org.apache.shiro.web.filter.authz.PortFilter。端口拦截器。主要属性:port:可通过的端口。

rest:org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter。rest风格拦截器,自动根据请求方法构建权限字符串(例:“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配,调用isPermittedAll)。

ssl:org.apache.shiro.web.filter.authz.SslFilter。ssl拦截器,只有请求协议是https才能通过。

onSessionCreation:org.apache.shiro.web.filter.session.NoSessionCreationFilter。不创建会话拦截器。


权限注解:

@RequiredAuthentication:表示当前Subject已经通过了login进行了身份验证;即Subject.isAuthenticated()返回true。

@RequiresUser:表示当前Subject已经身份验证或者通过记住我登录的。

@RequiresGuest:表示当前Subject没有身份验证或通过记住我登陆过,可视为游客身份。

@RequiresRoles(value={“roleCode1”,“roleCode2”} , logical= Logical.AND):表示当前Subject需要角色roleCode1和roleCode2。

@RequiresPermissions(value={“user:perCode1”,“user:perCode2”} , logical= Logical.OR):表示当前Subject需要权限user:perCode1或user:perCode2。


SpringBoot整合Shiro Demo

依赖:

<dependency>  
    <groupId>org.apache.shiro</groupId>  
    <artifactId>shiro-core</artifactId>  
    <version>1.2.2</version>  
</dependency> 
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-spring</artifactId>
   <version>1.2.2</version>
</dependency>
<dependency>
   <groupId>org.apache.shiro</groupId>
   <artifactId>shiro-ehcache</artifactId>
   <version>1.2.2</version>
</dependency>

建立相关表:

CREATE TABLE `sys_user` (
  `user_id` varchar(32) NOT NULL,
  `user_name` varchar(255) NOT NULL,
  `user_code` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `salt` varchar(255) NOT NULL,
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

CREATE TABLE `sys_role` (
  `role_id` varchar(32) NOT NULL,
  `role_name` varchar(255) NOT NULL,
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

CREATE TABLE `sys_user_role` (
  `user_role_id` varchar(32) NOT NULL,
  `user_id` varchar(32) NOT NULL,
  `role_id` varchar(32) NOT NULL,
  PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关系表';

CREATE TABLE `sys_permission` (
  `permission_id` varchar(32) NOT NULL,
  `permission_name` varchar(255) NOT NULL,
  `type` varchar(255) NOT NULL COMMENT '授权类型:menu菜单;permission权限',
  `url` varchar(255) NOT NULL,
  `percode` varchar(255) DEFAULT NULL COMMENT '权限代码(格式:“对象:操作”)',
  `parent_id` varchar(32) NOT NULL,
  `parent_id_tree` varchar(255) NOT NULL COMMENT '菜单结构树',
  `sort` varchar(255) DEFAULT NULL COMMENT '排序',
  PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';

CREATE TABLE `sys_role_permission` (
  `role_permission_id` varchar(32) NOT NULL,
  `role_id` varchar(32) DEFAULT NULL,
  `permission_id` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`role_permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限表';

整合流程概述:

1、 配置、注册安全管理器SecurityManager:

    ①   自定义realm,注入到SecurityManager。

    ②   配置凭证匹配器CredentialsMatcher,注入到SecurityManager。

    ③   自定义缓存管理器CacheManager,注入到SecurityManager。

2、 配置、注册表单过滤器FormAuthenticationFilter。

3、 配置、注册ShiroFilter。

    ①   注入安全管理器SecurityManager。

    ②   注入表单过滤器FormAuthenticationFilter。

    ③   配置、注入拦截器过滤链。

4、开启shiro注解支持。

5、开启SpringMVC AOP代理。


自定义Realm

用于登录验证、用户授权和清空缓存。

AuthenticationInfodoGetAuthenticationInfo(AuthenticationToken token):Shiro拦截到url时会进行过滤,PathMatchingFilter拦截器会判断是否已经登录(检查是否有session或token),没有登录时会调用Subject.login(new UsernamePasswordToken(userCode,password))方法,转交给realm的doGetAuthenticationInfo方法匹配。

AuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principals):需要验证权限时(Subject.isPermitted(percode)、Subject. isPermittedAl(percode1,percode2…)、Subject.checkPermission(percode)或进入带有@RequiresPermissions(percode)注解的方法),会调用realm的doGetAuthorizationInfo方法进行授权。

void clearCache():用于清空权限缓存。

public class MyRealm extends AuthorizingRealm {

	@Autowired
	private SysUserService sysUserService;
	
	/**
	 * 用户验证
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// 获得用户账号
		String userCode = (String)token.getPrincipal();
		SysUser sysUser = sysUserService.findByUserCode(userCode);
		// 校验用户
		String password = sysUser.getPassword();
		// 盐
		String salt = sysUser.getSalt();
		// 此处会根据shiroFilter注入的凭证匹配器的配置,对表单提交的密码加密后进行比对
		SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(sysUser, password, ByteSource.Util.bytes(salt), this.getName());
		return simpleAuthenticationInfo;
	}
	
	/**
	 * 用户授权
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		SysUser sysUser = (SysUser)principals.getPrimaryPrincipal();
		List<SysPermission> permissionList = null;
		try {
			permissionList = sysUserService.selectPermissionByUserId(sysUser.getUserId());
		} catch (Exception e) {
		}
		List<String> permissions = new ArrayList<String>();
		if (permissionList != null) {
			for (SysPermission sysPermission : permissionList) {
				permissions.add(sysPermission.getPercode());
			}
		}
		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
		simpleAuthorizationInfo.addStringPermissions(permissions);
		return simpleAuthorizationInfo;
	}
	
	public void clearCache() {
		PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
		super.clearCache(principals);
	}
}

注册MyRealm:

/**
 * 自定义realm 认证授权管理器
 * @return
 */
@Bean(name = "myRealm")
public MyRealm myRealm() {
	return new MyRealm();
}


设置凭证匹配器HashedCredentialsMatcher

hashAlgorithmName:使用的加密方式(md5或Base64等)。

hashIterations:加密次数。

/**
 * HashedCredentialsMatcher 凭证匹配器
 * @return
 */
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher credentialsMatcher() {
	HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
	// 设置加密方式及加密次数
	hashedCredentialsMatcher.setHashAlgorithmName(hashAlgorithmName);
	hashedCredentialsMatcher.setHashIterations(hashIterations);
	return hashedCredentialsMatcher;
}


设置缓存管理器CacheManager

此处以redis作为容器。

概述:

1、注入redis数据源,使用Spirng的RedisTemplate或注入JedisPool。

2、实现org.apache.shiro.cache.Cache接口。

3、实现org.apache.shiro.cache.CacheManager接口。

4、将自定义CacheManager的子类注入到Spring。

5、将自定义CacheManager注入到SecurityManager。


注入redis数据源:

redis依赖:
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.0</version>
</dependency>
注册JedisPool:
@Configuration
@PropertySource(value = "classpath:redis.properties")
public class RedisConfig {
	@Value("${reids.url}")
	private String redisUrl;
	
	@Value("${redis.password}")
	private String redisPassword;

	@Value("${redis.port}")
	private Integer redisPort;
	
	@Value("${redis.timeout}")
	private Integer redisTimeout;
	
	@Value("${redis.pool.maxTotal}")
	private Integer maxTotal;
	
	@Value("${redis.pool.minIdle}")
	private Integer minIdle;
	
	@Value("${redis.pool.maxIdle}")
	private Integer maxIdle;
	
	@Value("${redis.pool.maxWaitMillis}")
	private Integer maxWaitMillis;

	@Bean(name = "jedisPoolConfig")
	public JedisPoolConfig jedisPoolConfig() {
		JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
		// 最大连接数
		jedisPoolConfig.setMaxTotal(maxTotal);
		// 最大空闲连接数
		jedisPoolConfig.setMaxIdle(maxIdle);
		// 最小空闲连接数, (不设置为0)
		jedisPoolConfig.setMinIdle(minIdle);
		// 获取连接时的最大等待毫秒数
		jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
		// 在获取连接的时候检查有效性
		jedisPoolConfig.setTestOnBorrow(true);
		// 在获取连接的时候检查有效性
		jedisPoolConfig.setTestOnReturn(true);
		return jedisPoolConfig;
	}
	
	@Bean(name = "jedisPool")
	public JedisPool jedisPool() {
		JedisPool jedisPool = new JedisPool(jedisPoolConfig(), redisUrl, redisPort, redisTimeout, redisPassword);
		return jedisPool;
	}
}


实现org.apache.shiro.cache.Cache接口:

回调接口:
public interface RedisCallback {
	Object doWithRedis(Jedis jedis);
}
序列化工具类:
public enum JDKSerializer implements Serializer {
	INSTANCE;
	private static Logger log = Logger.getLogger(JDKSerializer.class);

	private JDKSerializer() {}
	
	public byte[] serialize(Object object) {
		ObjectOutputStream oos = null;
		ByteArrayOutputStream baos = null;
		try {
			// Object序列化
			baos = new ByteArrayOutputStream();
			oos = new ObjectOutputStream(baos);
			oos.writeObject(object);
			return baos.toByteArray();
		} catch (Exception e) {
			log.error(new String("JDKSerializer : serialize "), e);
			throw new CacheException(e);
		} finally {
			JDKSerializer.close(oos);
			JDKSerializer.close(baos);
		}
	}
	
	/**
	 * Object反序列化
	 * @param bytes
	 * @return
	 */
	public Object unserialize(byte[] bytes) {
		if (bytes == null) {
			return null;
		}
		ByteArrayInputStream bais = null;
		ObjectInputStream ois = null;
		try {
			bais = new ByteArrayInputStream(bytes);
			ois = new ObjectInputStream(bais);
			return ois.readObject();
		} catch (Exception e) {
			log.error(new String("JDKSerializer : unserialize "), e);
			throw new CacheException(e);
		} finally {
			JDKSerializer.close(bais);
			JDKSerializer.close(ois);
		}
	}

	/**
	 * 关闭IO流对象
	 * @param closeable
	 */
	public static void close(Closeable closeable) {
		if (closeable != null) {
			try {
				closeable.close();
			} catch (Exception e) {
				log.error("JDKSerializer : close ",e);
				throw new CacheException(e);
			}
		}
	}
}
实现Cache接口:
public final class ShiroRedisCache<K, V> implements Cache<K, V> {

	private JedisPool jedisPool;
	
	private static final Integer DATABASE = 3;
	
	private Serializer serializer = JDKSerializer.INSTANCE;
	
	private static final Integer TIMEOUT = 1800;
	
	public Object execute(RedisCallback callback) {
		if (jedisPool == null) {
			synchronized (ShiroRedisCache.class) {
				if (jedisPool == null) {
					this.setJedisPool(SpringUtil.getBean("jedisPool", JedisPool.class));
				}
			}
		}
		Jedis jedis = this.getJedisPool().getResource();
		jedis.select(DATABASE);
		try {
			return callback.doWithRedis(jedis);
		} finally {
			jedis.close();
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public Object get(final Object key) throws CacheException {
		return this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Object value = serializer.unserialize(jedis.get(key.toString().getBytes()));
				return value;
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object put(final Object key, final Object value) throws CacheException {
		return this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				jedis.set(key.toString().getBytes(), serializer.serialize(value));
				if (TIMEOUT != null && jedis.ttl(key.toString().getBytes()) == -1) {
					jedis.expire(key.toString().getBytes(), TIMEOUT);
				}
				return serializer.unserialize(jedis.get(key.toString().getBytes()));
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Object remove(final Object key) throws CacheException {
		return this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Object value = serializer.unserialize(jedis.get(key.toString().getBytes()));
				jedis.del(key.toString().getBytes());
				return value;
			}
		});
	}

	@Override
	public void clear() throws CacheException {
		this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				jedis.flushDB();
				return null;
			}
		});
	}

	@Override
	public int size() {
		return (Integer)this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Long size = jedis.dbSize();
				return size.intValue();
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Set keys() {
		return (Set<Object>)this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				Set<byte[]> keys = jedis.keys("*".getBytes());
				Set<Object> set = new HashSet<Object>();
				for (byte[] bs : keys) {
					set.add(serializer.unserialize(bs));
				}
				return set;
			}
		});
	}

	@SuppressWarnings("unchecked")
	@Override
	public Collection values() {
		final Set<Object> keys = this.keys();
		return (List<Object>)this.execute(new RedisCallback() {
			@Override
			public Object doWithRedis(Jedis jedis) {
				List<Object> values = new ArrayList<Object>();
				for (Object key : keys) {
					values.add(serializer.unserialize(jedis.get(key.toString().getBytes())));
				}
				return values;
			}
		});
	}
}


实现org.apache.shiro.cache.CacheManager接口

public class RedisCacheManager implements CacheManager {
	@Override
	public <K, V> Cache<K, V> getCache(String name) throws CacheException {
		return new ShiroRedisCache<K, V>();
	}
}

将自定义CacheManager的子类注入到Spring

/**
 * cacheManager 缓存管理器
 * @return
 */
@Bean(name = "cacheManager")
public CacheManager cacheManager() {
	return new RedisCacheManager();
}

配置、注册安全管理器SecurityManager

/**
 * scurityManager 安全管理器
 * @param realm
 * @param credentialsMatcher
 * @param cacheManager
 * @return
 */
@Bean(name = "securityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("myRealm")AuthorizingRealm realm, @Qualifier("credentialsMatcher")HashedCredentialsMatcher credentialsMatcher,@Qualifier("cacheManager")CacheManager cacheManager) {
	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
	// 注入凭证匹配器
	realm.setCredentialsMatcher(credentialsMatcher);
	// 注入realm
	securityManager.setRealm(realm);
	// 注入cache管理器
	securityManager.setCacheManager(cacheManager);
	return securityManager;
}


配置、注册ShiroFilter

/**
 * ShiroFilterFactoryBean shiro配置工厂
 * @param securityManager
 * @param formAuthenticationFilter
 * @return
 */
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager, 
		@Qualifier("formAuthenticationFilter")FormAuthenticationFilter formAuthenticationFilter) {
	ShiroFilterFactoryBean shiroFilterFactory = new ShiroFilterFactoryBean();
	// 注入securityManager
	shiroFilterFactory.setSecurityManager(securityManager);
	
	// 认证提交地址
	shiroFilterFactory.setLoginUrl(loginUrl);
	// 无权操作跳转页面
	shiroFilterFactory.setUnauthorizedUrl(unauthorizedUrl);
	// 验证成功后跳转页面
	shiroFilterFactory.setSuccessUrl(successUrl);
	
	// 设置filter
	Map<String, Filter> filterMap = new LinkedHashMap<String, Filter>();
	filterMap.put("authc", formAuthenticationFilter);
	shiroFilterFactory.setFilters(filterMap);
	
	// 设置过滤链,根据取出顺序执行
	Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
	// 静态资源可以匿名访问
	filterChainDefinitionMap.put("/static/**", "anon");
	filterChainDefinitionMap.put("/images/**", "anon");
	filterChainDefinitionMap.put("/js/**", "anon");
	filterChainDefinitionMap.put("/styles/**", "anon");
	// 退出
	filterChainDefinitionMap.put("/logout", "logout");
	// 所有url必须认证通过才能访问
	filterChainDefinitionMap.put("/**", "authc");
	shiroFilterFactory.setFilterChainDefinitionMap(filterChainDefinitionMap);
	
	return shiroFilterFactory;
}

猜你喜欢

转载自blog.csdn.net/qq_33755556/article/details/80270493