SpringBoot+Shiro+Jedis+JWT+基于url的权限拦截系统

在众多的开发任务里,权限管理系统开发是常见的也是大部分程序员并着手开发过的系统。在最近的任务,上级要求开发一个通用的基于url的权限控制系统,由于笔者对shiro早有接触,虽然springsecurity的功能强大,与spring易整合但结构复杂组件较多,为了在有限的开发周期内减少学习成本,最后确定技术选型:springboot+shiro+redis+jwt+mybatis+mysql

设计思路

SpringBoot+Shiro+Jedis+JWT+基于url的权限拦截系统SpringBoot+Shiro+Jedis+JWT+基于url的权限拦截系统

  • shiro配置放行uri,比如登录登出等游客身份也能调用的接口等资源,登陆成功的用户服务器会返回特有的jwt(作为令牌)给前端交由前台保存,可保存到localStorage或sessionStorage,之后该用户的请求的header都需带上jwt
  • 因为是基于url的拦截,新建拦截器继承BasicHttpAuthenticationFilter,重写preHadle(),isAccessAllowed().onAccessDenied()方法,主要功能:捕获请求,获取请求头的token进行身份验证,获取请求uri进行权限认证,认证通过才能调用controller接口
  • 自定义realm类重写doGetAuthenticationInfo(),doGetAuthorizationInfo()方法,自定义身份认证及权限认证方式,给上面的拦截器所调用
  • 生成的jwt会保存到redis交由redis维护,以用户名为key,过期时间30分钟,每次请求经过拦截器时候会刷新过期时间,若reidis获取不到jwt或jwt与请求头的jwt不一致则认证失败返回登陆页重新登陆

上菜

导包(展示部分)

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.0.7.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-support -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-support</artifactId>
            <version>2.3.3</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.0.7.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->

        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.55</version>
        </dependency>
        
        <!-- JWT:Json Web Token 配置 用于请求身份验证 -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>${jjwt.version}</version>
        </dependency>

        <!-- Shiro -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>

        <!-- Redis-Jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>${jedis.version}</version>
        </dependency>

    </dependencies>

配置Jedis

package com.rq.authority.config.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Jedis配置,项目启动注入JedisPool
 */
@Configuration
@EnableAutoConfiguration
public class JedisConfig {

    /**
     * LOGGER
     */
    private static final Logger logger = LoggerFactory.getLogger(JedisConfig.class);

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.timeout}")
    private int timeout;

    @Value("200")
    private int maxActive;

    @Value("-1")
    private int maxWait;

    @Value("8")
    private int maxIdle;

    @Value("0")
    private int minIdle;

    @Bean
    public JedisPool redisPoolFactory(){
        try {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxIdle(maxIdle);
            jedisPoolConfig.setMaxWaitMillis(maxWait);
            jedisPoolConfig.setMaxTotal(maxActive);
            jedisPoolConfig.setMinIdle(minIdle);
            JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
            logger.info("初始化Redis连接池JedisPool成功!地址: " + host + ":" + port);
            return jedisPool;
        } catch (Exception e) {
            logger.error("初始化Redis连接池JedisPool异常:" + e.getMessage());
        }
        return null;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getTimeout() {
        return timeout;
    }

    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public int getMaxActive() {
        return maxActive;
    }

    public void setMaxActive(int maxActive) {
        this.maxActive = maxActive;
    }

    public int getMaxWait() {
        return maxWait;
    }

    public void setMaxWait(int maxWait) {
        this.maxWait = maxWait;
    }

    public int getMaxIdle() {
        return maxIdle;
    }

    public void setMaxIdle(int maxIdle) {
        this.maxIdle = maxIdle;
    }

    public int getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(int minIdle) {
        this.minIdle = minIdle;
    }
}

shiro 配置

package com.rq.authority.config.shiro;


import com.rq.authority.config.shiro.cache.CustomCacheManager;
import com.rq.authority.config.shiro.realm.StatelessRealm;
import com.rq.authority.filter.StatelessAuthcFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.DefaultSessionManager;
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.apache.shiro.web.mgt.DefaultWebSubjectFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {

    private final static Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        shiroFilterFactoryBean.setSecurityManager(securityManager);

        logger.info("设置拦截器");
        Map<String, Filter> filters = new LinkedHashMap<String, Filter>();

        //无状态过滤
        filters.put("statelessAuthc", statelessAuthc());
        shiroFilterFactoryBean.setFilters(filters);

        logger.info("配置访问权限,拦截策略以键值对存入map");
        Map<String, String> filterChainDefinitions = new LinkedHashMap<>();//必须LinkedHashMap
        filterChainDefinitions.put("/**",  "statelessAuthc");//拦截自定义设置


        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitions);

        return shiroFilterFactoryBean;
    }

    @Bean(name = "securityManager")
    public SecurityManager securityManager(){
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();

        //设置不创建session
        securityManager.setSubjectFactory(subjectFactory());

        logger.info("设置sessionManager禁用掉会话调度器");
        securityManager.setSessionManager(sessionManager());

        logger.info("注入缓存管理器");
        securityManager.setCacheManager(new CustomCacheManager());//这个如果执行多次,也是同样的一个对象;

        //无状态需要设置不创建session,禁用使用Sessions 作为存储策略的实现,但它没有完全地禁用Sessions,所以需要配合context.setSessionCreationEnabled(false);
        ((DefaultSessionStorageEvaluator)((DefaultSubjectDAO)securityManager.getSubjectDAO()).getSessionStorageEvaluator()).setSessionStorageEnabled(false);

        logger.info("配置realm");
        securityManager.setRealm(statelessRealm());

        return securityManager;
    }

    /**
     * subject工厂管理器.
     * 用于禁用session
     * @return
     */
    @Bean
    public DefaultWebSubjectFactory subjectFactory(){
        StatelessDefaultSubjectFactory subjectFactory = new StatelessDefaultSubjectFactory();
        return subjectFactory;
    }

    /**
     * session管理器
     * sessionManager通过sessionValidationSchedulerEnabled禁用掉会话调度器,
     * 因为我们禁用掉了会话,所以没必要再定期过期会话了。
     * @return
     */
    @Bean
    public DefaultSessionManager sessionManager(){
        DefaultSessionManager sessionManager = new DefaultSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(false);
        return sessionManager;
    }

    /**
     * 自定义realm实现
     * @return
     */
    @Bean
    public StatelessRealm statelessRealm(){
        StatelessRealm statelessRealm = new StatelessRealm();
        statelessRealm.setCachingEnabled(false);
        return statelessRealm;
    }

    /**
     * 无状态权限验证
     * @return
     */
    @Bean
    public StatelessAuthcFilter statelessAuthc(){
        StatelessAuthcFilter statelessAuthc = new StatelessAuthcFilter();
        return statelessAuthc;
    }


    /**
     *  开启shiro aop注解支持.
     *  使用代理方式;所以需要开启代码支持;
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        LifecycleBeanPostProcessor lifecycleBeanPostProcessor = new LifecycleBeanPostProcessor();
        return lifecycleBeanPostProcessor;
    }

    /**
     * 必须加否则shiro注解无法使用
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }
}

因为不需要session,设置session不创建(自定义工厂类继承DefaultWebSubjectFactory)

package com.rq.authority.config.shiro;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.SubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSubjectFactory;

public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {

    @Override
    public Subject createSubject(SubjectContext context) {
        //不创建session
        context.setSessionCreationEnabled(false);
        return super.createSubject(context);
    }
}

shiro缓存方面的配置

package com.rq.authority.config.shiro.cache;

import com.rq.authority.common.constant.RedisConstant;
import com.rq.authority.util.JedisUtil;
import com.rq.authority.util.SerializableUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;

import java.util.*;

/**
 * 重写Shiro的Cache保存读取
 */
public class CustomCache<K,V> implements Cache<K,V> {

    /**
     * 缓存的key名称获取为shiro:cache:principalIdFieldName
     * @param key
     * @return java.lang.String
     */
    private String getKey(Object key){
        return RedisConstant.PREFIX_SHIRO_CACHE + key.toString();
    }

    /**
     * 获取缓存
     */
    @Override
    public Object get(Object key) throws CacheException {
        if(!JedisUtil.exists(this.getKey(key))){
            return null;
        }
        return JedisUtil.getObject(this.getKey(key));
    }

    /**
     * 保存缓存
     */
    @Override
    public Object put(Object key, Object value) throws CacheException {
        // 设置Redis的Shiro缓存
        return JedisUtil.setObject(this.getKey(key), value, RedisConstant.SHIRO_CACHE_EXPIRETIME);
    }

    /**
     * 移除缓存
     */
    @Override
    public Object remove(Object key) throws CacheException {
        if(!JedisUtil.exists(this.getKey(key))){
            return null;
        }
        JedisUtil.existAndDelKey(this.getKey(key));
        return null;
    }

    /**
     * 清空所有缓存
     */
    @Override
    public void clear() throws CacheException {
        JedisUtil.getJedis().flushDB();
    }

    /**
     * 缓存的个数
     */
    @Override
    public int size() {
        Long size = JedisUtil.getJedis().dbSize();
        return size.intValue();
    }

    /**
     * 获取所有的key
     */
    @Override
    public Set keys() {
        Set<byte[]> keys = JedisUtil.getJedis().keys(new String("*").getBytes());
        Set<Object> set = new HashSet<Object>();
        for (byte[] bs : keys) {
            set.add(SerializableUtil.unserializable(bs));
        }
        return set;
    }

    /**
     * 获取所有的value
     */
    @Override
    public Collection values() {
        Set keys = this.keys();
        List<Object> values = new ArrayList<Object>();
        for (Object key : keys) {
            values.add(JedisUtil.getObject(this.getKey(key)));
        }
        return values;
    }
}

缓存管理器实例化自定义缓存类

package com.rq.authority.config.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;

/**
 * 重写Shiro缓存管理器
 */
public class CustomCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new CustomCache<K,V>();
    }
}

——压轴招牌菜——
噔噔~~~自定义拦截器StatelessAuthcFilter,基本整个后台系统的权限控制交由该类处理,具体怎么实现呢,挺简单的,好吃的一口塞,好喝的一口闷

package com.rq.authority.filter;

import com.rq.authority.common.handler.UnFindTokenException;
import com.rq.authority.config.shiro.bo.JwtToken;
import com.rq.authority.util.JwtTokenUtil;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.UnauthorizedException;
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.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.naming.AuthenticationException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class StatelessAuthcFilter extends BasicHttpAuthenticationFilter {

    private static final Logger logger = LoggerFactory.getLogger(StatelessAuthcFilter.class);

    //需要过滤拦截的请求地址,多个地址用","分割
    @Value("${system.config.filterExclude}")
    private String excludeUrl;

    /**
     * 默认需要放行的请求地址
     */
    private String[] defalutExcludeUrl = new String[] {
            "/websocket","/logout","/login","/authenticate","/formLogin",".jpg",".png",".gif",".css",".js",".jpeg"
    };

    /**
     * CROS复杂请求时会先发送一个OPTIONS请求,来测试服务器是否支持本次请求,这个请求时不带数据的,请求成功后才会发送真实的请求。
     * 所以第一次发送请求要确认服务器支不支持接收这个header。第二次才会传数据,所以我们要做的就是把所有的OPTIONS请求统统放行
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpRequest = (HttpServletRequest)request;
        HttpServletResponse httpResponse = (HttpServletResponse)response;
        if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpResponse.setHeader("Access-control-Allow-Origin", "*");
            httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
            httpResponse.setHeader("Access-Control-Allow-Headers", httpRequest.getHeader("Access-Control-Request-Headers"));
            httpResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 先执行:isAccessAllowed 再执行onAccessDenied
     *
     * isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,
     * 如果允许访问返回true,否则false;
     *
     * 如果返回true的话,就直接返回交给下一个filter进行处理。
     * 如果返回false的话,回往下执行onAccessDenied
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue){
        logger.info("处理需过滤的请求");
        String u = ((HttpServletRequest)request).getRequestURI();
        for (String e : defalutExcludeUrl) {
            if (u.endsWith(e)) {
                return true;
            }
        }

        if(StringUtils.isNotBlank(excludeUrl)){
            String[] customExcludes = excludeUrl.split(",");
            for (String e : customExcludes) {
                if(e.contains("*")){
                    e = e.replace("*","");
                    if(u.startsWith(e)){
                        return true;
                    }
                }
                if (u.endsWith(e)) {
                    return true;
                }
            }
        }

        return false;

    }

    /**
     * onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;
     * 如果返回false表示该拦截器实例已经处理了,将直接返回即可。
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {

        logger.info("获取token进行登陆验证");
        String accessToken = this.getAuthzHeader(request);

        if (StringUtils.isBlank(accessToken)) {
            handleError(request,response, new UnFindTokenException(AUTHORIZATION_HEADER));
            return false;
        } else {
            logger.info("验证accessToken");
            try {
                JwtToken token = JwtTokenUtil.validateAndGetToken(accessToken);
                if (token != null) {
                    logger.info("委托realm类身份认证");
                    this.getSubject(request, response).login(token);
                }
            } catch (Exception e) {
                Throwable cause = e.getCause();
                logger.error("拦截器onAcessDenied()验证accessToken失败:{}",e);
                handleError(request,response,new AuthenticationException(e.getMessage()));
                return false;
            }
        }

        String u = ((HttpServletRequest)request).getRequestURI();
        logger.info("请求api:"+u);
        Subject subject = SecurityUtils.getSubject();
        logger.info("委托realm类授权认证");
        if (subject.isPermitted(u)) {
            return true;
        }else {
            handleError(request,response,new UnauthorizedException(u));
            return false;
        }

    }


    private void handleError(ServletRequest request, ServletResponse response, Exception e) throws Exception {

        request.setAttribute("exception",e);

        request.getRequestDispatcher("/exception/handle").forward(request,response);


    }

}

上述未放行的uri会交由onAccessDenied()方法处理,主要业务逻辑:先获取请求头header里的authorization字段的token即jwt,验证后调用realm的doGetAuthenticationInfo()方法认证用户,再获取请求uri调用doGetAuthorizationInfo()方法验证是否合法请求,若是则通过直接调用controller类的方法,不是则返回异常信息,异常信息统一转发到异常处理controller类由全局异常处理切面类捕获处理。
——辅菜——
自定义realm类

package com.rq.authority.config.shiro.realm;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.rq.authority.common.constant.RedisConstant;
import com.rq.authority.config.shiro.bo.JwtToken;
import com.rq.authority.entity.Resource;
import com.rq.authority.entity.User;
import com.rq.authority.service.ResourceService;
import com.rq.authority.service.UserService;
import com.rq.authority.util.JedisUtil;
import org.apache.commons.lang.StringUtils;
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 java.util.List;

public class StatelessRealm extends AuthorizingRealm {

    @Autowired
    @Lazy
    private ResourceService resourceService;

    @Autowired
    @Lazy
    private UserService userService;

    @Override
    public boolean supports(AuthenticationToken token) {
        /**
         * 仅支持jwtToken 类型的Token,
         * 那么如果在StatelessAuthcFilter类中返回的是UsernamePasswordToken,那么将会报如下错误信息:
         * Please ensure that the appropriate Realm implementation is configured correctly or
         * that the realm accepts AuthenticationTokens of this type.StatelessAuthcFilter.isAccessAllowed()
         */
        return token instanceof JwtToken;
    }

    /**
     * 认证方法
     * @param auth
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {

        String account = (String) auth.getPrincipal();
        String accessToken = (String) auth.getCredentials();
        // 帐号为空
        if (StringUtils.isBlank(account)) {
            throw new AuthenticationException("Token中帐号为空(The account in Token is empty.)");
        }
        // 查询用户是否存在
        User user = userService.selectOne(new EntityWrapper<User>().eq("username",account));
        if (user == null) {
            throw new AuthenticationException("该帐号不存在(The account does not exist.):" + account);
        }
        //校验accessToken
        String refreshToken =(String) JedisUtil.getObject(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account);
        if (StringUtils.isNotBlank(refreshToken) && accessToken.equals(refreshToken)) {
            //刷新refreshToken
            JedisUtil.expire(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,RedisConstant.REFRESH_TOKEN_EXPIRETIME);
            return new SimpleAuthenticationInfo(account, accessToken, getName());
        }

        throw new AuthenticationException("身份认证失败,请重新登陆!");

    }

    /**
     * 授权,只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        String username = (String) principals.getPrimaryPrincipal();
        String userId = userService.selectOne(new EntityWrapper<User>().eq("username",username)).getId();
        // 根据用户角色与所属组织获取资源交集
        List<Resource> resources = resourceService.selectResourcesByUserId(userId);
        if (!resources.isEmpty()) {
            for (Resource resource : resources) {
                simpleAuthorizationInfo.addStringPermission(resource.getUrl());
            }

        }
        return simpleAuthorizationInfo;
    }
}

异常处理类(controller)

package com.rq.authority.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/exception")
public class ExceptionController {

    @RequestMapping("/handle")
    public void exceptionHandler(HttpServletRequest request, HttpServletResponse response) throws Throwable {
        
        Exception e = (Exception) request.getAttribute("exception");

        throw e;

    }
}

全局捕获异常切面类

package com.rq.authority.common.handler;

import com.rq.authority.common.enums.ResultCode;
import com.rq.authority.common.vo.R;
import com.rq.authority.common.vo.ResponseBuilder;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.naming.AuthenticationException;
import javax.servlet.http.HttpServletRequest;

/**
 *  title: 全局异常处理切面
 *  Description: 利用 @ControllerAdvice + @ExceptionHandler组合处理Controller层RuntimeException异常
 */
@RestControllerAdvice
public class ExceptionAspect {

    private static final Logger logger = LoggerFactory.getLogger(ExceptionAspect.class);

    /**
     * 400 - Bad Request
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public R<String> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        logger.error("JSON 格式错误:{}", e);
        return ResponseBuilder.error(ResultCode.PARAM_ERROR,"JSON 格式错误");
    }

    /**
     * 400 - Bad Request
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public R<String> handleValidationException(MethodArgumentNotValidException e) {
        logger.error("参数异常:{}", e);
        return ResponseBuilder.error(ResultCode.PARAM_ERROR,"参数异常");
    }

    /**
     * 401 - 找不到JWT异常
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnFindTokenException.class)
    public R<String> handleUnfindTokenException(UnFindTokenException e) {
        logger.error("找不到token异常:{}",e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"找不到token:" + e.getMessage());
    }

    /**
     * 401 - 验签异常
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(SignatureException.class)
    public R<String> handleSignatureException(SignatureException e) {
        logger.error("token验签失败:{}", e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"token验签失败:" + e.getMessage());
    }

    /**
     * 401 - token过期异常
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ExpiredJwtException.class)
    public R<String> handleExpiredJwtException(ExpiredJwtException e) {
        logger.error("token过期异常:{}",e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"token过期:" + e.getMessage());
    }

    /**
     * 401 - token解析异常
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler({UnsupportedJwtException.class, MalformedJwtException.class})
    public R<String> handleUnsupportedJwtException(UnsupportedJwtException e) {
        logger.error("token解析异常:{}",e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"token格式不正确,解析失败:" + e.getMessage());
    }

    /**
     * 403 - shiro 认证异常
     * @param
     * @return
     */
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(UnauthenticatedException.class)
    public R<String> handleUnauthenticatedException(UnauthorizedException e) {
        logger.error("未经过身份认证:{}", e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"未经身份认证,无权访问:" + e.getMessage());
    }

    /**
     * 403 - shiro 授权异常
     * @param
     * @return
     */
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(UnauthorizedException.class)
    public R<String> handleUnauthorizedException(UnauthorizedException e) {
        logger.error("不在权限允许范围内:{}", e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"超出权限范围,无权访问:"+e.getMessage());
    }

    /**
     * 415 - Unsupported Media Type。HttpMediaTypeNotSupportedException
     * 是ServletException的子类,需要Servlet API支持
     */
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public R<String> handleHttpMediaTypeNotSupportedException(Exception e) {
        logger.error("不支持的媒体类型:{}", e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"不支持的媒体类型:"+e.getMessage());
    }

    /**
     * 500 - shiro身份认证异常
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(AuthenticationException.class)
    public R<String> handleAuthenticationException(AuthenticationException e) {
        logger.error("Shiro身份认证异常:{}", e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"身份认证失败");
    }

    /**
     * 500 - shiro授权处理异常
     */
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(AuthorizationException.class)
    public R<String> handleAuthorizationException(AuthorizationException e) {
        logger.error("Shiro授权处理异常:{}", e);
        return ResponseBuilder.error(ResultCode.EXCEPTION,"授权处理失败");
    }

    @ExceptionHandler(value = BusinessException.class)
    public R handleBusinessException(BusinessException e, HttpServletRequest request) {
        logger.error("请求[{}]业务异常", request.getRequestURI(), e);
        return R
                .builder()
                .code(e.getExceptionCode())
                .message(e.getMessage())
                .build();
    }

    @ExceptionHandler
    public R handleSystemException(Exception e, HttpServletRequest request) {
        logger.error("请求[{}]系统异常", request.getRequestURI(), e);
        return ResponseBuilder.exception();
    }

}

没吃饱?详情源码下载:https://download.****.net/download/qq_41981107/10937254