此文章作为自己学习总结用。请各位看官多多指正留言或发邮件给我。
邮箱地址:594187062@qq.com
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;
}