Springboot+Shiro+Jwt實(shí)現(xiàn)權(quán)限控制的項(xiàng)目實(shí)踐
前置背景
為什么寫下這篇文章?
因?yàn)樾枰獙?shí)現(xiàn)一個(gè)設(shè)備管理系統(tǒng)的權(quán)限管理模塊,在查閱很多博客以及其他網(wǎng)上資料之后,發(fā)現(xiàn)重復(fù)、無用的博客很多,因此寫一篇文章來記錄,以便后面復(fù)習(xí)。
涉及的知識(shí)點(diǎn)主要有下列知識(shí)點(diǎn):
- JWT
- shiro
書寫順序
- 首先使用springboot 結(jié)合 jwt完成前后端分離的token認(rèn)證。
- 其次結(jié)合shiro完成shiro+jwt的前后端分離的權(quán)限認(rèn)證管理。
權(quán)限管理的表結(jié)構(gòu)設(shè)計(jì)
一個(gè)user可以擁有多個(gè)role,一個(gè)role也可以被多個(gè)user擁有, 一個(gè) 角色擁有多個(gè)權(quán)限即功能,一個(gè)權(quán)限可以被多個(gè)role擁有。

用戶、角色、權(quán)限類

表結(jié)構(gòu)圖
Part1: spring boot + jwt
這一部分就可以完成前后端分離項(xiàng)目的登錄功能。在不需要添加權(quán)限管理的情況下,就可以滿足需求。
Spring boot集成JWT
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.2</version> </dependency>
思路整理
- 為什么需要使用到j(luò)wt?
- jwt是什么
- Java如何使用jwt
在前后端分離的項(xiàng)目中,由服務(wù)器使用的會(huì)話管理 session 無法滿足需求。需要一種技術(shù)做會(huì)話管理。因此選擇 JWT 。 Json web token (JWT) : 是目前流行的跨域認(rèn)證解決方案,是一種基于 Token 的認(rèn)證授權(quán)機(jī)制。 JWT 的數(shù)據(jù)結(jié)構(gòu)分為三部分 header payload signature。 這三部分通過 . 連接,如下

Token示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJwYXNzd29yZCI6InN1cGVyIiwiZXhwIjoxNjYzMTIzNDgzLCJ1c2VybmFtZSI6InN1cGVyIn0.
5xVg6IuOLe_uVwwOaeyRDbTHRjfmIbNsnb-DP9-Ic20
如何使用:當(dāng)用戶登錄系統(tǒng)后,服務(wù)端給前端發(fā)送一個(gè)基于用戶信息創(chuàng)建的token,然后在此后的每一次前端請(qǐng)求都會(huì)攜帶token。服務(wù)端通過攔截器攔截請(qǐng)求,同時(shí)驗(yàn)證攜帶的token是否正確。如果正確則放行請(qǐng)求,不正確則拒絕通過。 思路流程圖:

token的創(chuàng)建和驗(yàn)證
JWTUtil.java 負(fù)責(zé)創(chuàng)建和驗(yàn)證jwt格式的token
public class JWTUtil {
private static final long EXPIRE_TIME = 3 * 60 * 1000;//默認(rèn)3分鐘
//私鑰
private static final String TOKEN_SECRET = "privateKey";
?
public static String createToken(UserEntity userModel) {
try {
// 設(shè)置過期時(shí)間
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
log.info(String.valueOf(date));
// 私鑰和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 設(shè)置頭部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HSA256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("username", userModel.getUsername())
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 檢驗(yàn)token是否正確
*
* @param **token**
* @return
*/
public static boolean verifyToken(String token, String username) {
log.info("驗(yàn)證token..");
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username",username).build();
// 驗(yàn)證不通過會(huì)拋出異常。
verifier.verify(token);
return true;
} catch (Exception e) {
log.info("verifyToken = {}",e.getMessage());
return false;
}
}
?
// 通過withClaim添加在token里面的數(shù)據(jù)都可以通過這種方式獲取
public static String getUsername(String token){
DecodedJWT jwt = JWT.decode(token);
String username = String.valueOf(jwt.getClaim("username"));
if (StringUtils.hasLength(username)){
return username;
}
return null;
}
}攔截器的創(chuàng)建和配置
創(chuàng)建攔截器,攔截請(qǐng)求
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 當(dāng)前端是通過在請(qǐng)求里面以 token="xxxx.xxx.zzz"的方式傳遞時(shí),通過getHeader("token")
// 的方式獲取。
String token = request.getHeader("token");
log.info("token = {}",token);
if (token == null){
setReturnInfo((HttpServletResponse) response,401,"請(qǐng)攜帶token");
return false;
}
// 解析token中的數(shù)據(jù),JWTUtil.getUsername();
// 在這里可以通過findUserByUsername的方式從數(shù)據(jù)源中獲取數(shù)據(jù)
// 假定登錄用戶是super, 并傳遞給此方法傳遞參數(shù)
if ( !JWTUtil.verifyToken(token,"super")){
setReturnInfo((HttpServletResponse) response,401,"token已過期");
return false;
}
return true;
}
private static void setReturnInfo(HttpServletResponse httpResponse,int status,String msg) throws IOException {
log.info("token = null");
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
httpResponse.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
Map<String,String> result =new HashMap<>();
result.put("status",String.valueOf(status));
result.put("msg",msg);
httpResponse.getWriter().print(JSONUtils.toJSONString(result));
// 前端可根據(jù)返回的status判斷
}
}2. 配置攔截器
InterceptorConfig.java 負(fù)責(zé)將使用了JwtUtil的攔截器配置進(jìn)入Spring boot。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Resource
private LoginInterceptor interceptor;
?
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> patterns = new ArrayList<>();
?
// 添加過濾路由 默認(rèn)攔截所有請(qǐng)求
patterns.add("/**");
registry.addInterceptor(interceptor)
.addPathPatterns(patterns)
.excludePathPatterns("/user/login"); // 用戶登錄請(qǐng)求不攔截
}
?
}Part2:shiro+jwt
- 明確shiro是什么
- shiro的工作流程
- spring boot如何配置shiro框架
- shiro和jwt的整合
shiro是一個(gè)權(quán)限管理框架,相對(duì)于Spring Security而言,代碼簡(jiǎn)單。適用場(chǎng)景多,不只局限于Java。 shiro的架構(gòu)原理圖:

- Subject:當(dāng)前和系統(tǒng)交互的"用戶",可以是人,系統(tǒng),第三方插件等。統(tǒng)稱為Subject。 SecurityManager:類似于Spring容器的一個(gè)容器,是Shiro的核心。管理眾多的組件。
- Authenticator:認(rèn)證組件,用戶需要先通過系統(tǒng)認(rèn)證,在進(jìn)行用戶授權(quán),需要先判斷系統(tǒng)中是否有這個(gè)用戶,在進(jìn)行后續(xù)操作,因此在這里就是進(jìn)行系統(tǒng)認(rèn)證的地方。
- Authorizer:授權(quán),當(dāng)一個(gè)用戶是屬于當(dāng)前系統(tǒng)時(shí),這個(gè)用戶的一些操作就需要判斷是否有權(quán)力去做這件事情。在這里就需要進(jìn)行授權(quán)相關(guān)的
- Realm 可以理解為數(shù)據(jù)源,就是在realm記錄了那些屬于本系統(tǒng)的用戶,他們具有什么樣的角色
就相當(dāng)于在一個(gè)擁有多個(gè)公司的工業(yè)園區(qū),人們需要有這個(gè)園區(qū)的卡片,才允許進(jìn)入園區(qū),而進(jìn)入園區(qū)之后需要由本公司的門禁你才能進(jìn)入公司,否則就不能進(jìn)入公司一樣。A公司的員工不能進(jìn)入B公司。 Subject 就是員工,或者快遞小哥, SecurityManager 就是園區(qū)門禁系統(tǒng), Authenticator 就是門禁的閘機(jī) Authorizer 的作用就是公司的門禁一樣, realm 就是園區(qū)系統(tǒng)的數(shù)據(jù)庫(kù),記錄了系統(tǒng)的用戶和權(quán)限信息 。
工作流程:一個(gè)subject通過login()方法,將subject的信息提交給SecurityManager,SecurityManager調(diào)用自己的組件去判斷,認(rèn)證,授權(quán)等。shiro是通過filter來進(jìn)行攔截請(qǐng)求的,因此在結(jié)合jwt時(shí),就不需要interceptor就能達(dá)到預(yù)期的效果。
思路流程圖:

第一步改造JwtToken
// AuthenticationToken 是shiro框架的。
public class JWTToken implements AuthenticationToken {
private String token;
?
public JWTToken(String token) {
this.token = token;
}
?
public String getToken() {
return token;
}
?
@Override
public Object getPrincipal() {
return token;
}
?
@Override
public Object getCredentials() {
return token;
}
?
}JWTUtils代碼不變,創(chuàng)建和驗(yàn)證邏輯如上。 第二步編寫自己的數(shù)據(jù)源Realm
public class MyRealm extends AuthorizingRealm {
?
// 指定憑證匹配器。匹配器工作在認(rèn)證后,授權(quán)前。
public MyRealm() {
this.setCredentialsMatcher(new JWTCredentialsMatcher());
}
@Resource
UserServiceInt userServiceInt;
// 判斷token是否為JWTToken 必須重寫
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
// 認(rèn)證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
?
log.info("AuthenticationInfo 開始認(rèn)證");
String token = ((JWTToken) authenticationToken).getToken();
String username = JWT.decode(token).getClaim("username").asString();
// 從系統(tǒng)的數(shù)據(jù)庫(kù)查找是否擁有這個(gè)用戶,也可以提前把數(shù)據(jù)加載到Redis中,從redis中查找即可。
UserModel user = userServiceInt.getUserByUsername(username);
if (user == null) {
log.info("user為空");
// 認(rèn)證不通過
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
user,
token,
"myRealm"
);
return simpleAuthenticationInfo;
}
// 授權(quán)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("開始授權(quán)...");
// 從PrincipalCollection獲取user
UserModel userModel = (UserModel) principalCollection.getPrimaryPrincipal();
?
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 模擬數(shù)據(jù)庫(kù)操作。實(shí)際上可以利用mybatis的級(jí)聯(lián)查詢,查詢出用戶的角色和權(quán)限信息。
if (userModel.getUsername().equals("super")){
// 添加用戶角色
simpleAuthorizationInfo.addRole("admin");
// 添加用戶權(quán)限
simpleAuthorizationInfo.addStringPermission("user:list");
}
return simpleAuthorizationInfo;
}
}public class JWTCredentialsMatcher implements CredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) {
?
String token = ((JWTToken)authenticationToken).getToken();
log.info("JWTCredentialsMatcher token = {}",token);
UserModel userModel = (UserModel) authenticationInfo.getPrincipals().getPrimaryPrincipal();
log.info("JWTCredentialsMatcher token = {}",userModel.toString());
// 調(diào)用JwtUtils驗(yàn)證token即可
return JWTUtil.verifyToken(token, userModel.getUsername(), userModel.getPassword());
}
}第三:編寫filter攔截前端請(qǐng)求
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
log.info("isAccessAllowed : 驗(yàn)證是否擁有token");
String token = ((HttpServletRequest) request).getHeader("token");
HttpServletResponse servletResponse = (HttpServletResponse) response;
if (!StringUtils.hasLength(token)) {
try {
setReturnInfo(servletResponse, 401, "token為空");
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
return executeLogin(request,response);
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
log.info("executeLogin : 執(zhí)行登錄");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String token = httpServletRequest.getHeader("token");
JWTToken jwtToken = new JWTToken(token);
// 提交給realm進(jìn)行登入,如果錯(cuò)誤他會(huì)拋出異常并被捕獲
try {
getSubject(request, response).login(jwtToken);
} catch (Exception e) {
log.info("認(rèn)證出現(xiàn)異常:{}", e.getMessage());
try {
setReturnInfo(httpServletResponse,401,"token錯(cuò)誤");
} catch (IOException ex) {
ex.printStackTrace();
}
return false;
}
// 如果沒有拋出異常則代表登入成功,返回true
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
log.info("登錄失敗");
return super.onAccessDenied(request, response);
}
/**
* 對(duì)跨域提供支持
*/
@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"));
httpServletResponse.setCharacterEncoding("UTF-8");
// 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請(qǐng)求,這里我們給option請(qǐng)求直接返回正常狀態(tài)
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return true;
}
return super.preHandle(request, response);
}
private static void setReturnInfo(HttpServletResponse response, int status, String msg) throws IOException {
response.setContentType("application/json;charset=utf-8");
Map<String, String> result = new HashMap<>();
result.put("status", String.valueOf(status));
result.put("msg", msg);
response.getWriter().write(JSONUtils.toJSONString(result));
}第四:配置shiro
@Configuration
public class ShiroConfig {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("jwt",new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login","anon");
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
// 禁用session
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator(){
DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean("securityManager")
public SecurityManager securityManager(MyRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 指定SecurityManager的域
securityManager.setRealm(userRealm);
/*
* 關(guān)閉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("myRealm")
public MyRealm shiroRealm() {
MyRealm shiroRealm = new MyRealm();
return shiroRealm;
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
/**
* 開啟shiro aop注解支持.
* 使用代理方式;所以需要開啟代碼支持;
* @param
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}第五步在controller的方法上適用shiro權(quán)限控制的注解即可
@GetMapping("list")
// @RequiresAuthentication
@RequiresRoles(value = {"admin"})
// @RequiresPermissions(value = {"user:list"})
public List<UserModel> listUsers(){
return userServiceInt.listUser();
}到此這篇關(guān)于Springboot+Shiro+Jwt實(shí)現(xiàn)權(quán)限控制的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)Springboot Shiro Jwt實(shí)現(xiàn)權(quán)限控制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?面向?qū)ο笸ㄟ^new揭開對(duì)象實(shí)例化
各位鐵汁們大家好呀,我們上次博客講了,通過?Student?student1?=?new?Student();就可以實(shí)例化一個(gè)對(duì)象,這個(gè)對(duì)象就有Student類中的所以成員變量??墒?對(duì)象student1?和?類Student到底是怎樣建立聯(lián)系的,在內(nèi)存中到底發(fā)生了什么2022-04-04
Java枚舉類實(shí)現(xiàn)Key-Value映射的多種實(shí)現(xiàn)方式
在 Java 開發(fā)中,枚舉(Enum)是一種特殊的類,本文將詳細(xì)介紹 Java 枚舉類實(shí)現(xiàn) key-value 映射的多種方式,有需要的小伙伴可以根據(jù)需要進(jìn)行選擇2025-04-04
新版Android?Studio修改jdk版本的簡(jiǎn)單步驟
這篇文章主要介紹了新版Android?Studio修改jdk版本的簡(jiǎn)單步驟,升級(jí)Android?Studio后,JDK版本設(shè)置選項(xiàng)消失,可以通過Settings面板進(jìn)入Gradle設(shè)置,修改GradleJDK路徑來解決問題,需要的朋友可以參考下2025-03-03
Java多線程中ReentrantLock與Condition詳解
這篇文章主要介紹了Java多線程中ReentrantLock與Condition詳解,需要的朋友可以參考下2017-11-11
java實(shí)現(xiàn)OpenGL ES紋理映射的方法
這篇文章主要介紹了java實(shí)現(xiàn)OpenGL ES紋理映射的方法,以實(shí)例形式較為詳細(xì)的分析了紋理映射的實(shí)現(xiàn)技巧,需要的朋友可以參考下2015-06-06
JavaBean和SpringBean的區(qū)別及創(chuàng)建SpringBean方式
這篇文章主要介紹了JavaBean和SpringBean的區(qū)別及創(chuàng)建SpringBean方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10

