一、shiro
1.1 shiro简介
????????Apache Shiro?是一个功能强大且易于使用的 Java 安全框架,它执行身份验证、授权、加密和会话管理。借助 Shiro 易于理解的 API,您可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序。
https://www.infoq.com/articles/apache-shiro/
二、springboot+mybatis整合shiro?
2.1? 引入jar包,生成代码(sys_user、sys_role、sys_menu)
<!--spring整合shiro包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.0</version>
</dependency>
2.2 编写权限相关业务接口
服务工具类
package com.aaa.springboot.util;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author :caicai
* @date :Created in 2022/7/23 20:40
* @description: 结果返回处理类
* @modified By:
* @version:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyReturn<T> {
private int code;
private String msg;
private T data;
}
package com.aaa.springboot.util;
public enum RebStatusEnum {
SUCCESS(1,"操作成功"),
FAIL(-1,"操作失败"),
PARAM_NOT_EMPTY(5001,"参数不能为空!!");
private int code;
private String msg;
RebStatusEnum(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
package com.aaa.springboot.controller;
import com.aaa.springboot.util.MyReturn;
import com.aaa.springboot.util.RebStatusEnum;
public class BaseController {
// 传入一个原始对象,得到一个成功的返回对象
public static <T> MyReturn getSuccess(T t){
return new MyReturn(RebStatusEnum.SUCCESS.getCode(), RebStatusEnum.SUCCESS.getMsg(),t);
}
// 传入一个原始对象,得到一个失败的返回对象
public static <T> MyReturn getFail(T t){
return new MyReturn(RebStatusEnum.FAIL.getCode(), RebStatusEnum.FAIL.getMsg(),t);
}
// 异常处理 传入参数不能为空
public static <T> MyReturn not_empty(T t){
return new MyReturn(RebStatusEnum.PARAM_NOT_EMPTY.getCode(), RebStatusEnum.PARAM_NOT_EMPTY.getMsg(),t);
}
}
????????用户表? ? ? dao层:
public interface UserDao {
// 根据用户名查询用户信息
User queryByUserName(String userName);
// 新增数据
int insert(User user);
}
? ? ? ? ? ? ? mapper层:
<!--根据用户名查询用户信息-->
<select id="queryByUserName" resultMap="UserMap">
select
user_id, dept_id, login_name, user_name, user_type, email, phonenumber, sex, avatar, password, salt, status, del_flag, login_ip, login_date, create_by, create_time, update_by, update_time, remark, role_id
from sys_user
where user_name = #{userName} and status=0 and del_flag=0
</select>
<!--新增所有列-->
<insert id="insert" keyProperty="userId" useGeneratedKeys="true">
insert into sys_user(dept_id, login_name, user_name, user_type, email, phonenumber, sex, avatar, password, salt, status, del_flag, login_ip, login_date, create_by, create_time, update_by, update_time, remark, role_id)
values (#{deptId}, #{loginName}, #{userName}, #{userType}, #{email}, #{phonenumber}, #{sex}, #{avatar}, #{password}, #{salt}, #{status}, #{delFlag}, #{loginIp}, #{loginDate}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{remark}, #{roleId})
</insert>
? ? ? ? ? ? server层实现类
package com.aaa.springboot.service.impl;
import com.aaa.springboot.entity.PageEntity;
import com.aaa.springboot.entity.User;
import com.aaa.springboot.dao.UserDao;
import com.aaa.springboot.service.UserService;
import com.aaa.springboot.util.ConstantUtil;
import com.aaa.springboot.util.CustomException;
import com.aaa.springboot.util.MyReturn;
import com.aaa.springboot.util.RebStatusEnum;
import com.github.pagehelper.PageHelper;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.Sha512Hash;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;
import com.github.pagehelper.PageInfo;
import javax.annotation.Resource;
import java.util.UUID;
@Service("userService")
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
// 根据用户名查询用户信息
@Override
public User queryByUserName(String userName) {
return userDao.queryByUserName(userName);
}
/**
* 用户登录
*/
@Override
public MyReturn login(User user) {
if (user.getUserName() == null || user.getPassword()== null) {
// 进行业务编写时 可以抛出自定义异常
throw new CustomException(RebStatusEnum.PARAM_NOT_EMPTY.getCode(),
RebStatusEnum.PARAM_NOT_EMPTY.getMsg());
}
// 收集用户信息(用户名和密码) 多态
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUserName(), user.getPassword());
// 获取当前用户
Subject currentUser = SecurityUtils.getSubject();
try {
// 登录
currentUser.login(usernamePasswordToken);
// 获取session
Session session = currentUser.getSession();
// 获取登录成功后的信息 SimpleAuthenticationInfo()第一个参数
User userInfo = (User) currentUser.getPrincipal();
// 设置用户信息
session.setAttribute("userInfo",userInfo);
return new MyReturn(RebStatusEnum.SUCCESS.getCode(),RebStatusEnum.SUCCESS.getMsg(),"登录成功");
} catch (AuthenticationException e) {
e.printStackTrace();
}
return new MyReturn(RebStatusEnum.FAIL.getCode(),RebStatusEnum.FAIL.getMsg(),"用户名或者密码错误");
}
}
? ? ? ? ? ? controller层:
package com.aaa.springboot.controller;
import com.aaa.springboot.entity.PageEntity;
import com.aaa.springboot.entity.User;
import com.aaa.springboot.service.UserService;
import com.aaa.springboot.util.MyReturn;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("user")
public class UserController extends BaseController {
/**
* 服务对象
*/
@Resource
private UserService userService;
/**
* 登录
*/
@GetMapping("login")
public MyReturn login(User user){
return userService.login(user);
}
}
角色表? ? ? ?dao层:
public interface RoleDao {
// 根据用户名查询角色信息
List<Role> queryByUserName(String userName);
}
mapper层:
<!-- 根据用户名查询角色信息-->
<select id="queryByUserName" resultType="com.aaa.springboot.entity.Role">
select * from sys_role r
join sys_user_role ur on r.role_id = ur.role_id
join sys_user u on ur.user_id=u.user_id
where r.`status`=0 and r.del_flag=0 and u.user_name=#{userName}
and u.`status`=0 and u.del_flag=0
</select>
server层实现类
@Service("roleService")
public class RoleServiceImpl implements RoleService {
@Resource
private RoleDao roleDao;
// 根据用户名查询角色信息
@Override
public List<Role> queryByUserName(String userName) {
return roleDao.queryByUserName(userName);
}
}
菜单表? ? dao层
public interface MenuDao {
// 根据用户名查询菜单信息
List<Menu> queryByUserName(String userName);
}
mapper 层
<!-- 根据用户名查询菜单信息-->
<select id="queryByUserName" resultType="com.aaa.springboot.entity.Menu">
select DISTINCT m.* from sys_menu m
join sys_role_menu rm on m.menu_id=rm.menu_id
join sys_user_role ur on ur.role_id=rm.role_id
join sys_user u on u.user_id=ur.user_id
where m.visible=0 and u.`status`=0 and u.del_flag=0 and user_name=#{userName}
</select>
?server层实现类
@Service("menuService")
public class MenuServiceImpl implements MenuService {
@Resource
private MenuDao menuDao;
// 根据用户名查询菜单信息
@Override
public List<Menu> queryByUserName(String userName) {
return menuDao.queryByUserName(userName);
}
}
三??shiro的具体使用
https://shiro.apache.org/spring-xml.html
3.1 shiro 加密操作
3.1.1??编写SpringShiroConfig类
package com.aaa.springboot.config;
import com.aaa.springboot.util.ConstantUtil;
import com.aaa.springboot.util.MyCustomRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author :caicai
* @date :Created in 2022/8/24 14:55
* @description:
* @modified By:
* @version:
*/
@Configuration // 相当于 spring-shiro-config.xml <beans>....</beans>
public class SpringShiroConfig {
/**
* shiro过滤工厂类 拦截所有请求,交给shiro处理
*/
@Bean // 相当 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
public ShiroFilterFactoryBean shiroFilter(){
// 实例化过滤工厂类
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 依赖注入securityManager() <property name="securityManager" ref="securityManager"/>
shiroFilterFactoryBean.setSecurityManager(securityManager());
// 设置认证失败后继续跳转的页面 通常都配置登录界面
shiroFilterFactoryBean.setLoginUrl("/html/login.html");
// 认证成功后,默认跳转界面
//shiroFilterFactoryBean.setSuccessUrl("");
// 设置认证成功后,访问了未授权方法跳转路径
shiroFilterFactoryBean.setUnauthorizedUrl("/html/unauthorized.html");
// 下面顺序不能变 不能把拦截的所有的放在放行的上面配置
// 拦截过滤链定义 !!!! 一定要使用LinkedHashMap 要有序 如果顺序出错可能就访问不了
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
// 放行配置
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/images/**","anon");
filterChainDefinitionMap.put("/html/login.html","anon");
filterChainDefinitionMap.put("/user/login","anon");
filterChainDefinitionMap.put("/user/add","anon");
// 用户退出配置
filterChainDefinitionMap.put("/logout","logout");
// 除了上面的全部拦截 必须认证才能通过
filterChainDefinitionMap.put("/**","authc");
// 拦截过滤链定义
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
/*// shiro会拦截url中含有admin的请求 authc = org.apache.shiro.web.filter.authc.FormAuthenticationFilter 该请求必须认证后才可以访问
// roles[admin]并且必须具备admin这个角色
// 例如 http://localhost:16666/admin/queryById?id=1
filterChainDefinitionMap.put("/admin/**","authc, roles[admin]");
// shiro会拦截url中含有docs的请求 authc = org.apache.shiro.web.filter.authc.FormAuthenticationFilter 该请求必须认证后才可以访问
// roles[admin]并且必须具备[document:read]的权限才能访问
filterChainDefinitionMap.put("/docs/**","authc, perms[document:read]");*/
return shiroFilterFactoryBean;
}
/**
* 实例化securityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(){
// 实例化
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 依赖注入myRealm() <property name="realm" ref="myRealm"/>
defaultWebSecurityManager.setRealm(myCustomRealm());
return defaultWebSecurityManager;
}
/**
* 实例化 myCustomRealm
*/
@Bean
public MyCustomRealm myCustomRealm(){
// 实例化
MyCustomRealm myCustomRealm = new MyCustomRealm();
// 依赖注入 CredentialsMatcher() 加密加盐算法
myCustomRealm.setCredentialsMatcher(credentialsMatcher());
return myCustomRealm;
}
/**
* 定义加密加盐算法名称和计算次数
* 实例化 HashedCredentialsMatcher
*/
@Bean
public HashedCredentialsMatcher credentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置加密加盐算法名称
hashedCredentialsMatcher.setHashAlgorithmName(ConstantUtil.CredentialsMatcher.HASH_ALGORITHM_NAME);
// 设置加密加盐 hash次数
hashedCredentialsMatcher.setHashIterations(ConstantUtil.CredentialsMatcher.HASH_ITERATIONS);
return hashedCredentialsMatcher;
}
}
3.1.2? 定义一个常量工具类
package com.aaa.springboot.util;
/**
* @author :caicai
* @date :Created in 2022/8/24 15:29
* @description:定义有一个常量类
* @modified By:
* @version:
*/
public class ConstantUtil {
/**
* shiro的校验匹配器
* 内部类
*/
public interface CredentialsMatcher{
String HASH_ALGORITHM_NAME = "SHA-512";
int HASH_ITERATIONS = 1024;
}
/**
* 还可以继续定义其他常量
*/
}
3.1.3? 编写Realm
package com.aaa.springboot.util;
import com.aaa.springboot.entity.Menu;
import com.aaa.springboot.entity.Role;
import com.aaa.springboot.entity.User;
import com.aaa.springboot.service.MenuService;
import com.aaa.springboot.service.RoleService;
import com.aaa.springboot.service.UserService;
import com.alibaba.druid.util.StringUtils;
import lombok.extern.log4j.Log4j2;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import javax.annotation.Resource;
import java.util.List;
/**
* @author :caicai
* @date :Created in 2022/8/23 13:32
* @description: MyCustomRealm
* @modified By:
* @version:
*/
@Log4j2
public class MyCustomRealm extends AuthorizingRealm {
// 依赖注入用户接口
@Resource
private UserService userService;
// 如果根据用户编号查询角色集合 放入roleService
@Resource
private RoleService roleService;
@Resource
private MenuService menuService;
/**
* 获取授权信息数据
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取认证中的 SimpleAuthenticationInfo()第一个参数
User user = (User) principalCollection.getPrimaryPrincipal();
// 获取用户名
String userName = user.getUserName();
// 根据用户名获取当前用户对应的角色列表和菜单列表
List<Role> roles = roleService.queryByUserName(userName);
List<Menu> menus = menuService.queryByUserName(userName);
// 实例化返回对象
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 循环遍历
//循环添加角色集合
for (Role role : roles) {
log.info("角色-------------"+role.getRoleKey());
simpleAuthorizationInfo.addRole(role.getRoleKey());
}
//循环添加权限集合
for (Menu menu : menus) {
log.info("权限-------------"+menu.getPerms());
if (!StringUtils.isEmpty(menu.getPerms())) {
simpleAuthorizationInfo.addStringPermission(menu.getPerms());
}
}
return simpleAuthorizationInfo;
}
/**
* 获取认证信息数据
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 通过认证信息获取收集到的用户名 就是用户名 admin
String username = authenticationToken.getPrincipal().toString();
// 调用服务层userService 根据用户名,查询该用户的信息
User user = userService.queryByUserName(username);
// 判断用户是否为空,如果为空,说明用户不存在,说明用户名不对
if (user == null){
// 抛出账号异常
throw new AccountException();
}
// 如果抛异常,说明用户名错误,程序不再执行 ,如果正确,执行下面代码
// 获取数据库中的加密密码
String password = user.getPassword();
// 获取数据库中的颜值 随机 增加密码破解难道
String salt = user.getSalt();
//通过实例化该类,它会把从数据库中获取到的安全数据,交给SecurityManager进行认证
//SimpleAuthenticationInfo的构造第1个参数: 把用户的信息进行传递,方便认证成功后,保存在session中使用
// 构造第2个参数: 数据库获取的加密密码 SecurityManager底层会进行校验
// 构造第3个参数: 数据库获取的加密盐值 颜值封装对象 使用盐值构造出的ByteSource
// 构造第4个参数: getName()获取当前realm的名称 该方法可以重写
return new SimpleAuthenticationInfo(user,password,
ByteSource.Util.bytes(salt),getName());
}
}
3.1.4?编写登录及用户添加
用户登录:? 登录时做认证
/**
* 用户登录
*/
@Override
public MyReturn login(User user) {
if (user.getUserName() == null || user.getPassword()== null) {
// 进行业务编写时 可以抛出自定义异常
throw new CustomException(RebStatusEnum.PARAM_NOT_EMPTY.getCode(),
RebStatusEnum.PARAM_NOT_EMPTY.getMsg());
}
// 收集用户信息(用户名和密码) 多态
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUserName(), user.getPassword());
// 获取当前用户
Subject currentUser = SecurityUtils.getSubject();
try {
// 登录
currentUser.login(usernamePasswordToken);
// 获取session
Session session = currentUser.getSession();
// 获取登录成功后的信息 SimpleAuthenticationInfo()第一个参数
User userInfo = (User) currentUser.getPrincipal();
// 设置用户信息
session.setAttribute("userInfo",userInfo);
return new MyReturn(RebStatusEnum.SUCCESS.getCode(),RebStatusEnum.SUCCESS.getMsg(),"登录成功");
} catch (AuthenticationException e) {
e.printStackTrace();
}
return new MyReturn(RebStatusEnum.FAIL.getCode(),RebStatusEnum.FAIL.getMsg(),"用户名或者密码错误");
}
用户的添加? ?添加时做加盐加密
@Override
public User insert(User user) {
// 获取原始密码
String password = user.getPassword();
// 随机盐值
String randSalt = UUID.randomUUID().toString();
// 使用随机颜值和原始密码加密
Sha512Hash sha512Hash = new Sha512Hash(password,randSalt,
ConstantUtil.CredentialsMatcher.HASH_ITERATIONS);
// 设置 存入数据库
user.setSalt(randSalt);
user.setPassword(sha512Hash.toString());
user.setStatus("0");
user.setDelFlag("0");
this.userDao.insert(user);
return user;
}
? ?测试结果
添加用户
?数据库中生成的颜值和密文密码
?3.2 使用注解方式为role添加shiro权限
????????在角色的控制层添加下面注解
??????????????@RequiresRoles("xxx")??????????判断当前登录用户是否拥有某一个角色,如果有可以访问该方法,如果没有,无权访问
???????@RequiresPermissions("角色 : 权限")????????判断当前登录用户是否拥有某一个权限,如果有可以访问该方法,如果没有,无权访问
// 判断当前登录用户是否拥有某一个角色,如果有可以访问该方法,如果没有,无权访问
@RequiresRoles("guanliyuan")
@GetMapping("queryById")
public MyReturn queryById(Integer id) {
return getSuccess(this.roleService.queryById(id));
}
// 判断当前登录用户是否拥有某一个角色,如果有可以访问该方法,如果没有,无权访问
@RequiresRoles("dailishang")
@GetMapping("queryByIdA")
public MyReturn queryByIdA(Integer id) {
return getSuccess(this.roleService.queryById(id));
}
// 判断当前登录用户是否拥有某一个权限,如果有可以访问该方法,如果没有,无权访问
@RequiresPermissions("role:queryById")
@GetMapping("queryByIdB")
public MyReturn queryByIdB(Integer id) {
return getSuccess(this.roleService.queryById(id));
}
// 判断当前登录用户是否拥有某一个权限,如果有可以访问该方法,如果没有,无权访问
@RequiresPermissions("dept:query")
@GetMapping("queryByIdC")
public MyReturn queryByIdC(Integer id) {
return getSuccess(this.roleService.queryById(id));
}
?数据表对应关系
用户
?用户角色中间表
?角色表
?菜单表
?说明caicai这个用户具有 dailishang 这个角色? 并且拥有? dept:update? 和? ?dept:query? 这两个权限
接下来对上面的代码进行测试
先使用caicai这个用户进行登录,在在地址栏中输入一下地址进行测试
http://localhost:16666/role/queryById?id=1
http://localhost:16666/role/queryByIdA?id=1
http://localhost:16666/role/queryByIdB?id=1
http://localhost:16666/role/queryByIdC?id=1
?四 shiro整合ehcache缓存
????????每次我们刷新页面,或者每次进行权限验证时,都需要进行查询该用户的所有的权限数据, 花费了大量的时间,查询相同的数据。 所以,我们需要缓存。 如果我们想查询的数据,在缓存里面,就直接从缓存里面拿 ,如果缓存中不存在想查询的数据,那么才从数据库中查询。
EhCache简单入门 - 月染霜华 - 博客园
4.1 引入jar
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.9.0</version>
</dependency>
4.2?在resource下添加配置ehcache.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
</ehcache>
如果shiro使用xml配置的则需添加下面配置,修改securityManager注入缓存
<!-- 配置ehcache 缓存 -->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"></property>
</bean>
<!--SecurityManager 安全管理器配置,真正认证,授权都是它来处理-->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!--依赖注入realm-->
<property name="realm" ref="myCustomRealm"></property>
<!--注入缓存-->
<property name="cacheManager" ref="cacheManager"></property>
</bean>
如果是配置类在上面配置的基础下添加下面代码
/**
* 实例化securityManager
* SecurityManager 安全管理器配置,真正认证,授权都是它来处理
*/
@Bean
public DefaultWebSecurityManager securityManager(){
// 实例化
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
// 依赖注入myRealm() <property name="realm" ref="myRealm"/>
defaultWebSecurityManager.setRealm(myCustomRealm());
// 注入缓存
defaultWebSecurityManager.setCacheManager(ehCacheManager());
return defaultWebSecurityManager;
}
@Bean
public MyCustomRealm myCustomRealm(){
// 实例化
MyCustomRealm myCustomRealm = new MyCustomRealm();
// 依赖注入 CredentialsMatcher() 加密加盐算法
myCustomRealm.setCredentialsMatcher(credentialsMatcher());
// 开启支持缓存,需要配置如下几个参数
myCustomRealm.setCachingEnabled(true);
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
myCustomRealm.setAuthenticationCachingEnabled(true);
return myCustomRealm;
}
/**
* 配置ehcache 缓存
* @return
*/
@Bean
public EhCacheManager ehCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return ehCacheManager;
}
加入缓存后第一次登录
?再次登录则不会加载上面内容
信息第一次查询时
?第二次查询? 打印的日志不在显示拥有的角色权限什么的? ?直接在缓存中获取
?
|