权限管理
为了实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问且只能访问自己被授权的资源。
用户认证
为了验证用户访问系统的合法性。
用户授权
在用户认证通过后,只能访问被系统授权的资源,授权过程可以理解为who对what(which)进行how操作
关键对象
- subject:主体
访问系统资源的对象,权限管理需要对主体进行身份认证 - principal:身份信息
身份信息通常是唯一的,一个主体可能会有多个身份信息,都有一个主身份信息(primary principal) - credential:凭证信息
密码、证书等,主体在进行身份认证时需要身份信息和凭证信息 - resource 资源
必须具备相应权限才可以访问的对象 - permission 权限/许可
主体需要相应权限才能访问、操作相应资源
权限模型
- 主体:账户、密码
- 角色:角色名称
- 权限:权限名称、资源名称、资源访问地址
- 主体与角色的关系
- 角色与权限的关系
权限控制
基于角色的访问控制
RBAC(Role Based Access Control),基于角色的访问控制
基于资源的访问控制
RBAC(Resource Based Access Control),基于资源的访问控制
权限粒度
- 粗粒度权限管理:对资源类型的权限管理
- 细粒度权限管理:对资源实例的权限管理
shiro架构
- Subject:主体
- SecurityManager:安全管理器,进行主体的认证和授权
- Authenticator:用户认证管理器
- Authorizer:权限管理器
- SessionManager:web应用中一般是用web容器对session管理,shiro也提供一套管理session的方式
- SessionDao:对Session进行CRUD操作(可与redis集成管理session数据)
- CacheManager:缓存管理器,主要对session和授权数据进行缓存
- Cryptography:加密方式
- Realm:存取认证、授权相关数据(逻辑)
shiro缓存
当需要访问受限资源时,会实时去查询权限数据,这样的查询是频繁的,而权限信息又不是经常变化的,所以需要配置缓存来提高性能。
缓存带来的问题:当用户不退出系统(正常退出、非正常退出),是不会清空缓存的,如果权限发生变更,不能及时改变用户所拥有的权限。
shiro会话
shiro支持通过SessionManager取代web容器来管理会话,可以通过配置SessionDao(对Session的CRUD)集成Reis集群来对session进行共享、更新、删除。
使用Spring集成Shiro
数据库设计
DROP TABLE IF EXISTS users;
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT
COMMENT '用户编号',
name VARCHAR(255) NOT NULL
COMMENT '用户名称',
username VARCHAR(255) NOT NULL
COMMENT '账号',
password VARCHAR(255) NOT NULL
COMMENT '密码',
salt VARCHAR(255) NOT NULL
COMMENT '盐',
status TINYINT NOT NULL DEFAULT 1
COMMENT '用户状态 0-无效,1-有效',
PRIMARY KEY (id),
UNIQUE KEY (username)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8
COMMENT = '用户';
DROP TABLE IF EXISTS roles;
CREATE TABLE roles (
id INT NOT NULL AUTO_INCREMENT
COMMENT '角色编号',
role_name VARCHAR(255) NOT NULL
COMMENT '角色名称',
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8
COMMENT = '角色';
DROP TABLE IF EXISTS permission;
CREATE TABLE permission (
id INT NOT NULL AUTO_INCREMENT
COMMENT '权限编号',
url VARCHAR(255) NOT NULL
COMMENT 'url地址',
url_name VARCHAR(255) NOT NULL
COMMENT 'url描述',
perm VARCHAR(255) NOT NULL
COMMENT '权限标识符',
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8
COMMENT = '权限';
DROP TABLE IF EXISTS user_roles;
CREATE TABLE user_roles (
user_id INT NOT NULL
COMMENT '用户编号',
role_id INT NOT NULL
COMMENT '角色编号',
PRIMARY KEY (user_id, role_id)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8
COMMENT = '用户-角色';
DROP TABLE IF EXISTS role_permissions;
CREATE TABLE role_permissions (
role_id INT NOT NULL
COMMENT '角色编号',
permission_id INT NOT NULL
COMMENT '权限编号',
PRIMARY KEY (role_id, permission_id)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8
COMMENT = '角色-权限';
依赖
除了基本的Spring依赖,还需要shiro-spring、shiro-cache、aspectj。
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.11</version>
</dependency>
spring-shiro配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- shiroFilter -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- 登录地址(登录页面地址,不拦截,登录失败跳回该页) -->
<property name="loginUrl" value="/"/>
<!-- 成功登录跳转地址 -->
<property name="successUrl" value="/home"/>
<!-- 自定义表单验证filter配置 -->
<property name="filters">
<map>
<entry key="authc" value-ref="authFormFilter" />
</map>
</property>
<!-- 过滤器链定义,由上往下顺序执行 -->
<property name="filterChainDefinitions">
<value>
<!-- 设置静态资源匿名访问 -->
/resources/** = anon
<!-- ajax登录url,不拦截 -->
/login = anon
<!-- 配置登出url -->
/logout = logout
<!-- 此处可以配置权限,也可在类或方法上标注
/home = authc
/query = perms[/query]
/add = perms[/add]
/update = perms[/update]
/delete = perms[/delete]
-->
</value>
</property>
</bean>
<!-- securityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="authRealm"/>
<property name="cacheManager" ref="cacheManager"/>
<property name="sessionManager" ref="sessionManager"/>
</bean>
<!-- 配置realm,用于认证、授权 -->
<bean id="systemRealm" class="com.wch.ssm.shiro.realm.SystemRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!-- 配置凭证匹配器,加密方式和hash次数 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="md5"/>
<property name="hashIterations" value="1"/>
</bean>
<!-- cacheManager -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:config/shiro-ehcache.xml"/>
</bean>
<!-- sessionManager -->
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<!-- 设置session的失效时长 -->
<property name="globalSessionTimeout" value="600000"/>
<!-- 删除失效的session -->
<property name="deleteInvalidSessions" value="true"/>
</bean>
<!-- 配置自定义表单验证过滤器 -->
<bean id="authFormFilter" class="com.wch.ssm.shiro.AuthFormFilter"/>
</beans>
自定义Realm
/**
* 自定义Realm,用于认证和授权
*/
public class AuthRealm extends AuthorizingRealm {
@Resource
private SecurityService securityService;
private static final Logger LOGGER = LoggerFactory.getLogger(AuthRealm.class);
/**
* 认证
*
* @param token token
* @return AuthenticationInfo
* @throws AuthenticationException AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
User user = securityService.getPasswordAndSalt(username);
if (null == user) {
throw new UnknownAccountException("不存在该账户!");
}
String name = user.getName();
String password = user.getPassword();
String salt = user.getSalt();
if (null == name || null == password || null == salt) {
throw new AccountException("账户异常!");
}
// 身份信息,密码(数据库中加密后的密码),salt,realmName
return new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(salt), this.getName());
}
/**
* 授权
*
* @param principals principals
* @return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = null;
try {
// 获取身份信息
User user = (User) principals.getPrimaryPrincipal();
// 查询权限信息
Set<String> permissions = securityService.getStringPermissions(user.getId());
info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
return info;
}
/**
* 用户权限发生变动,调用此方法清除缓存
*/
public void clearCache() {
PrincipalCollection principals = SecurityUtils.getSubject().getPrincipals();
super.clearCache(principals);
}
}
控制器
/**
* 验证登录
*
* @return json data
* @throws ShiroException ShiroException
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public @ResponseBody
Result login(String username, String password) {
try {
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 登录失败:包括账户不存在、密码错误等,都会抛出ShiroException
SecurityUtils.getSubject().login(token);
return Result.response(ResultEnum.SUCCESS);
} catch (ShiroException e) {
LOGGER.error("登录失败,{},{}", e.getClass().getName(), e.getMessage());
return Result.response(ResultEnum.FAIL);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
return Result.response(ResultEnum.FAIL);
}
}
/**
* successUrl
* 使用注解 @RequiresAuthentication 来标注该访问该url需要认证
*
* @param model model
* @return Page
*/
@RequestMapping("/home")
@RequiresAuthentication
public String home(Model model) {
// 获取在身份认证时放入的身份信息
User user = (User) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("name", user.getName());
return "home";
}
/**
* unauthorizedUrl,未授权时跳转该url
*
* @return json
*/
@ExceptionHandler(UnauthorizedException.class)
@RequiresAuthentication
public @ResponseBody
String forbidden() {
return "403";
}
/**
* 使用 @RequiresPermissions 注解来标注访问该url需要 "user:query" 权限
*
* @return json
*/
@RequestMapping("/query")
@RequiresPermissions("user:query")
public @ResponseBody
String query() {
return "permit query.";
}
登录交互
<script type="text/javascript">
$('#submit').click(function () {
$.ajax({
url: 'login',
type: 'POST',
data: {
username: $('#username').val().trim(),
password: $('#password').val().trim()
},
success: function (res) {
if (res.code === 200) {
window.location.href = 'home'
} else {
alert("Login Failed!");
}
}
});
});
</script>
使用SpringBoot集成shiro
配置ShiroConfig
对于需要配置权限的url,每个都配置注解是很不方便的,可以通过应用启动时查询持久化到数据库中的权限配置来生成拦截器链。
ShiroConfig加载到容器中时,查询权限的Service可能还未注入,导致空指针异常。因此在ShiroConfig中应使用手动注入的方式来获取查询权限Service。
获取ApplicationContext
为了获取ApplicationContext,ShiroConfig需要实现ApplicationContextAware接口,实现setApplicationContext()方法。
private ApplicationContext context;
/**
* 获取ApplicationContext
*
* @param applicationContext applicationContext
* @throws BeansException BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
适配权限标识
在自定义Realm中重写的doGetAuthorizationInfo()方法,返回类型SimpleAuthorizationInfo,添加权限的方式是通过ddStringPermissions(Collection<String> permissions)添加权限的字符串形式,例如sys:add,但是在拦截器链中配置权限的要求是perms[sys:add]的形式,因此需要对权限标识进行适配。
/**
* 适配拦截器权限标识符
*
* @param perm perm
* @return perms[]
*/
private String adaptPerms(String perm) {
StringBuilder sb = new StringBuilder();
sb.append("perms[").append(perm).append("]");
return sb.toString();
}
配置拦截器链
// 拦截器链,由上到下顺序执行
Map<String, String> filterChain = new LinkedHashMap<>();
// 动态添加权限
SecurityService securityService = null;
while (securityService == null) {
securityService = (SecurityService) context.getBean("securityServiceImpl");
}
List<Permission> permissions = securityService.getPermissions();
for (Permission permission : permissions) {
filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
}
完整配置
@Configuration
public class ShiroConfig implements ApplicationContextAware {
private ApplicationContext context;
/**
* 配置realm,用于认证、授权
*
* @return Realm
*/
@Bean
public Realm authRealm() {
// 凭证匹配器,配置加密方式和hash次数
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(CommonConstants.HASH_CREDENTIAL_NAME);
credentialsMatcher.setHashIterations(CommonConstants.HASH_ITERATIONS);
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(credentialsMatcher);
return authRealm;
}
/**
* 配置EhCache缓存管理器,用于授权信息缓存
*
* @return CacheManager
*/
private CacheManager getEhCacheManager() {
EhCacheManager cacheManager = new EhCacheManager();
cacheManager.setCacheManagerConfigFile("classpath:config/shiro-ehcache.xml");
return cacheManager;
}
/**
* 配置SecurityManager
*
* @return SecurityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authRealm());
securityManager.setCacheManager(getEhCacheManager());
return securityManager;
}
/**
* 设置由servlet容器管理filter生命周期
*
* @return LifecycleBeanPostProcessor
*/
@Bean
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启aop,对类代理
*
* @return Proxy
*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/**
* 开启shiro注解支持
*
* @return Advisor
*/
@Bean
public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
/**
* 配置shiroFilter,beanName必须为shiroFilter
*
* @return ShiroFilter
*/
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
// 配置SecurityManager
filter.setSecurityManager(securityManager());
// 配置登录页
filter.setLoginUrl("/");
// 登录成功跳转链接
filter.setSuccessUrl("/sys");
// 未授权界面
filter.setUnauthorizedUrl("/403");
// 拦截器链,由上到下顺序执行
Map<String, String> filterChain = new LinkedHashMap<>();
// 配置ajax登录url匿名访问
filterChain.put("/login", "anon");
// 配置登出路径
filterChain.put("/logout", "logout");
// 静态资源处理
filterChain.put("/js/**", "anon");
filterChain.put("/css/**", "anon");
filterChain.put("/img/**", "anon");
// 动态添加权限
SecurityService securityService = null;
while (securityService == null) {
securityService = (SecurityService) context.getBean("securityServiceImpl");
}
List<Permission> permissions = securityService.getPermissions();
for (Permission permission : permissions) {
filterChain.put(permission.getUrl(), this.adaptPerms(permission.getPerm()));
}
// 认证后访问
filterChain.put("/**", "authc");
filter.setFilterChainDefinitionMap(filterChain);
return filter;
}
/**
* 适配拦截器权限标识符
*
* @param perm perm
* @return perms[]
*/
private String adaptPerms(String perm) {
StringBuilder sb = new StringBuilder();
sb.append("perms[").append(perm).append("]");
return sb.toString();
}
/**
* 获取ApplicationContext
*
* @param applicationContext applicationContext
* @throws BeansException BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
}