SpringBoot+Shiro+JWT前后端分离实现用户权限和接口权限控制

1. 引入需要的依赖

我使用的是原生jwt的依赖包,在maven仓库中有好多衍生的jwt依赖包,可自己在maven仓库中选择,实现大同小异。

    <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>${shiro.version}</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>${shiro-redis.version}</version>
            <exclusions>
                <exclusion>
                    <artifactId>shiro-core</artifactId>
                    <groupId>org.apache.shiro</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--JWT依赖-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>${jwt.version}</version>
        </dependency>

2. 配置shiro信息

2.1. 配置文件增加属性值配置

# shiro 配置
shiro:
  filter-chain-map:
    # 用户登录
    '[/login/**]': origin
    # 获取api token
    '[/api/token/**]': anon
    # api接口权限配置
    '[/api/**]': api
    # 用户权限控制
    '[/**]': origin, jwt
  # 设置权限缓存时间
  cache-timeout: 60

2.2. shiro 配置类

package com.example.shiro.configuration;

import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SessionStorageEvaluator;
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.DefaultWebSessionStorageEvaluator;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;

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

/**
 * shiro配置文件
 *
 * @author xsshu
 * @date 2020-07-18 16:22
 */
@Configuration
@AutoConfigureAfter(ShiroProperties.class)
public class ShiroConfig {

    @Autowired
    private ShiroProperties shiroProperties;

    @Value("${spring.redis.host}:${spring.redis.port}")
    private String host;
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.database}")
    private int database;
    /**
     * shiroFilter
     *
     * @param securityManager
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(@Qualifier("webSecurityManager") @Lazy DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(shiroProperties.getFilterChainMap());
        Map<String, Filter> filters = new HashMap<>(3);
        // 跨域拦截
        OriginFilter originFilter = new OriginFilter();
        filters.put("origin", originFilter);
        // 用户请求拦截
        filters.put("jwt", new UserJwtFilter());
        // API请求拦截
        filters.put("api", new AppJwtFilter());
        shiroFilterFactoryBean.setFilters(filters);
        return shiroFilterFactoryBean;
    }

    /**
     * redis配置
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPassword(password);
        redisManager.setDatabase(database);
        return redisManager;
    }

    @Bean("redisCacheManager")
    public RedisCacheManager cacheManager() {
        RedisCacheManager cacheManager = new RedisCacheManager();
        cacheManager.setRedisManager(redisManager());
        // redis key默认 = shiro:cache:com.unionticketing.auth.interceptor.realm.MyRealm.authorizationCache:用户ID
        // redis key = shiro:cache:com.unionticketing.auth.interceptor.realm.MyRealm.authorizationCache:token值
        cacheManager.setPrincipalIdFieldName("token");
        cacheManager.setExpire(shiroProperties.getCacheTimeout());
        return cacheManager;
    }

    /**
     * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
     * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session
     */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator(){
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }
    /**
     * 配置webSecurityManager
     *
     * @param
     * @return
     **/
    @Bean("webSecurityManager")
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm());
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

    @Bean("myRealm")
    public MyRealm myRealm() {
        return new MyRealm();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("webSecurityManager") @Lazy
                                                                                           DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

2.3. MyRealm

package com.example.shiro.configuration;

import com.example.shiro.service.TokenService;
import lombok.NoArgsConstructor;
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.apache.shiro.subject.SimplePrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;

import java.util.ArrayList;
import java.util.List;

/**
 * 自定义Realm
 *
 * @author xsshu
 * @date 2020-07-18 16:33:12
 */
@Slf4j
@NoArgsConstructor
public class MyRealm extends AuthorizingRealm {

    /**
     * 增加@Lazy注解 是TokenService为低优先级注入的bean,为防止项目启动时报Bean 'xxx' of type [com.xx.xxx.xxxx.xxxxx$$EnhancerBySpringCGLIB$$babebd0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
     */
    @Autowired
    @Lazy
    private TokenService tokenService;

    /**
     * 定义自己的认证匹配方式
     *
     * @param jwtCredentialsMatcher
     */
    public MyRealm(JwtCredentialsMatcher jwtCredentialsMatcher) {
        super(jwtCredentialsMatcher);
    }

    /**
     * 添加支持自定义token
     *
     * @param token token
     * @return 是否支持
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        if (token instanceof JwtToken) {
            return true;
        }
        return super.supports(token);
    }

    /**
     * 清除权限缓存
     *
     * @param principals
     */
    @Override
    protected void clearCachedAuthenticationInfo(PrincipalCollection principals) {
        super.clearCachedAuthenticationInfo(new SimplePrincipalCollection(principals, getName()));
    }

    /**
     * 授权
     *
     * @param authenticationToken 请求的token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (authenticationToken instanceof JwtToken) {
            JwtToken jwtToken = (JwtToken) authenticationToken;
            // 用户TOKEN 授权
            String tokenType = jwtToken.getTokenType();
            try {
                if (JwtToken.USER_TYPE.equals(tokenType) || JwtToken.API_TYPE.equals(tokenType)) {
                    tokenService.validateToken(jwtToken);
                } else {
                    log.error("不合法的token");
                    throw new AuthenticationException("不合法的token");
                }
            } catch (Exception e) {
                log.error("tokenType:{} 校验异常:{}", tokenType, e.getMessage());
                throw new AuthenticationException("token校验失败", e);
            }
            return new SimpleAuthenticationInfo(jwtToken, authenticationToken, getName());
        }
        throw new AuthenticationException("token不合法.");
    }

    /**
     * 设置权限信息
     *
     * @param principals
     * @return 设置权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Object primaryPrincipal = principals.getPrimaryPrincipal();
        List<String> permissionList = new ArrayList<>();
        if (primaryPrincipal instanceof JwtToken) {
            JwtToken jwtToken = (JwtToken) primaryPrincipal;
            // TOKEN 授权
            String token = jwtToken.getToken();
            String tokenType = jwtToken.getTokenType();
            if (tokenType.equals(JwtToken.USER_TYPE)) {
                // 根据token解析用户信息查询用户所拥有的的权限列表,这里只是测试数据
                permissionList.add("demo:user:query");
            } else {
                // 获取接口的权限列表,这里只是测试数据
                permissionList.add("demo:api:test:add");
                permissionList.add("demo:api:test:delete");
            }
        }
        SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();
        authInfo.addStringPermissions(permissionList);
        return authInfo;
    }

}

说明

  1. MyReam类中用到了@Lazy注解,该注解的作用是:增加@Lazy注解 是TokenService为低优先级注入的bean,为防止项目启动时报Bean 'xxx' of type [com.xx.xxx.xxxx.xxxxx$$EnhancerBySpringCGLIB$$babebd0] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
  2. 关于权限的设置,如果权限存在包含关系,那么配置了大范围的权限后,即使没有配置范围小的权限,也是可以访问的。比如:父菜单-用户权限管理,对应权限编码为:demo:user:auth;功能菜单用户管理查询对应权限编码为:demo:user:auth:query;那么配置了demo:user:auth后,即使没有配置demo:user:auth:query,去访问相应的带权限查询接口的时候依然可以访问到。避免上述问题的解决方案具体操作如下:
    1. 父菜单-用户权限管理,对应权限编码为:demo:user:auth:manager;子菜单-用户管理查询,对应权限编码为:demo:user:auth:query
    2. 查询菜单的时候排除掉 父菜单-用户权限管理这种非功能性菜单权限

3. 测试

3.1. 登录测试

POST http://localhost:6666/login
Content-Type: application/json;utf-8

Body
{"account": "admin", "password": "admin"}

返回数据
{"ret":0,"code":0,"msg":"success","data":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoidXNlciIsIm5iZiI6MTU5NTIyNzg5MywiaXNzIjoiYWRtaW4iLCJpYXQiOjE1OTUyMjc4OTMsImFjY291bnQiOiJhZG1pbiIsImp0aSI6ImExYjEyYjkyLWM1YjQtNGRmZC05ZjI5LWNjNTRiOGNkZjU4YyJ9.H4SGEHKo6f9SwrRYEYacKKJfR9GxKhYFO3zGmCv_f5k"}

3.2. 用户查询(权限编码:demo:user:query)

GET http://localhost:6666/user/query
Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoidXNlciIsIm5iZiI6MTU5NTIyNzg5MywiaXNzIjoiYWRtaW4iLCJpYXQiOjE1OTUyMjc4OTMsImFjY291bnQiOiJhZG1pbiIsImp0aSI6ImExYjEyYjkyLWM1YjQtNGRmZC05ZjI5LWNjNTRiOGNkZjU4YyJ9.H4SGEHKo6f9SwrRYEYacKKJfR9GxKhYFO3zGmCv_f5k

返回数据
{"ret":0,"code":0,"msg":"success","data":"hell query"}

demo地址:https://gitee.com/xsshu/shiro-demo.git

如果有什么表述不对的地方,还请各位大佬纠正。

原文地址:https://www.cnblogs.com/xsshu/p/13336760.html