SpringCloud基础框架搭建 –2 shiro

这是系列搭建springcloud基础框架的文章,内容包括集成shiro、Mysql主从、seata、activiti、drools、hadoop大数据常用组件、keepalive+nginx https配置等;

1.将旧有框架拷贝shiro相相关代码
有部分代码需要 @Data注解的,需要 Lombok 插件,但是 plugin marketplace 无法连接,就到 https://plugins.jetbrains.com/plugin/6317-lombok/versions 下,注,需要下载对应版本的才可以;
另下载了 idea 2020.1.4 版本
2. 集成 mybatis mapper 代码,先用 git 下载 原有 gitee 源码 (原有框架)
下载地址 : https://blog.csdn.net/liuzehn/article/details/124313436
https://sourceforge.net/projects/git-osx-installer/
下载后,提示git 2.33 与 当前系统版本不兼容
改用 2.31 版,终端窗口要关闭重开;
3. 遇到 alibaba-fastjson 下载不了的情况,采用方法:
链接至:Maven 配置多个远程仓库 (都生效办法)
在 settings.xml 的 mirrors 下配置,如果第一个地址可以通,是不会再访问第二个的;
4. mybatis 版本号要写

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>

mysql 的依赖,一直载不了,是因为有其他报错影响,先把其他的完成

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.18</version>
</dependency>

5. mybatis mapper 非定义实现添加完成 ;
补充 apache commons 包

<!-- common -->
<!-- Apache Commons 集合工具类 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

<!-- Apache Commons 配置文件读取工具类 -->
<dependency>
    <groupId>commons-configuration</groupId>
    <artifactId>commons-configuration</artifactId>
    <version>1.10</version>
</dependency>

<!-- Apache Commons IO 文件/流工具类 -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

<!-- Apache Commons Lang 字符串/对象工具类 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>

<!-- Apache Commons 数学工具类 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-math3</artifactId>
    <version>3.6.1</version>
</dependency>

<!-- Apache Commons 网络工具类 -->
<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.8.0</version>
</dependency>

<!-- Apache HttpClient HTTP 客户端工具类 -->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

6. 添加相关物理表 ;
1)首先 配置docker”阿里云镜像”;
Mac 安装 docker 及使用
2)再下载 mysql image:
a. docker search mysql;
b. docker pull musql:5.7
c. 安装 mysql-server 5.7
3) 下载 dbeaver;
新建 选择 mysql 连接; -> localhost: mysql:3306
输入 连接信息 localhost: 3306 及 密码 ;-> 首次需要安装一些组件
再次点击 连接测试,显示成功,再点击确认;
4) 导入数据 ;
在 左侧 “数据库” – 下某个数据库下,右键 “导入数据” -> 弹窗口 ;
选择 左侧 “输入文件” ,弹出窗口,选择文件类型 “*”;
切换到 sql 脚本所在目录下,选择要导入的文件,可多选;
然后,一直 “下一步”, “继续” 完成;
——–
以上导入数据有问题,使用下面的方式;
右键 “数据库”,“SQL编辑器”,sql框内,右键 “文件” – “导入sql脚本”,再点击左侧“第三个” 黄色按钮 “执 行sql脚本”;
左侧 “第一个” 是执行sql语句;

7. 将 核心 common 代码,包括mapper、redis、spring、constants 相关代码调整完;
8. chen-manager 相关源码及配置:

@SpringBootApplication
@EnableFeignClients(value = "com.chen.frame.api") //不是同一个包下
@ComponentScan(basePackages = {"com.chen.frame.common", "com.chen.frame.api", "com.chen.frame.shiro"})
public class ManagerApplication {

    public static void main(String ...args) {
        //SpringApplication.run(ManagerApplication.class, args);

        new SpringApplicationBuilder(ManagerApplication.class)
                .properties(new ConcurrentHashMap<String, Object>(){{
                    put("frame.module.basePackages", "com.chen.frame.manager.mapper");
                }}).run(args);
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer placeholderConfigurer () {
        PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
        configurer.setIgnoreUnresolvablePlaceholders(true);
        return configurer;
    }

    @Bean
    @Primary
    public ObjectMapper xssObjectMapper(Jackson2ObjectMapperBuilder builder) {
        // 解析器
        ObjectMapper objectMapper = builder.createXmlMapper(false).build();
        // 注册XSS SQL 解析器
        SimpleModule xssModule = new SimpleModule("XssStringJsonSerializer");
        xssModule.addSerializer(new XssSqlStringJsonSerializer());
        objectMapper.registerModule(xssModule);
        return objectMapper;
    }

bootstrap.yml

server:
  port: 8033

spring:
  application:
    name: chen-manager
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
      config:
        server-addr: localhost:8848
        file-extension: yml
        group: DEFAULT_GROUP
        shared-configs[0]:
          data-id: application-dev.yml
          group: DEFAULT_GROUP
          refresh: true
  profiles:
    active: dev

  redis:
    host: 192.168.64.128
    port: 6378
    timeout: 3000
    max-wait: 30000
    max-active: 100
    max-idle: 20
    min-idle: 0

mybatis:
  mapperLocations: classpath:mapper/**Mapper.xml
  type-aliases-package: com.chen.frame.manager.mapper
  configuration:
    map-underscore-to-camel-case: true

运行 chen-manager 报错 :
1). 源码没错 程序包也存在 maven install提示不存在 (java(x, y): error: 程序包 xx.xx.xxx 不存在 )
解决,在被依赖包的 pom.xml 添加

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <skip>true</skip> <!-- skip 掉 -->
            </configuration>
        </plugin>
    </plugins>
</build>

2) 添加 @FeignClient 修改 @EnableFeignClient的value值后,提示 :
SpringCloud启动报错Did you forget to include spring-cloud-starter-loadbalancer;
将如下 依赖添加到全局 :

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

3) 没有使用 却RequestParam.value() was empty on parameter 0 (给方法的 @RequestParam 添加了 value注解 或 去掉@RequestParam 也一样报错)
解决修改的地方 :
a. prferences -> build,execution, deployment -> compiler -> java compiler
添加 “additional common line parameters” 值 :-parameters 没有效果;
b. 在最顶级 pom.xml 的 compiler 插件添加 configuration/parameters:true 配置;

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <encoding>UTF-8</encoding>
        <parameters>true</parameters><!-- 这个配置 -->
    </configuration>
</plugin>

4) 错误 : 数据库相关的报错,没有数据源相关的报错
参看 : SpringCloud基础框架搭建 –3 mysql+redis主从+sharding-sphere
5) 验证shrio
a. jwtFilter.java

public class JwtFilter extends BasicHttpAuthenticationFilter {
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        // 一般作为验证是能访问该页面
        // 比较先验证下是否有登录,再验证是否有权限访问;
        // subject.login(jwtToken);
        
        // 或者
        // 以下语句,判断当前是否有登录,如果有登录,再判断当前的url是否在自己的 jwtRealm 设置权限字符中;
        // 因为在权限管理系统中,权限URL一般只会有左侧那些权限Url, 具体的那些 【新增,删除。。】没有配置,需只会有权限的标志符如 user:add, user:delete
        // 因此 对于新增的url 只判断是否登录,然后在 新增 controller方法上使用 RequirePermissions value=user:add
        //return SecurityUtils.getSubject().isPermitted(getPathWithinApplication(request));
        //SecurityUtils.getSubject().getPrincipals()
        // 以下只验证是否登录
        return SecurityUtils.getSubject().isAuthenticated();
        
        // return true 
        // true 表示验证验证,就不会再走 onAccessDenied;
        // 但会再往走下一个 验证链 的 realm 对象;                                    
    }
    
    public boolean onAccessDenied(...) {
        //验证不通过时,会走到这里,一般是输出消息到前端转向到登录页; 
        // 这里只返回false, 登录由PermissionFilter验证转向;
        return false;   
    }
}

PermissionFilter.java

@Slf4j
public class PermissionFilter extends /*PermissionsAuthorizationFilter*/AccessControlFilter {
    public boolean isAccessAllowed(...) {
        // 权限验证过滤,首先经过 jwt 再走 permissionFilter
        // 因此能走到这里表示有jwt权限验证过了,这里只再验证下是否登录过 session是否过其他
        // 如果返回 false 再走 onAccessDenied 让转前端登录;
        return SecurityUtils.getSubject().isAuthenticated();
    }
}

管理后台权限设计;
1)系统管理-菜单管理,这里的URL一般只作为前端界面的菜单显示用,因为还包括按钮上的控制器方法,有很多,如果
都用SecurityUtils.getSubject().isPermitted(getPathWithinApplication(request)); 验证,那得将所有URL都加到菜单里,或配置里,不实际; 所以针对的权限,一般该菜单下具体的某个方法的权限或角色;
这些菜单如 /manage/user/add, /manage/role/add 是具体的URL;
我们需要某类URL需要身份验证/权限验证,添加过滤链 filters 配置,如下:

@Override
public Map<String, String> initGetFilterChain() {
    Map<String,String> filterChain = new LinkedHashMap<>();

    // 已登录用户
    List<String> needPerms = Arrays.asList("/manage/**", "/shiroLoged/**");
    needPerms.forEach(auth -> filterChain.put(auth, "jwt,myPerm"));

    List<String> guests = Arrays.asList("/user/**","/permission/**", "/shiro/**");
    guests.forEach(guest -> filterChain.put(guest, "anon"));

    return filterChain;
}

通常需要认证的是这样配置的:
/manag/**=authc;

没有特殊用途的话,只要用户有登录了就可以访问
如果 要想某个具体的按钮功能(初始页列表数据,新增功能等)是否能操作,有两种处理:
a. 在按钮功能对应的 contoller.功能 method 上添加 @RequirePermission,@RequireRoles 注解;
这种方式 @RestControllerAdvice 可以捕捉到;
i. 如果是权限的,本源码使用 自定义的

public class UrlPermissionResolver implements PermissionResolver {...}

以及

public class ResourcePermission implements Permission, Serializable {...}

来实现 权限 permission 的验证;
但是 requirepermissions或requireroles 这两种注解,需要首先配置 adivce 的 aop 配置;

/**
 * 添加注解支持
 */
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    // 强制使用cglib,防止重复代理和可能引起代理出错的问题
    defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
    return defaultAdvisorAutoProxyCreator;
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    return advisor;
}

猜:requireroles 通过上面的 bean 配置来拦截,首先通过 jwtFilter 的 isAccessAllowed ,验证成功后,再走角色验证的过程;
由于 jwtRealm 由自定义的 permission 实现 :

JwtRealm jwt = new JwtRealm();
jwt.setAuthenticationTokenClass(JwtToken.class);
jwt.setManagerServiceFeign(iSysUserService);
jwt.setRedisUtil(redisUtil);
jwt.setPermissionResolver(myPermissionResolver()); //自定义
realmList.add(jwt);

当是 requirepermissions 注解时,就会走自定义的 Permission 验证;
注:有配置 shiro 拦截的 如 /manage/** 只是表示这个前缀的资源需要认证(authc)
如果没有配置过 @requirePermissions或 @RequireRoles 则 或 /manage/user/**=perms[“user:add,…”] 这样的配置
则不会进入 shiro realm 的授权方法
b. 或者配置 /manage/user/**=perms[“user:add,…”]
这种方式无法被 @RestControllerAdvice 捕获 unauthorized 401 异常,如果 jwtrealm 的权限设置里没有给 permission 字符集设置:

//permSet.add("user:perm");

时,会报这样的错:

{
    "timestamp": "2023-04-28T19:48:36.703+00:00",
    "status": 401,
    "error": "Unauthorized",
    "message": "No message available",
    "path": "/shiroLoged/configPerms"
}

而不是自定义 ResultData(code,msg, data) 这种格式的错误;
=======================================================
相关controller 代码

package com.chen.frame.manager.controller;

import com.alibaba.fastjson.JSONObject;
import com.chen.frame.api.frame.bean.entity.SysUser;
import com.chen.frame.api.frame.bean.entity.Test;
import com.chen.frame.api.frame.bean.vo.SysUserVO;
import com.chen.frame.common.constants.CacheKeys;
import com.chen.frame.common.http.ResultData;
import com.chen.frame.common.http.ResultStatus;
import com.chen.frame.common.redis.RedisUtil;
import com.chen.frame.common.utils.AesUtil;
import com.chen.frame.common.utils.CommonStringUtils;
import com.chen.frame.common.utils.IpUtil;
import com.chen.frame.manager.service.SysUserService;
import com.chen.frame.manager.service.TestService;
import com.chen.frame.shiro.bean.LoginToken;
import com.chen.frame.shiro.utils.JwtUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.List;

@RestController
@RequestMapping(value = "/shiro")
public class ShiroController {

    @Autowired
    private TestService testService;

    @Autowired
    private SysUserService sysUserService;

    @Resource
    private RedisUtil redisUtil;

    @PostMapping(value = "/insertTest")
    public ResultData insertTest(@RequestBody Test test) {
        testService.insert(test);
        return new ResultData(ResultStatus.SUCCESS, "ok");
    }

    @GetMapping(value = "/queryTest")
    public ResultData queryTest() {
        List<Test> list = testService.queryTest(null);
        return new ResultData(ResultStatus.SUCCESS, "ok", list);
    }

    @PostMapping(value = "/login")
    public ResultData login(@RequestBody SysUserVO sysUser, HttpServletRequest request) {
        //JSONObject loginKey = getKey(request);
        //String encodePass = AesUtil.aesEncode(sysUser.getUserPass(), loginKey.getString("tokenKey"));

        LoginToken loginToken =
                new LoginToken(sysUser.getUserName(), sysUser.getUserPass());

        Subject subject = SecurityUtils.getSubject();

        SysUser userParams = new SysUser();
        userParams.setUserName(sysUser.getUserName());

        SysUser userData = sysUserService.getUser(userParams);

        if (userData == null) {
            return new ResultData(ResultStatus.NOT_EXISTS_USER, "用户不存在");
        }

        String token = "";
        try {
            subject.login(loginToken);
            String timeMillis = String.valueOf(System.currentTimeMillis());

            token = JwtUtils.createToken(sysUser.getUserName(), timeMillis, userData.getSecSalt());
            redisUtil.set(CacheKeys.LOGIN_REFRESH_TOKEN + sysUser.getUserName(), timeMillis, CacheKeys.REFRESH_TIME_LONG);
        }catch (AuthenticationException e) {
            return new ResultData(ResultStatus.USER_AUTH_EXCEPTION, "用户认证失败"+ e.toString());
        }catch (Exception e) {
            return new ResultData(ResultStatus.USER_LOGIN_EXCEPTION, "用户登录异常"+ e.toString(), null);
        }

        return new ResultData(ResultStatus.SUCCESS, "登录成功", token);
    }

    public JSONObject getKey(ServletRequest request) {
        //动态生成秘钥,redis存储秘钥供之后秘钥验证使用,设置有效期5秒用完即丢弃
        String tokenKey = CommonStringUtils.getRandomString(16);
        String userKey = CommonStringUtils.getRandomString(6);
        try {
            redisUtil.set("TOKEN_KEY_"+ IpUtil.getIpFromRequest(WebUtils.toHttp(request)).toUpperCase()+userKey.toUpperCase(),tokenKey,5);

            JSONObject json = new JSONObject();
            json.put("tokenKey", tokenKey);
            json.put("userKey", userKey.toUpperCase());

            return json;

        }catch (Exception e) {
            return null;
        }
    }
}

登录后的 shiro

package com.chen.frame.manager.controller;

import com.chen.frame.common.http.ResultData;
import com.chen.frame.common.http.ResultStatus;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.cloud.client.loadbalancer.RequestData;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping(value = "/shiroLoged")
public class ShiroLogedController {

    @GetMapping(value = "/welcome")
    public ResultData welcome() {
        log.info("shirologed welcome");
        return new ResultData(ResultStatus.SUCCESS, "ok");
    }

    @PostMapping(value = "shiroRole")
    @RequiresRoles(value = { "factoryHeader" })
    public ResultData shiroRole() {
        log.info("shirologed shiroRole");
        return new ResultData(ResultStatus.SUCCESS, "ok");
    }

    @PostMapping(value = "shiroPermission")
    @RequiresPermissions(value = {"sysUser"})
    public ResultData shiroPermission() {
        log.info("shirologed shiroPermission");
        return new ResultData(ResultStatus.SUCCESS, "ok");
    }

    @GetMapping(value = "/configPerms")
    public ResultData configPerms() {
        log.info("shirologed configPerms");
        return new ResultData(ResultStatus.SUCCESS, "ok");
    }

}

目前的设计是
/静态资源或登录相关页面=anon
/如 /manage/**,/shiroLoged/** = jwt,myPerms
因此在 jwtFilter 如果判断没有登录,就会发送前端Response.write登录;
1)第一步在 jwtFilter 的 isAccessAllowed 进行验证当前登录,subject.login到 jwtRealm 认证;
2)再通过 requirepermissions或requireroles进行权限验证,这两个是通过 shiroConfig 配置 advice与 AuthorizationAttributeSourceAdvisor aop拦截来进行权限验证;
3)isAccessAllowed 通过后,再走 myPerms即 permissionFiler.java 的过滤器进行权限验证;
这里在 isAccessAllowed 只验证是否登录正常的验证;
4)因此,如果是 /xxx/** = jwt,myPerms 的地址,只要有登录过,都可以访问,只是到具体功能,如果有requirepermissions或requireroles 就会走权限验证;
=====================================
参考相关 :

https://www.pianshen.com/article/1945325406/
=============================================
添加 shiro 使用 redis 做缓存支持;
1)修改 chen-common/下 redisConfig 的 RedisTemplate bean注解:

@Primary
    @Bean("redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);

        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
//      Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();
        // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        //jacksonSeial.setObjectMapper(om);

        GenericJackson2JsonRedisSerializer jacksonSeial = new GenericJackson2JsonRedisSerializer(om);

        // 值采用json序列化
        template.setValueSerializer(jacksonSeial);
        template.setHashValueSerializer(jacksonSeial);

        //使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(new StringRedisSerializer());

        template.afterPropertiesSet();

        return template;
    }

    @Bean("shiroRedisTemplate")
    public RedisTemplate<String, Object> shiroRedisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 配置连接工厂
        template.setConnectionFactory(factory);
        //使用StringRedisSerializer来序列化和反序列化redis的key值
        RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();

        template.setKeySerializer(serializer);
        // 设置hash key 和value序列化模式
        template.setHashKeySerializer(serializer);

        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();

        return template;
    }

shiroRedisTemplate 只用于实现shiro缓存,其Key也需是Jdk序列化,不然会报错;
2)再在 chen-shiro/redis/ 下添加:

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Collection;
import java.util.Set;

@Component
public class RedisCache<K, V> implements Cache<K, V> {

    //redis key
    private final K prefix = (K)"shiro_cache";

    @Resource(name = "shiroRedisTemplate")
    private RedisTemplate shiroRedisTemplate;

    public RedisTemplate getRedisTemplate() {
        return shiroRedisTemplate;
    }

    public void setRedisTemplate(RedisTemplate shiroRedisTemplate) {
        this.shiroRedisTemplate = shiroRedisTemplate;
    }

    //------------------------
    @Override
    public V get(K key) throws CacheException {
        Object o = shiroRedisTemplate.opsForHash().get(prefix, key);
        return o == null ? null : (V) o;
    }

    @Override
    public V put(K key, V value) throws CacheException {
        V old = this.get(key);
        shiroRedisTemplate.opsForHash().put(prefix,key,value);
        return old;
    }

    @Override
    public V remove(K key) throws CacheException {
        V old = this.get(key);
        shiroRedisTemplate.opsForHash().delete(prefix,key);
        return old;
    }

    @Override
    public void clear() throws CacheException {
        shiroRedisTemplate.delete(prefix);
    }

    @Override
    public int size() {
        return  shiroRedisTemplate.opsForHash().size(prefix).intValue();
    }

    @Override
    public Set<K> keys() {
        return shiroRedisTemplate.opsForHash().keys(prefix);
    }

    @Override
    public Collection<V> values() {
        return shiroRedisTemplate.opsForHash().values(prefix);
    }

}

以及:

import org.apache.shiro.cache.AbstractCacheManager;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ChenRedisManager extends AbstractCacheManager {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected Cache createCache(String name) throws CacheException {
        return redisCache;
    }
}

3)再配置 realm 开启缓存支持; shiro-chen/realm/RealmManger

@Autowired
private ChenRedisManager chenRedisManager;

JwtRealm jwt = new JwtRealm();
jwt.setAuthenticationTokenClass(JwtToken.class);
jwt.setManagerServiceFeign(iSysUserService);
jwt.setRedisUtil(redisUtil);
jwt.setPermissionResolver(myPermissionResolver());

//开启缓存,设置缓存管理器
jwt.setCachingEnabled(true);
jwt.setAuthenticationCachingEnabled(true);
jwt.setAuthorizationCachingEnabled(true);
jwt.setCacheManager(chenRedisManager);

这样如果有注解 @RequirePermissions 或 roles 时会进入realm 方法,就会缓存权限角色 集合;
获取方法:

Subject subject = SecurityUtils.getSubject();

// 检查用户是否已经认证
if (subject.isAuthenticated()) {
    DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();

    if (securityManager != null && securityManager.getRealms() != null) {
        for (Realm realm : securityManager.getRealms()) {
            if (!(realm instanceof  JwtRealm)) {
                continue;
            }

            JwtRealm jwtRealm = (JwtRealm) realm;
            Cache<?, ?> cache = jwtRealm.getAuthorizationCache();
            List<AuthorizationInfo> lists = (List<AuthorizationInfo>) cache.values();
            if (CollectionUtils.isEmpty(lists)) {
                break;
            }

            for (Object info: lists) {
                if (!(info instanceof  AuthorizationInfo)) {
                    continue;
                }
                AuthorizationInfo authorizationInfo = (AuthorizationInfo)info;
                Collection<String> collection = authorizationInfo.getStringPermissions();

                userVO.setPermissions(new HashSet<>(collection));
                break;
            }
            break;
        }
    }


}

2023-09-19 补充:
如果 shiro 加入了缓存,会先判断是否有缓存,会先取shiro 定义的权限数据缓存,没有的话,再走
@Override doGetAuthorizationInfo()
入口为 isPermitted 方法,这里会先判决是否支持缓存;
所以如果 直接编辑数据表,会获取不到最新的数据;使用redis-cli或其他工具删除 ;

cd /usr/local/bin/
./redis-cli -p 6379
auth 123456
keys *shiro_cache*
> "xxxxx"
> del "xxxxx" -- 要包括双引号

页面更新用户权限时,需要先删除更新shiro 缓存;

欢迎您的到来,感谢您的支持!

为您推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注