權(quán)限管理
為了實現(xiàn)對用戶訪問系統(tǒng)的控制,按照安全規(guī)則或安全策略控制用戶可以訪問且只能訪問自己被授權(quán)的資源纷宇。
用戶認證
為了驗證用戶訪問系統(tǒng)的合法性。
用戶授權(quán)
在用戶認證通過后,只能訪問被系統(tǒng)授權(quán)的資源招刨,授權(quán)過程可以理解為who對what(which)進行how操作
關(guān)鍵對象
- subject:主體
訪問系統(tǒng)資源的對象,權(quán)限管理需要對主體進行身份認證 - principal:身份信息
身份信息通常是唯一的哀军,一個主體可能會有多個身份信息计济,都有一個主身份信息(primary principal) - credential:憑證信息
密碼、證書等排苍,主體在進行身份認證時需要身份信息和憑證信息 - resource 資源
必須具備相應權(quán)限才可以訪問的對象 - permission 權(quán)限/許可
主體需要相應權(quán)限才能訪問沦寂、操作相應資源
權(quán)限模型
- 主體:賬戶、密碼
- 角色:角色名稱
- 權(quán)限:權(quán)限名稱淘衙、資源名稱传藏、資源訪問地址
- 主體與角色的關(guān)系
- 角色與權(quán)限的關(guān)系
權(quán)限控制
基于角色的訪問控制
RBAC(Role Based Access Control),基于角色的訪問控制
基于資源的訪問控制
RBAC(Resource Based Access Control)彤守,基于資源的訪問控制
權(quán)限粒度
- 粗粒度權(quán)限管理:對資源類型的權(quán)限管理
- 細粒度權(quán)限管理:對資源實例的權(quán)限管理
shiro架構(gòu)
- Subject:主體
- SecurityManager:安全管理器毯侦,進行主體的認證和授權(quán)
- Authenticator:用戶認證管理器
- Authorizer:權(quán)限管理器
- SessionManager:web應用中一般是用web容器對session管理,shiro也提供一套管理session的方式
- SessionDao:對Session進行CRUD操作(可與redis集成管理session數(shù)據(jù))
- CacheManager:緩存管理器具垫,主要對session和授權(quán)數(shù)據(jù)進行緩存
- Cryptography:加密方式
- Realm:存取認證侈离、授權(quán)相關(guān)數(shù)據(jù)(邏輯)
shiro緩存
當需要訪問受限資源時,會實時去查詢權(quán)限數(shù)據(jù)筝蚕,這樣的查詢是頻繁的卦碾,而權(quán)限信息又不是經(jīng)常變化的铺坞,所以需要配置緩存來提高性能。
緩存帶來的問題:當用戶不退出系統(tǒng)(正常退出洲胖、非正常退出)济榨,是不會清空緩存的,如果權(quán)限發(fā)生變更绿映,不能及時改變用戶所擁有的權(quán)限擒滑。
shiro會話
shiro支持通過SessionManager取代web容器來管理會話,可以通過配置SessionDao(對Session的CRUD)集成Reis集群來對session進行共享叉弦、更新丐一、刪除。
使用Spring集成Shiro
數(shù)據(jù)庫設計
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 '用戶狀態(tài) 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 '權(quán)限編號',
url VARCHAR(255) NOT NULL
COMMENT 'url地址',
url_name VARCHAR(255) NOT NULL
COMMENT 'url描述',
perm VARCHAR(255) NOT NULL
COMMENT '權(quán)限標識符',
PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8
COMMENT = '權(quán)限';
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 '權(quán)限編號',
PRIMARY KEY (role_id, permission_id)
)
ENGINE = INNODB
DEFAULT CHARSET = utf8
COMMENT = '角色-權(quán)限';
依賴
除了基本的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="/"/>
<!-- 成功登錄跳轉(zhuǎn)地址 -->
<property name="successUrl" value="/home"/>
<!-- 自定義表單驗證filter配置 -->
<property name="filters">
<map>
<entry key="authc" value-ref="authFormFilter" />
</map>
</property>
<!-- 過濾器鏈定義拧略,由上往下順序執(zhí)行 -->
<property name="filterChainDefinitions">
<value>
<!-- 設置靜態(tài)資源匿名訪問 -->
/resources/** = anon
<!-- ajax登錄url,不攔截 -->
/login = anon
<!-- 配置登出url -->
/logout = logout
<!-- 此處可以配置權(quán)限瘪弓,也可在類或方法上標注
/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垫蛆,用于認證、授權(quán) -->
<bean id="systemRealm" class="com.wch.ssm.shiro.realm.SystemRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
<!-- 配置憑證匹配器腺怯,加密方式和hash次數(shù) -->
<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袱饭,用于認證和授權(quán)
*/
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("賬戶異常呛占!");
}
// 身份信息虑乖,密碼(數(shù)據(jù)庫中加密后的密碼),salt晾虑,realmName
return new SimpleAuthenticationInfo(user, password, ByteSource.Util.bytes(salt), this.getName());
}
/**
* 授權(quán)
*
* @param principals principals
* @return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo info = null;
try {
// 獲取身份信息
User user = (User) principals.getPrimaryPrincipal();
// 查詢權(quán)限信息
Set<String> permissions = securityService.getStringPermissions(user.getId());
info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
return info;
}
/**
* 用戶權(quán)限發(fā)生變動疹味,調(diào)用此方法清除緩存
*/
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洪灯,未授權(quán)時跳轉(zhuǎn)該url
*
* @return json
*/
@ExceptionHandler(UnauthorizedException.class)
@RequiresAuthentication
public @ResponseBody
String forbidden() {
return "403";
}
/**
* 使用 @RequiresPermissions 注解來標注訪問該url需要 "user:query" 權(quán)限
*
* @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
對于需要配置權(quán)限的url,每個都配置注解是很不方便的竟痰,可以通過應用啟動時查詢持久化到數(shù)據(jù)庫中的權(quán)限配置來生成攔截器鏈签钩。
ShiroConfig加載到容器中時掏呼,查詢權(quán)限的Service可能還未注入,導致空指針異常边臼。因此在ShiroConfig中應使用手動注入的方式來獲取查詢權(quán)限Service哄尔。
獲取ApplicationContext
為了獲取ApplicationContext假消,ShiroConfig需要實現(xiàn)ApplicationContextAware接口柠并,實現(xiàn)setApplicationContext()方法。
private ApplicationContext context;
/**
* 獲取ApplicationContext
*
* @param applicationContext applicationContext
* @throws BeansException BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
適配權(quán)限標識
在自定義Realm中重寫的doGetAuthorizationInfo()方法富拗,返回類型SimpleAuthorizationInfo臼予,添加權(quán)限的方式是通過ddStringPermissions(Collection<String> permissions)添加權(quán)限的字符串形式,例如sys:add啃沪,但是在攔截器鏈中配置權(quán)限的要求是perms[sys:add]的形式粘拾,因此需要對權(quán)限標識進行適配。
/**
* 適配攔截器權(quán)限標識符
*
* @param perm perm
* @return perms[]
*/
private String adaptPerms(String perm) {
StringBuilder sb = new StringBuilder();
sb.append("perms[").append(perm).append("]");
return sb.toString();
}
配置攔截器鏈
// 攔截器鏈创千,由上到下順序執(zhí)行
Map<String, String> filterChain = new LinkedHashMap<>();
// 動態(tài)添加權(quán)限
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缰雇,用于認證、授權(quán)
*
* @return Realm
*/
@Bean
public Realm authRealm() {
// 憑證匹配器追驴,配置加密方式和hash次數(shù)
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(CommonConstants.HASH_CREDENTIAL_NAME);
credentialsMatcher.setHashIterations(CommonConstants.HASH_ITERATIONS);
AuthRealm authRealm = new AuthRealm();
authRealm.setCredentialsMatcher(credentialsMatcher);
return authRealm;
}
/**
* 配置EhCache緩存管理器械哟,用于授權(quán)信息緩存
*
* @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("/");
// 登錄成功跳轉(zhuǎn)鏈接
filter.setSuccessUrl("/sys");
// 未授權(quán)界面
filter.setUnauthorizedUrl("/403");
// 攔截器鏈暇咆,由上到下順序執(zhí)行
Map<String, String> filterChain = new LinkedHashMap<>();
// 配置ajax登錄url匿名訪問
filterChain.put("/login", "anon");
// 配置登出路徑
filterChain.put("/logout", "logout");
// 靜態(tài)資源處理
filterChain.put("/js/**", "anon");
filterChain.put("/css/**", "anon");
filterChain.put("/img/**", "anon");
// 動態(tài)添加權(quán)限
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;
}
/**
* 適配攔截器權(quán)限標識符
*
* @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;
}
}