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

 

如果有什么表述不對的地方,還請各位大佬糾正。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM