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; } }
說明
- 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) 關於權限的設置,如果權限存在包含關系,那么配置了大范圍的權限后,即使沒有配置范圍小的權限,也是可以訪問的。比如:父菜單-用戶權限管理,對應權限編碼為:demo:user:auth;功能菜單用戶管理查詢對應權限編碼為:demo:user:auth:query;那么配置了demo:user:auth后,即使沒有配置demo:user:auth:query,去訪問相應的帶權限查詢接口的時候依然可以訪問到。避免上述問題的解決方案具體操作如下:
- 父菜單-用戶權限管理,對應權限編碼為:demo:user:auth:manager;子菜單-用戶管理查詢,對應權限編碼為:demo:user:auth:query
- 查詢菜單的時候排除掉 父菜單-用戶權限管理這種非功能性菜單權限
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
如果有什么表述不對的地方,還請各位大佬糾正。