1.准备数据
用户-角色-权限 RBAC模型
| 用户 | 角色 | 权限 |
|---|---|---|
| luo | 用户管理员 | 对后台用户的CRU |
| zhou | 仓库管理员 | 对仓库数据的CRU |
| admin | 超级管理员 | 所有库中的权限 |
业务描述:
当用户访问首页时,尽请访问
当用户查看用户列表时,需要登录、需要有该权限
当用户查看仓库列表时,需要有仓库权限
当用户删除用户时,需要有超级管理员角色
2.springboot项目
2.1 引入依赖
2.2 pojo
2.3 DAO
是用mybatis plus
https://mp.baomidou.com/guide/
spring
接口继承BaseMapper<T>
public interface AdministratorMapper extends BaseMapper<Administrator> {
}
pojo添加注解
引导类添加扫描
@MapperScan("com.itheima.shiro.mapper")
2.4 service
public interface AdminService {
}
controller
省略...
视图
<!--使用thymeleaf 首先完成一个登陆页面-->
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org/">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<body>
<!--<h5 th:text="${err_msg}"></h5>-->
<form action="/backend/login" method="post">
<input name="username"/><br>
<input name="password"/><br>
<input type="submit" value="登录"/>
</form>
</body>
</html>
3.shiro配置
3.1 用户访问路径测试
需求:用户未登录时,访问/user/all路径,告诉用户调到登录页面
添加shiro配置:安全管理器、realm、shiroFilter
@Configuration
public class ShiroConfig {
//0.配置shiroFilter
@Bean
public ShiroFilterFactoryBean shiroFilter(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
shiroFilterFactoryBean.setLoginUrl("/backend/toLogin");
Map filterChainMap = new LinkedHashMap<String,String>();
filterChainMap.put("/backend/toLogin","anon"); //跳转登录页面放行
filterChainMap.put("/backend/login","anon"); //登录请求 放行
filterChainMap.put("/**","authc"); //认证
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
return shiroFilterFactoryBean;
}
//1.配置安全管理器
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//2.配置realm
@Bean
public Realm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
}
4.认证(登录)
需求:用户新增时,密码进行加密(md5+随机盐加密): MD5(明文密码+随机salt)
用户创建
public void saveAdmin(Administrator admin) {
String password = admin.getPassword();
String salt = RandomStringUtils.randomNumeric(6,8);
admin.setPrivateSalt(salt);
Md5Hash md5Hash = new Md5Hash(password,salt); //模拟md5加密一次
admin.setPassword(md5Hash.toString());
admin.setUserStatus("1");
adminMapper.insert(admin);
}
登录配置、测试、访问
@RequestMapping("/login")
public String login(@RequestParam String username, @RequestParam String password){
//登录
try{
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
subject.login(token);
}catch (Exception e){
e.printStackTrace();
}
return "success";
}
配置、开发realm
//realm需要密码匹配器设置
public CredentialsMatcher myMd5Matcher(){
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");
matcher.setHashIterations(1);
return matcher;
}
realm的认证信息完善:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("经过认证用户的获取");
UsernamePasswordToken loginToken = (UsernamePasswordToken)token;
String username = loginToken.getUsername();
//根据用户名查询用户
Administrator admin = adminService.findAdminByUsername(username);
if(admin == null){
return null; //框架自动抛出位置账户异常
}else{
ByteSource saltBS = new SimpleByteSource(admin.getPrivateSalt());
return new SimpleAuthenticationInfo(admin,admin.getPassword(),saltBS,getName());
}
}
退出
filterChainMap.put("/backend/logout","logout");
//也可以准备一个controller方法,使用Subject的方法进行退出 Subject subject = SecurityUtils.getSubject(); subject.logout();
5.授权
当用户查看用户列表时,需要登录、需要有该权限
filterChainMap.put("/user/all","perms[user:select]"); //查询所有用户 需要认证(登录)
//当用户查看仓库列表时,需要有仓库权限
filterChainMap.put("/storage/all","perms[storage:select]");
//当用户删除用户时,需要有超级管理员角色
filterChainMap.put("/user/del/*","roles[role_superman]");
权限控制:角色、权限
filterChainMap.put("/user/all","perms[user:select]"); //需要权限 user:select
filterChainMap.put("/user/*","roles[role_user]"); //需要角色 role_user
赋权:
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("经过权限获取");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//从数据库查询该用户的权限列表
Administrator principal = (Administrator) principals.getPrimaryPrincipal();
String password = principal.getPassword();
simpleAuthorizationInfo.addStringPermission("user:select"); //为当前登录用户主体赋权
return simpleAuthorizationInfo;
}
数据库数据赋权:
private void addPerms(String username,SimpleAuthorizationInfo simpleAuthorizationInfo){
Set<String> roleSet = adminService.findRolesByUsername(username);
if(roleSet != null && roleSet.size() >0){
simpleAuthorizationInfo.addRoles(roleSet);
}
Set<String> permissionSet = adminService.findPermissionsByUsername(username);
if(permissionSet != null && permissionSet.size() >0){
simpleAuthorizationInfo.addStringPermissions(permissionSet);
}
}
6.注解权限控制
@RequiresPermissions("page:storage")
@RequiresRoles("role_superman")
只是用注解是不生效的,需要添加配置
/**
* 注解支持:
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
7.页面标签权限控制
需要引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
配置标签支持
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
在页面中使用标签
<shiro:principal property="username"></shiro:principal>
8.会话管理(redis)
自定义会话管理器
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
//设置会话过期时间
sessionManager.setGlobalSessionTimeout(3*60*1000); //默认半小时
sessionManager.setDeleteInvalidSessions(true); //默认自定调用SessionDAO的delete方法删除会话
//设置会话定时检查
// sessionManager.setSessionValidationInterval(180000); //默认一小时
// sessionManager.setSessionValidationSchedulerEnabled(true);
return sessionManager;
}
@Bean
public SessionDAO redisSessionDAO(){
ShiroRedisSessionDao redisDAO = new ShiroRedisSessionDao();
return redisDAO;
}
自定义CachingSessionDao
public class ShiroRedisSessionDao extends CachingSessionDAO {
public static final String SHIRO_SESSION_KEY = "shiro_session_key";
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doUpdate(Session session) {
this.saveSession(session);
}
@Override
protected void doDelete(Session session) {
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return ;
}
//根据session id删除session
redisTemplate.boundHashOps(SHIRO_SESSION_KEY).delete(session.getId());
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if(sessionId == null){
logger.error("传入的 session id is null");
return null;
}
return (Session)redisTemplate.boundHashOps(SHIRO_SESSION_KEY).get(sessionId);
}
/**
* 将session 保存进redis 中
* @param session 要保存的session
*/
private void saveSession(Session session) {
if (session == null || session.getId() == null) {
logger.error("session or session id is null");
return ;
}
redisTemplate.boundHashOps(SHIRO_SESSION_KEY).put(session.getId(),session);
}
}
交给安全管理器
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setSessionManager(sessionManager());
securityManager.setRealm(myRealm());
return securityManager;
}
9.缓存管理(redis)
每次访问带有权限相关的判断的请求时,都会执行doGetAuthorizationInfo()方法 可以缓存授权权限信息,不需要每次都查询数据库赋权 其实,shiro默认支持的缓存是ehcache(java语言开发的本地缓存技术,依赖jvm)
自定义缓存管理器
public class MyRedisCacheManager implements CacheManager {
@Autowired
private RedisTemplate redisTemplate;
@Override
public <K, V> Cache<K, V> getCache(String name) throws CacheException {
return new ShiroRedisCache(name,redisTemplate);
}
}
自定义redis缓存
package com.itheima.shiroConfig;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
*
*/
public class ShiroRedisCache<K, V> implements Cache<K, V> {
private static Logger LOGGER = LogManager.getLogger(ShiroRedisCache.class);
/**
* key前缀
*/
private static final String REDIS_SHIRO_CACHE_KEY_PREFIX = "shiro_cache_key_";
/**
* cache name
*/
private String name;
/**
* jedis 连接工厂
*/
private RedisTemplate redisTemplate;
/**
* 序列化工具
*/
private RedisSerializer serializer = new JdkSerializationRedisSerializer();
/**
* 存储key的redis.list的key值
*/
private String keyListKey;
private RedisConnection getConnection(){
return this.redisTemplate.getConnectionFactory().getConnection();
}
public ShiroRedisCache(String name,RedisTemplate redisTemplate) {
this.name = name;
this.redisTemplate = redisTemplate;
this.keyListKey = REDIS_SHIRO_CACHE_KEY_PREFIX + name;
}
@Override
public V get(K key) throws CacheException {
LOGGER.debug("shiro redis cache get.{} K={}", name, key);
RedisConnection redisConnection = null;
V result = null;
try {
redisConnection = getConnection();
result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key))));
} catch (Exception e) {
LOGGER.error("shiro redis cache get exception. ", e);
} finally {
if (null != redisConnection) {
redisConnection.close();
}
}
return result;
}
@Override
public V put(K key, V value) throws CacheException {
LOGGER.debug("shiro redis cache put.{} K={} V={}", name, key, value);
RedisConnection redisConnection = null;
V result = null;
try {
redisConnection = getConnection();
result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key))));
redisConnection.set(serializer.serialize(generateKey(key)), serializer.serialize(value));
redisConnection.lPush(serializer.serialize(keyListKey), serializer.serialize(generateKey(key)));
} catch (Exception e) {
LOGGER.error("shiro redis cache put exception. ", e);
} finally {
if (null != redisConnection) {
redisConnection.close();
}
}
return result;
}
@Override
public V remove(K key) throws CacheException {
LOGGER.debug("shiro redis cache remove.{} K={}", name, key);
RedisConnection redisConnection = null;
V result = null;
try {
redisConnection = getConnection();
result = (V) serializer.deserialize(redisConnection.get(serializer.serialize(generateKey(key))));
redisConnection.expireAt(serializer.serialize(generateKey(key)), 0);
redisConnection.lRem(serializer.serialize(keyListKey), 1, serializer.serialize(key));
} catch (Exception e) {
LOGGER.error("shiro redis cache remove exception. ", e);
} finally {
if (null != redisConnection) {
redisConnection.close();
}
}
return result;
}
@Override
public void clear() throws CacheException {
LOGGER.debug("shiro redis cache clear.{}", name);
RedisConnection redisConnection = null;
try {
redisConnection = getConnection();
Long length = redisConnection.lLen(serializer.serialize(keyListKey));
if (0 == length) {
return;
}
List<byte[]> keyList = redisConnection.lRange(serializer.serialize(keyListKey), 0, length - 1);
for (byte[] key : keyList) {
redisConnection.expireAt(key, 0);
}
redisConnection.expireAt(serializer.serialize(keyListKey), 0);
keyList.clear();
} catch (Exception e) {
LOGGER.error("shiro redis cache clear exception.", e);
} finally {
if (null != redisConnection) {
redisConnection.close();
}
}
}
@Override
public int size() {
LOGGER.debug("shiro redis cache size.{}", name);
RedisConnection redisConnection = null;
int length = 0;
try {
redisConnection = getConnection();
length = Math.toIntExact(redisConnection.lLen(serializer.serialize(keyListKey)));
} catch (Exception e) {
LOGGER.error("shiro redis cache size exception.", e);
} finally {
if (null != redisConnection) {
redisConnection.close();
}
}
return length;
}
@Override
public Set keys() {
LOGGER.debug("shiro redis cache keys.{}", name);
RedisConnection redisConnection = null;
Set resultSet = null;
try {
redisConnection = getConnection();
Long length = redisConnection.lLen(serializer.serialize(keyListKey));
if (0 == length) {
return resultSet;
}
List<byte[]> keyList = redisConnection.lRange(serializer.serialize(keyListKey), 0, length - 1);
resultSet = keyList.stream().map(bytes -> serializer.deserialize(bytes)).collect(Collectors.toSet());
} catch (Exception e) {
LOGGER.error("shiro redis cache keys exception.", e);
} finally {
if (null != redisConnection) {
redisConnection.close();
}
}
return resultSet;
}
@Override
public Collection values() {
RedisConnection redisConnection = getConnection();
Set keys = this.keys();
List<Object> values = new ArrayList<Object>();
for (Object key : keys) {
byte[] bytes = redisConnection.get(serializer.serialize(key));
values.add(serializer.deserialize(bytes));
}
return values;
}
/**
* 重组key
* 区别其他使用环境的key
*
* @param key
* @return
*/
private String generateKey(K key) {
return REDIS_SHIRO_CACHE_KEY_PREFIX + name + "_" + key;
}
private byte[] getByteKey(K key) {
if (key instanceof String) {
String preKey = generateKey(key);
return preKey.getBytes();
}
return serializer.serialize(key);
}
}
可以只在realm中设置缓存管理器
//
@Bean
public Realm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(myMd5Matcher());
myShiroRealm.setAuthorizationCacheName("perms");
myShiroRealm.setAuthorizationCachingEnabled(true);
myShiroRealm.setAuthenticationCachingEnabled(false);
//设置缓存管理器
myShiroRealm.setCacheManager(cacheManager());
return myShiroRealm;
}
//缓存管理
@Bean
public CacheManager cacheManager(){
MyRedisCacheManager cacheManager = new MyRedisCacheManager();
return cacheManager;
}
注意,我在此处做得会话和缓存管理没有对过期的缓存数据进行定时清理!!!
有一个已经第三方框架做了对shiro和redis的整合:
https://github.com/alexxiyang/shiro-redis -- 把会话管理和缓存管理都整合好了,直接依赖即可
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
10.异常处理
可以使用全局异常处理器来捕获权限异常
@ControllerAdvice
public class GloableExceptionResolver {
@ExceptionHandler(UnauthorizedException.class)
public void calUnauthorizedException(UnauthorizedException e){
PrintWriter writer = null;
try{
//判断是否是异步请求
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
HttpServletResponse response = requestAttributes.getResponse();
String header = request.getHeader("X-Requested-With");
if(StringUtils.isNoneBlank(header) && "XMLHttpRequest".equalsIgnoreCase(header)){
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
writer = response.getWriter();
// {"status":401,"message":"无权访问"}
// String respStr = ""
writer.write("{"status":401,"message":"无权访问"}");
}else{
String contextPath = request.getContextPath();
if("/".equals(contextPath))
contextPath = "";
response.sendRedirect(request.getContextPath() + "/backend/toDenied");
}
}catch (IOException io){
io.printStackTrace();
}finally {
if(writer != null)
writer.close();
}
}
}