spring boot+shiro+jwt+redis无状态自动续签

redis工具类 https://www.jianshu.com/p/1b3f33a045bf

  1. 相关依赖

    org.apache.shiro
    shiro-spring
    1.4.0


    com.auth0
    java-jwt
    3.9.0




    org.springframework.boot
    spring-boot-starter-data-redis



    cn.hutool
    hutool-all
    5.6.3


    com.alibaba
    fastjson
    1.2.76

  1. yml配置jwt
jwt:
  authoritiesKey: auth
  # 密匙KEY
  secret: KeXu6IgYf7xaPe4jpw
  # HeaderKEY
  tokenHeader: Token
  # jwt过期时间 单位秒 1天后过期=86400 7天后过期=604800
  expiration: 1800
  # 放入redis过期时间 单位秒 1天后过期=86400 7天后过期=604800
  redisExpiration: 1800

获取yml里的jwt配置信息

import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * JWT配置类
 */
@Getter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JWTConfig {

    public static String authoritiesKey;

    /**
     * 密钥KEY
     */
    public static String secret;
    /**
     * TokenKey
     */
    public static String tokenHeader;
    /**
     * 过期时间
     */
    public static Integer expiration;
    /**
     * redis过期时间
     */
    public static Integer redisExpiration;

    public void setAuthoritiesKey(String authoritiesKey) {
        this.authoritiesKey = authoritiesKey;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public void setTokenHeader(String tokenHeader) {
        this.tokenHeader = tokenHeader;
    }

    public void setExpiration(Integer expiration) {
        this.expiration = expiration;
    }

    public void setRedisExpiration(Integer redisExpiration) {
        this.redisExpiration = redisExpiration;
    }
    
}
  1. JwtTokenUtil用于解析和生成token
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.mp.generator.config.jwt.JWTConfig;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;


public class JwtTokenUtil implements Serializable {

    private static final long serialVersionUID = -5625635588908941275L;

    public static String generateToken(String username, Long current) {
        if (null == current) {
            current = System.currentTimeMillis();
        }
        Algorithm algorithm = Algorithm.HMAC256(JWTConfig.secret);
        return JWT.create()
                .withClaim(JWTConfig.authoritiesKey, username)
                .withClaim("current", current)
                .withExpiresAt(new Date(current + JWTConfig.expiration * 1000))
                .sign(algorithm);
    }

    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(JWTConfig.authoritiesKey).asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    public static String getToken() {
        String token;
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        token = request.getHeader(JWTConfig.tokenHeader);
        if (StringUtils.isBlank(token)) {
            return "";
        }
        return token;
    }

    public static boolean isExpired(String token) {
        DecodedJWT jwt = JWT.decode(token);
        Date expiration = jwt.getExpiresAt();
        return expiration.before(new Date());
    }

    public static boolean verify(String token) {
        try {
            //解密
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(JWTConfig.secret)).build();
            verifier.verify(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public static Long getCurrent(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("current").asLong();
        }catch (Exception e){
            return null;
        }
    }
}
  1. UserToken,TokenFilter过滤器中使用,传递token信息
import org.apache.shiro.authc.AuthenticationToken;

import java.io.Serializable;


public class UserToken implements AuthenticationToken, Serializable {

    private static final long serialVersionUID = 1841491628743017587L;

    private final String token;


    public UserToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}
  1. 实现过滤器TokenFilter,判断用户是否登录
import com.mp.generator.config.jwt.JWTConfig;
import com.mp.generator.constants.RedisConstant;
import com.mp.generator.utils.JwtTokenUtil;
import com.mp.generator.utils.RedisUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

public class TokenFilter extends BasicHttpAuthenticationFilter {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    /**
     * 如果带有 com.token,则对 com.token 进行检查,否则直接通过
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)) {
            //如果存在,则进入 executeLogin 方法执行登入,检查 com.token 是否正确
            try {
                return executeLogin(request, response);
            } catch (Exception e) {
                log.error("shiro验证异常 {}", Arrays.asList(e.getStackTrace()));
                responseError(response, "shiro fail");
                return false;
            }
        }
        //1.如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 com.token,直接返回 true
        //2.如果请求头不存在 Token,并且返回false,说明走我们的过滤器时必须带token
        return true;
    }

    /**
     * 判断用户是否想要登入。
     * 检测 header 里面是否包含 JWTConfig.tokenHeader 字段
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(JWTConfig.tokenHeader);
        return StringUtils.isNotBlank(token);
    }

    /**
     * 执行登陆操作
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(JWTConfig.tokenHeader);
        UserToken userToken = new UserToken(token);
        try {
            Subject subject = this.getSubject(request, response);
            subject.login(userToken);
            return this.onLoginSuccess(userToken, subject, request, response);
        } catch (AuthenticationException var5) {
            return this.onLoginFailure(userToken, var5, request, response);
        }
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * token错误
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        try {
            String token = ((HttpServletRequest) request).getHeader(JWTConfig.tokenHeader);
            log.info("--------token过期:{}--------", token);
        } catch (Exception e) {
            log.info("--------token不存在--------");
        }
        this.sendChallenge(request, response);
        this.responseError(response, "token已过期或不存在!");
        return Boolean.FALSE;
    }

    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
        log.info("--------onLoginSuccess--------");
        String tokenPrincipal = (String) token.getPrincipal();
        if (StringUtils.isNotBlank(tokenPrincipal)) {
            if (JwtTokenUtil.verify(tokenPrincipal)) {
                return true;
            } else {
                return refreshToken(request, response);
            }
        }
        return true;
    }

    /**
     * 将非法请求跳转到 /unauthorized/**
     */
    private void responseError(ServletResponse response, String message) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        response.reset();
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        String jsonStr = "{\"result\":\"FAILURE\",\"code\":401,\"message\":\"" + message + "\"}";
        try (PrintWriter out = response.getWriter()) {
            out.append(jsonStr);
        } catch (IOException e) {
            log.error("sendChallenge error,can not resolve httpServletResponse");
        }

    }

    public  T getBean(Class clazz, HttpServletRequest request) {
        WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
        return applicationContext.getBean(clazz);
    }

    // 刷新token
    private boolean refreshToken(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        RedisUtil redisUtil = getBean(RedisUtil.class, req);
        // 获取传递过来的accessToken
        String accessToken = req.getHeader(JWTConfig.tokenHeader);
        // 获取token里面的用户名
        String username = JwtTokenUtil.getUsername(accessToken);
        // 判断refreshToken是否过期了,过期了那么所含的username的键不存在
        if (redisUtil.hasKey(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + username)) {
            // 判断refresh的时间节点和传递过来的accessToken的时间节点是否一致,不一致校验失败
            Long current = (Long) redisUtil.getCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + username);
            if (Objects.equals(JwtTokenUtil.getCurrent(accessToken), current)) {
                // 获取当前时间节点
                long currentTimeMillis = System.currentTimeMillis();
                // 生成刷新的token
                String token = JwtTokenUtil.generateToken(username, currentTimeMillis);
                // 刷新redis里面的refreshToken,过期时间是(JWTConfig.expiration + 30*60)min
                redisUtil.setCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + username, currentTimeMillis, JWTConfig.expiration + JWTConfig.redisExpiration, TimeUnit.SECONDS);
                // 最后将刷新的AccessToken存放在Response的Header中的JWTConfig.tokenHeader字段返回
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.setHeader(JWTConfig.tokenHeader, token);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", JWTConfig.tokenHeader);
                return true;
            }
        }
        return false;
    }
}
  1. 新建ShiroConfig,添加自己的过滤规则
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
@Slf4j
public class ShiroConfig {
    /**
     * 先走 com.**.TokenFilter ,然后 com.**.TokenFilter 如果检测到请求头存在token,则用token 去 login,走 Realm 去验证
     */
    @Bean
    public ShiroFilterFactoryBean factory(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为token
        Map filterMap = new HashMap<>();
        //设置我们自定义的Token过滤器
        filterMap.put("token", new TokenFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        // 设置无权限时跳转的 url;
        factoryBean.setUnauthorizedUrl("/unauthorized/无权限");
        Map filterRuleMap = new LinkedHashMap<>();

        filterRuleMap.put("/unauthorized/**", "anon");
        filterRuleMap.put("/api/login/**", "anon");
        filterRuleMap.put("/**", "token");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置自定义 realm.
        securityManager.setRealm(userRealm);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    /**
     * 添加注解支持
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }
}
  1. UserRealm,对登录用户和用户的权限进行验证
import cn.hutool.core.util.ObjectUtil;
import com.mp.generator.entity.SysMenu;
import com.mp.generator.entity.SysRole;
import com.mp.generator.entity.UserInfo;
import com.mp.generator.service.UserInfoService;
import com.mp.generator.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.stream.Collectors;

@Slf4j
@Component
public class UserRealm extends AuthorizingRealm {

    @Lazy
    @Autowired
    private UserInfoService userInfoService;

    /**
     * 必须重写此方法,不然会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UserToken;
    }

    /**
     * 授权
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.info("授权验证->doGetAuthenticationInfo()");
        String token = principals.toString();
        String username = JwtTokenUtil.getUsername(token);
        UserInfo userInfo = userInfoService.getUserByUsername(username);
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //查询数据库来获取用户的角色
        info.addRoles(userInfo.getRoleList().stream().map(SysRole::getRoleName).collect(Collectors.toSet()));
        //查询数据库来获取用户的权限
        info.addStringPermissions(userInfo.getMenuList().stream().map(SysMenu::getPermission).collect(Collectors.toSet()));
        return info;
    }

    /**
     * 身份验证
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     *
     * @param authenticationToken token
     * @return SimpleAuthenticationInfo
     * @throws AuthenticationException 登录异常
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        log.info("登录验证->doGetAuthenticationInfo()");
        String token = (String) authenticationToken.getCredentials();
        log.info("当前token : {}", token);
        String username = JwtTokenUtil.getUsername(token);
        if (ObjectUtil.isNull(username)) {
            throw new AuthenticationException("登录过期,请重新登录!");
        }
        UserInfo userInfo = userInfoService.getUserByUsername(username);
        if (null == userInfo) {
            throw new AuthenticationException("该用户不存在");
        }
        log.info("当前用户 : {}", userInfo);
        return new SimpleAuthenticationInfo(token, token, "UserRealm");
    }
}

  1. 登录实现
import com.mp.generator.config.jwt.JWTConfig;
import com.mp.generator.constants.RedisConstant;
import com.mp.generator.dto.request.LoginRequest;
import com.mp.generator.entity.UserInfo;
import com.mp.generator.service.LoginService;
import com.mp.generator.service.UserInfoService;
import com.mp.generator.utils.JwtTokenUtil;
import com.mp.generator.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private UserInfoService userInfoService;
    @Autowired
    private RedisUtil redisUtil;

    @Override
    public String login(LoginRequest request) {
        if (StringUtils.isBlank(request.getPassword())) {
            throw new RuntimeException("请输入密码");
        }
        UserInfo userInfo = userInfoService.getUserInfo(request.getUserName());
        if (null == userInfo) {
            throw new RuntimeException("用户名不存在");
        }
        String password = new SimpleHash("MD5", request.getPassword(), userInfo.getSalt(), 1024).toHex();
        if (!password.equals(userInfo.getPassword())) {
            throw new RuntimeException("密码不正确");
        }
        long currentTimeMillis = System.currentTimeMillis();
        String token = JwtTokenUtil.generateToken(userInfo.getUserName(), currentTimeMillis);
        redisUtil.setCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_ENTER + userInfo.getUserName(), currentTimeMillis, JWTConfig.expiration + JWTConfig.redisExpiration, TimeUnit.SECONDS);
        redisUtil.deleteObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + userInfo.getUserName());
        userInfoService.getUserByUsername(userInfo.getUserName());
        return token;
    }
}

UserInfoServiceImpl

import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mp.generator.config.jwt.JWTConfig;
import com.mp.generator.entity.SysMenu;
import com.mp.generator.entity.SysRole;
import com.mp.generator.entity.UserInfo;
import com.mp.generator.constants.RedisConstant;
import com.mp.generator.mapper.UserInfoMapper;
import com.mp.generator.service.UserInfoService;
import com.mp.generator.utils.JwtTokenUtil;
import com.mp.generator.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Slf4j
@Service
public class UserInfoServiceImpl extends ServiceImpl implements UserInfoService {

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public UserInfo getUserInfo(String username) {
        return baseMapper.selectOne(Wrappers.lambdaQuery().eq(UserInfo::getUserName, username).last("LIMIT 1"));
    }

    @Override
    public UserInfo getUserByUsername(String username) {
        UserInfo userInfo = (UserInfo) redisUtil.getCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + username);
        if (null == userInfo) {
            log.info("redis缓存用户已过期");
            userInfo = this.getUserInfo(username);
            if (null == userInfo) {
                throw new AuthenticationException("用户不存在");
            }
            List roleList = this.selectSysRoleByUserId(userInfo.getId());
            List menuList = this.selectSysMenuByUserId(userInfo.getId());
            menuList.add(new SysMenu().setName("添加用户").setPermission("user:add"));
            menuList.add(new SysMenu().setName("用户分页").setPermission("user:page"));
            userInfo.setRoleList(roleList);
            userInfo.setMenuList(menuList);
            redisUtil.setCacheObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + userInfo.getUserName(), userInfo, JWTConfig.expiration + JWTConfig.redisExpiration, TimeUnit.SECONDS);
        }
        return userInfo;
    }

    @Override
    public UserInfo getCurrentUserInfo() {
        return this.getCurrentUserInfo(Boolean.FALSE);
    }

    @Override
    public UserInfo getCurrentUserInfo(Boolean forceRefresh) {
        String username = JwtTokenUtil.getUsername(JwtTokenUtil.getToken());
        if (Boolean.TRUE.equals(forceRefresh)) {
            redisUtil.deleteObject(RedisConstant.CACHE_PREFIX + RedisConstant.CACHE_USER_INFO + username);
        }
        return this.getUserByUsername(username);
    }

    @Override
    public List selectSysRoleByUserId(Long userId) {
        // 具体根据业务实现
        return new ArrayList<>();
    }

    @Override
    public List selectSysMenuByUserId(Long userId) {
        // 具体根据业务实现
        return new ArrayList<>();
    }
}

你可能感兴趣的:(spring boot+shiro+jwt+redis无状态自动续签)