Springboot+Shiro+Jwt實現(xiàn)權限控制的項目實踐
前置背景
為什么寫下這篇文章?
因為需要實現(xiàn)一個設備管理系統(tǒng)的權限管理模塊,在查閱很多博客以及其他網(wǎng)上資料之后,發(fā)現(xiàn)重復、無用的博客很多,因此寫一篇文章來記錄,以便后面復習。
涉及的知識點主要有下列知識點:
- JWT
- shiro
書寫順序
- 首先使用springboot 結合 jwt完成前后端分離的token認證。
- 其次結合shiro完成shiro+jwt的前后端分離的權限認證管理。
權限管理的表結構設計
一個user可以擁有多個role,一個role也可以被多個user擁有, 一個 角色擁有多個權限即功能,一個權限可以被多個role擁有。
用戶、角色、權限類
表結構圖
Part1: spring boot + jwt
這一部分就可以完成前后端分離項目的登錄功能。在不需要添加權限管理的情況下,就可以滿足需求。
Spring boot集成JWT
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.2</version> </dependency>
思路整理
- 為什么需要使用到jwt?
- jwt是什么
- Java如何使用jwt
在前后端分離的項目中,由服務器使用的會話管理 session
無法滿足需求。需要一種技術做會話管理。因此選擇 JWT
。 Json web token (JWT)
: 是目前流行的跨域認證解決方案,是一種基于 Token 的認證授權機制。 JWT
的數(shù)據(jù)結構分為三部分 header payload signature。 這三部分通過 .
連接,如下
Token示例:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJwYXNzd29yZCI6InN1cGVyIiwiZXhwIjoxNjYzMTIzNDgzLCJ1c2VybmFtZSI6InN1cGVyIn0.
5xVg6IuOLe_uVwwOaeyRDbTHRjfmIbNsnb-DP9-Ic20
如何使用:當用戶登錄系統(tǒng)后,服務端給前端發(fā)送一個基于用戶信息創(chuàng)建的token,然后在此后的每一次前端請求都會攜帶token。服務端通過攔截器攔截請求,同時驗證攜帶的token是否正確。如果正確則放行請求,不正確則拒絕通過。 思路流程圖:
token的創(chuàng)建和驗證
JWTUtil.java
負責創(chuàng)建和驗證jwt格式的token
public class JWTUtil { private static final long EXPIRE_TIME = 3 * 60 * 1000;//默認3分鐘 //私鑰 private static final String TOKEN_SECRET = "privateKey"; ? public static String createToken(UserEntity userModel) { try { // 設置過期時間 Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); log.info(String.valueOf(date)); // 私鑰和加密算法 Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); // 設置頭部信息 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; } } /** * 檢驗token是否正確 * * @param **token** * @return */ public static boolean verifyToken(String token, String username) { log.info("驗證token.."); try { Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); JWTVerifier verifier = JWT.require(algorithm) .withClaim("username",username).build(); // 驗證不通過會拋出異常。 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)建攔截器,攔截請求
@Slf4j @Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 當前端是通過在請求里面以 token="xxxx.xxx.zzz"的方式傳遞時,通過getHeader("token") // 的方式獲取。 String token = request.getHeader("token"); log.info("token = {}",token); if (token == null){ setReturnInfo((HttpServletResponse) response,401,"請攜帶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
負責將使用了JwtUtil的攔截器配置進入Spring boot。
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Resource private LoginInterceptor interceptor; ? @Override public void addInterceptors(InterceptorRegistry registry) { List<String> patterns = new ArrayList<>(); ? // 添加過濾路由 默認攔截所有請求 patterns.add("/**"); registry.addInterceptor(interceptor) .addPathPatterns(patterns) .excludePathPatterns("/user/login"); // 用戶登錄請求不攔截 } ? }
Part2:shiro+jwt
- 明確shiro是什么
- shiro的工作流程
- spring boot如何配置shiro框架
- shiro和jwt的整合
shiro是一個權限管理框架,相對于Spring Security而言,代碼簡單。適用場景多,不只局限于Java。 shiro的架構原理圖:
- Subject:當前和系統(tǒng)交互的"用戶",可以是人,系統(tǒng),第三方插件等。統(tǒng)稱為Subject。 SecurityManager:類似于Spring容器的一個容器,是Shiro的核心。管理眾多的組件。
- Authenticator:認證組件,用戶需要先通過系統(tǒng)認證,在進行用戶授權,需要先判斷系統(tǒng)中是否有這個用戶,在進行后續(xù)操作,因此在這里就是進行系統(tǒng)認證的地方。
- Authorizer:授權,當一個用戶是屬于當前系統(tǒng)時,這個用戶的一些操作就需要判斷是否有權力去做這件事情。在這里就需要進行授權相關的
- Realm 可以理解為數(shù)據(jù)源,就是在realm記錄了那些屬于本系統(tǒng)的用戶,他們具有什么樣的角色
就相當于在一個擁有多個公司的工業(yè)園區(qū),人們需要有這個園區(qū)的卡片,才允許進入園區(qū),而進入園區(qū)之后需要由本公司的門禁你才能進入公司,否則就不能進入公司一樣。A公司的員工不能進入B公司。 Subject
就是員工,或者快遞小哥, SecurityManager
就是園區(qū)門禁系統(tǒng), Authenticator
就是門禁的閘機 Authorizer
的作用就是公司的門禁一樣, realm
就是園區(qū)系統(tǒng)的數(shù)據(jù)庫,記錄了系統(tǒng)的用戶和權限信息 。
工作流程:一個subject通過login()方法,將subject的信息提交給SecurityManager,SecurityManager調(diào)用自己的組件去判斷,認證,授權等。shiro是通過filter來進行攔截請求的,因此在結合jwt時,就不需要interceptor就能達到預期的效果。
思路流程圖:
第一步改造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)建和驗證邏輯如上。 第二步編寫自己的數(shù)據(jù)源Realm
public class MyRealm extends AuthorizingRealm { ? // 指定憑證匹配器。匹配器工作在認證后,授權前。 public MyRealm() { this.setCredentialsMatcher(new JWTCredentialsMatcher()); } @Resource UserServiceInt userServiceInt; // 判斷token是否為JWTToken 必須重寫 @Override public boolean supports(AuthenticationToken token) { return token instanceof JWTToken; } // 認證 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { ? log.info("AuthenticationInfo 開始認證"); String token = ((JWTToken) authenticationToken).getToken(); String username = JWT.decode(token).getClaim("username").asString(); // 從系統(tǒng)的數(shù)據(jù)庫查找是否擁有這個用戶,也可以提前把數(shù)據(jù)加載到Redis中,從redis中查找即可。 UserModel user = userServiceInt.getUserByUsername(username); if (user == null) { log.info("user為空"); // 認證不通過 return null; } SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( user, token, "myRealm" ); return simpleAuthenticationInfo; } // 授權 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.info("開始授權..."); // 從PrincipalCollection獲取user UserModel userModel = (UserModel) principalCollection.getPrimaryPrincipal(); ? SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 模擬數(shù)據(jù)庫操作。實際上可以利用mybatis的級聯(lián)查詢,查詢出用戶的角色和權限信息。 if (userModel.getUsername().equals("super")){ // 添加用戶角色 simpleAuthorizationInfo.addRole("admin"); // 添加用戶權限 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驗證token即可 return JWTUtil.verifyToken(token, userModel.getUsername(), userModel.getPassword()); } }
第三:編寫filter攔截前端請求
public class JwtFilter extends BasicHttpAuthenticationFilter { @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { log.info("isAccessAllowed : 驗證是否擁有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進行登入,如果錯誤他會拋出異常并被捕獲 try { getSubject(request, response).login(jwtToken); } catch (Exception e) { log.info("認證出現(xiàn)異常:{}", e.getMessage()); try { setReturnInfo(httpServletResponse,401,"token錯誤"); } 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); } /** * 對跨域提供支持 */ @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"); // 跨域時會首先發(fā)送一個option請求,這里我們給option請求直接返回正常狀態(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); /* * 關閉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權限控制的注解即可
@GetMapping("list") // @RequiresAuthentication @RequiresRoles(value = {"admin"}) // @RequiresPermissions(value = {"user:list"}) public List<UserModel> listUsers(){ return userServiceInt.listUser(); }
到此這篇關于Springboot+Shiro+Jwt實現(xiàn)權限控制的項目實踐的文章就介紹到這了,更多相關Springboot Shiro Jwt實現(xiàn)權限控制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JAVA實現(xiàn)社會統(tǒng)一信用代碼校驗的方法
這篇文章主要介紹了JAVA實現(xiàn)社會統(tǒng)一信用代碼校驗的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-07-07Springboot發(fā)送郵件功能的實現(xiàn)詳解
電子郵件是—種用電子手段提供信息交換的通信方式,是互聯(lián)網(wǎng)應用最廣的服務。本文詳細為大家介紹了SpringBoot實現(xiàn)發(fā)送電子郵件功能的示例代碼,需要的可以參考一下2022-09-09SpringBoot通過@Scheduled實現(xiàn)定時任務及單線程運行問題解決
Scheduled定時任務是Spring boot自身提供的功能,所以不需要引入Maven依賴包,下面這篇文章主要給大家介紹了關于SpringBoot通過@Scheduled實現(xiàn)定時任務以及問題解決的相關資料,需要的朋友可以參考下2023-02-02