摘要:基于的改造集成真正支持請(qǐng)求這個(gè)模塊分離至項(xiàng)目權(quán)限管理系統(tǒng)與前后端分離實(shí)踐權(quán)限管理系統(tǒng)與前后端分離實(shí)踐,感覺(jué)那樣太長(zhǎng)了找不到重點(diǎn),分離出來(lái)要好點(diǎn)。基于角色的授權(quán)模型中,角色所擁有的資源形式為。最后項(xiàng)目實(shí)現(xiàn)了基于的動(dòng)態(tài)權(quán)限認(rèn)證。
基于shiro的改造集成真正支持restful請(qǐng)求
這個(gè)模塊分離至項(xiàng)目[api權(quán)限管理系統(tǒng)與前后端分離實(shí)踐]api權(quán)限管理系統(tǒng)與前后端分離實(shí)踐,感覺(jué)那樣太長(zhǎng)了找不到重點(diǎn),分離出來(lái)要好點(diǎn)。
首先說(shuō)明設(shè)計(jì)的這個(gè)安全體系是是RBAC(基于角色的權(quán)限訪問(wèn)控制)授權(quán)模型,即用戶--角色--資源,用戶不直接和權(quán)限打交道,角色擁有資源,用戶擁有這個(gè)角色就有權(quán)使用角色所用戶的資源。所有這里沒(méi)有權(quán)限一說(shuō),簽發(fā)jwt里面也就只有用戶所擁有的角色而沒(méi)有權(quán)限。
為啥說(shuō)是真正的restful風(fēng)格集成,雖說(shuō)shiro對(duì)rest不友好但他本身是有支持rest集成的filter--HttpMethodPermissionFilter,這個(gè)shiro rest的 風(fēng)格攔截器,會(huì)自動(dòng)根據(jù)請(qǐng)求方法構(gòu)建權(quán)限字符串( GET=read,POST=create,PUT=update,DELETE=delete)構(gòu)建權(quán)限字符串;eg: /users=rest[user] , 會(huì) 自動(dòng)拼接出user:read,user:create,user:update,user:delete”權(quán)限字符串進(jìn)行權(quán)限匹配(所有都得匹配,isPermittedAll)。
但是這樣感覺(jué)不利于基于jwt的角色的權(quán)限控制,在細(xì)粒度上驗(yàn)權(quán)url(即支持get,post,delete鑒別)就更沒(méi)法了(個(gè)人見解)。打個(gè)比方:我們對(duì)一個(gè)用戶簽發(fā)的jwt寫入角色列(role_admin,role_customer)。對(duì)不同request請(qǐng)求:url="api/resource/",httpMethod="GET",url="api/resource",httpMethod="POST",在基于角色-資源的授權(quán)模型中,這兩個(gè)url相同的請(qǐng)求對(duì)HttpMethodPermissionFilter是一種請(qǐng)求,用戶對(duì)應(yīng)的角色擁有的資源url="api/resource",只要請(qǐng)求的url是"api/resource",不論它的請(qǐng)求方式是什么,都會(huì)判定通過(guò)這個(gè)請(qǐng)求,這在restful風(fēng)格的api中肯定是不可取的,對(duì)同一資源有些角色可能只要查詢的權(quán)限而沒(méi)有修改增加的權(quán)限。
可能會(huì)說(shuō)在jwt中再增加權(quán)限列就好了嘛,但是在基于用戶-資源的授權(quán)模型中,雖然能判別是不同的請(qǐng)求,但是太麻煩了,對(duì)每個(gè)資源我們都要設(shè)計(jì)對(duì)應(yīng)的權(quán)限列然后再塞入到j(luò)wt中,對(duì)每個(gè)用戶都要多帶帶授權(quán)資源這也是不可取的。
對(duì)shiro的改造這里自定義了一些規(guī)則:
shiro過(guò)濾器鏈的url=url+"=="+httpMethod
eg:對(duì)于url="api/resource/",httpMethod="GET"的資源,其拼接出來(lái)的過(guò)濾器鏈匹配url=api/resource==GET
這樣對(duì)相同的url而不同的訪問(wèn)方式,會(huì)判定為不同的資源,即資源不再簡(jiǎn)單是url,而是url和httpMethod的組合。基于角色的授權(quán)模型中,角色所擁有的資源形式為url+"=="+httpMethod。
這里改變了過(guò)濾器的過(guò)濾匹配url規(guī)則,重寫PathMatchingFilterChainResolver的getChain方法,增加對(duì)上述規(guī)則的url的支持。
/* * * @Author tomsun28 * @Description * @Date 21:12 2018/4/20 */ public class RestPathMatchingFilterChainResolver extends PathMatchingFilterChainResolver { private static final Logger LOGGER = LoggerFactory.getLogger(RestPathMatchingFilterChainResolver.class); public RestPathMatchingFilterChainResolver() { super(); } public RestPathMatchingFilterChainResolver(FilterConfig filterConfig) { super(filterConfig); } /* * * @Description 重寫filterChain匹配 * @Param [request, response, originalChain] * @Return javax.servlet.FilterChain */ @Override public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) { FilterChainManager filterChainManager = this.getFilterChainManager(); if (!filterChainManager.hasChains()) { return null; } else { String requestURI = this.getPathWithinApplication(request); Iterator var6 = filterChainManager.getChainNames().iterator(); String pathPattern; boolean flag = true; String[] strings = null; do { if (!var6.hasNext()) { return null; } pathPattern = (String)var6.next(); strings = pathPattern.split("=="); if (strings.length == 2) { // 分割出url+httpMethod,判斷httpMethod和request請(qǐng)求的method是否一致,不一致直接false if (WebUtils.toHttp(request).getMethod().toUpperCase().equals(strings[1].toUpperCase())) { flag = false; } else { flag = true; } } else { flag = false; } pathPattern = strings[0]; } while(!this.pathMatches(pathPattern, requestURI) || flag); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. Utilizing corresponding filter chain..."); } if (strings.length == 2) { pathPattern = pathPattern.concat("==").concat(WebUtils.toHttp(request).getMethod().toUpperCase()); } return filterChainManager.proxy(originalChain, pathPattern); } } }
重寫PathMatchingFilter的路徑匹配方法pathsMatch(),加入httpMethod支持。
/* * * @Author tomsun28 * @Description 重寫過(guò)濾鏈路徑匹配規(guī)則,增加REST風(fēng)格post,get.delete,put..支持 * @Date 23:37 2018/4/19 */ public abstract class BPathMatchingFilter extends PathMatchingFilter { public BPathMatchingFilter() { } /* * * @Description 重寫URL匹配 加入httpMethod支持 * @Param [path, request] * @Return boolean */ @Override protected boolean pathsMatch(String path, ServletRequest request) { String requestURI = this.getPathWithinApplication(request); // path: url==method eg: http://api/menu==GET 需要解析出path中的url和httpMethod String[] strings = path.split("=="); if (strings.length <= 1) { // 分割出來(lái)只有URL return this.pathsMatch(strings[0], requestURI); } else { // 分割出url+httpMethod,判斷httpMethod和request請(qǐng)求的method是否一致,不一致直接false String httpMethod = WebUtils.toHttp(request).getMethod().toUpperCase(); return httpMethod.equals(strings[1].toUpperCase()) && this.pathsMatch(strings[0], requestURI); } } }
這樣增加httpMethod的改造就完成了,重寫ShiroFilterFactoryBean使其使用改造后的chainResolver:RestPathMatchingFilterChainResolver
/* * * @Author tomsun28 * @Description rest支持的shiroFilterFactoryBean * @Date 21:35 2018/4/20 */ public class RestShiroFilterFactoryBean extends ShiroFilterFactoryBean { private static final Logger LOGGER = LoggerFactory.getLogger(RestShiroFilterFactoryBean.class); public RestShiroFilterFactoryBean() { super(); } @Override protected AbstractShiroFilter createInstance() throws Exception { LOGGER.debug("Creating Shiro Filter instance."); SecurityManager securityManager = this.getSecurityManager(); String msg; if (securityManager == null) { msg = "SecurityManager property must be set."; throw new BeanInitializationException(msg); } else if (!(securityManager instanceof WebSecurityManager)) { msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); } else { FilterChainManager manager = this.createFilterChainManager(); RestPathMatchingFilterChainResolver chainResolver = new RestPathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); return new RestShiroFilterFactoryBean.SpringShiroFilter((WebSecurityManager)securityManager, chainResolver); } } private static final class SpringShiroFilter extends AbstractShiroFilter { protected SpringShiroFilter(WebSecurityManager webSecurityManager, FilterChainResolver resolver) { if (webSecurityManager == null) { throw new IllegalArgumentException("WebSecurityManager property cannot be null."); } else { this.setSecurityManager(webSecurityManager); if (resolver != null) { this.setFilterChainResolver(resolver); } } } } }
上面是一些核心的代碼片段,更多請(qǐng)看項(xiàng)目代碼。
對(duì)用戶賬戶登錄注冊(cè)的過(guò)濾filter:PasswordFilter
/* * * @Author tomsun28 * @Description 基于 用戶名密碼 的認(rèn)證過(guò)濾器 * @Date 20:18 2018/2/10 */ public class PasswordFilter extends AccessControlFilter { private static final Logger LOGGER = LoggerFactory.getLogger(PasswordFilter.class); private StringRedisTemplate redisTemplate; @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request,response); // 如果其已經(jīng)登錄,再此發(fā)送登錄請(qǐng)求 if(null != subject && subject.isAuthenticated()){ return true; } // 拒絕,統(tǒng)一交給 onAccessDenied 處理 return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { // 判斷若為獲取登錄注冊(cè)加密動(dòng)態(tài)秘鑰請(qǐng)求 if (isPasswordTokenGet(request)) { //動(dòng)態(tài)生成秘鑰,redis存儲(chǔ)秘鑰供之后秘鑰驗(yàn)證使用,設(shè)置有效期5秒用完即丟棄 String tokenKey = CommonUtil.getRandomString(16); try { redisTemplate.opsForValue().set("PASSWORD_TOKEN_KEY_"+request.getRemoteAddr().toUpperCase(),tokenKey,5, TimeUnit.SECONDS); // 動(dòng)態(tài)秘鑰response返回給前端 Message message = new Message(); message.ok(1000,"issued tokenKey success") .addData("tokenKey",tokenKey); RequestResponseUtil.responseWrite(JSON.toJSONString(message),response); }catch (Exception e) { LOGGER.warn(e.getMessage(),e); // 動(dòng)態(tài)秘鑰response返回給前端 Message message = new Message(); message.ok(1000,"issued tokenKey fail"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),response); } return false; } // 判斷是否是登錄請(qǐng)求 if(isPasswordLoginPost(request)){ AuthenticationToken authenticationToken = createPasswordToken(request); Subject subject = getSubject(request,response); try { subject.login(authenticationToken); //登錄認(rèn)證成功,進(jìn)入請(qǐng)求派發(fā)json web token url資源內(nèi) return true; }catch (AuthenticationException e) { LOGGER.warn(authenticationToken.getPrincipal()+"::"+e.getMessage(),e); // 返回response告訴客戶端認(rèn)證失敗 Message message = new Message().error(1002,"login fail"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),response); return false; }catch (Exception e) { LOGGER.error(e.getMessage(),e); // 返回response告訴客戶端認(rèn)證失敗 Message message = new Message().error(1002,"login fail"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),response); return false; } } // 判斷是否為注冊(cè)請(qǐng)求,若是通過(guò)過(guò)濾鏈進(jìn)入controller注冊(cè) if (isAccountRegisterPost(request)) { return true; } // 之后添加對(duì)賬戶的找回等 // response 告知無(wú)效請(qǐng)求 Message message = new Message().error(1111,"error request"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),response); return false; } private boolean isPasswordTokenGet(ServletRequest request) { // String tokenKey = request.getParameter("tokenKey"); String tokenKey = RequestResponseUtil.getParameter(request,"tokenKey"); return (request instanceof HttpServletRequest) && ((HttpServletRequest) request).getMethod().toUpperCase().equals("GET") && null != tokenKey && "get".equals(tokenKey); } private boolean isPasswordLoginPost(ServletRequest request) { // String password = request.getParameter("password"); // String timestamp = request.getParameter("timestamp"); // String methodName = request.getParameter("methodName"); // String appId = request.getParameter("appId"); Mapmap = RequestResponseUtil.getRequestParameters(request); String password = map.get("password"); String timestamp = map.get("timestamp"); String methodName = map.get("methodName"); String appId = map.get("appId"); return (request instanceof HttpServletRequest) && ((HttpServletRequest) request).getMethod().toUpperCase().equals("POST") && null != password && null != timestamp && null != methodName && null != appId && methodName.equals("login"); } private boolean isAccountRegisterPost(ServletRequest request) { // String uid = request.getParameter("uid"); // String methodName = request.getParameter("methodName"); // String username = request.getParameter("username"); // String password = request.getParameter("password"); Map map = RequestResponseUtil.getRequestParameters(request); String uid = map.get("uid"); String username = map.get("username"); String methodName = map.get("methodName"); String password = map.get("password"); return (request instanceof HttpServletRequest) && ((HttpServletRequest) request).getMethod().toUpperCase().equals("POST") && null != username && null != password && null != methodName && null != uid && methodName.equals("register"); } private AuthenticationToken createPasswordToken(ServletRequest request) { // String appId = request.getParameter("appId"); // String password = request.getParameter("password"); // String timestamp = request.getParameter("timestamp"); Map map = RequestResponseUtil.getRequestParameters(request); String appId = map.get("appId"); String timestamp = map.get("timestamp"); String password = map.get("password"); String host = request.getRemoteAddr(); String tokenKey = redisTemplate.opsForValue().get("PASSWORD_TOKEN_KEY_"+host.toUpperCase()); return new PasswordToken(appId,password,timestamp,host,tokenKey); } public void setRedisTemplate(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } }
支持restful風(fēng)格的jwt鑒權(quán)filter:BJwtFilter
/* * * @Author tomsun28 * @Description 支持restful url 的過(guò)濾鏈 JWT json web token 過(guò)濾器,無(wú)狀態(tài)驗(yàn)證 * @Date 0:04 2018/4/20 */ public class BJwtFilter extends BPathMatchingFilter { private static final Logger LOGGER = LoggerFactory.getLogger(BJwtFilter.class); private StringRedisTemplate redisTemplate; private AccountService accountService; protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception { Subject subject = getSubject(servletRequest,servletResponse); // 判斷是否為JWT認(rèn)證請(qǐng)求 if ((null == subject || !subject.isAuthenticated()) && isJwtSubmission(servletRequest)) { AuthenticationToken token = createJwtToken(servletRequest); try { subject.login(token); // return this.checkRoles(subject,mappedValue) && this.checkPerms(subject,mappedValue); return this.checkRoles(subject,mappedValue); }catch (AuthenticationException e) { LOGGER.info(e.getMessage(),e); // 如果是JWT過(guò)期 if (e.getMessage().equals("expiredJwt")) { // 這里初始方案先拋出令牌過(guò)期,之后設(shè)計(jì)為在Redis中查詢當(dāng)前appId對(duì)應(yīng)令牌,其設(shè)置的過(guò)期時(shí)間是JWT的兩倍,此作為JWT的refresh時(shí)間 // 當(dāng)JWT的有效時(shí)間過(guò)期后,查詢其refresh時(shí)間,refresh時(shí)間有效即重新派發(fā)新的JWT給客戶端, // refresh也過(guò)期則告知客戶端JWT時(shí)間過(guò)期重新認(rèn)證 // 當(dāng)存儲(chǔ)在redis的JWT沒(méi)有過(guò)期,即refresh time 沒(méi)有過(guò)期 String appId = WebUtils.toHttp(servletRequest).getHeader("appId"); String jwt = WebUtils.toHttp(servletRequest).getHeader("authorization"); String refreshJwt = redisTemplate.opsForValue().get("JWT-SESSION-"+appId); if (null != refreshJwt && refreshJwt.equals(jwt)) { // 重新申請(qǐng)新的JWT // 根據(jù)appId獲取其對(duì)應(yīng)所擁有的角色(這里設(shè)計(jì)為角色對(duì)應(yīng)資源,沒(méi)有權(quán)限對(duì)應(yīng)資源) String roles = accountService.loadAccountRole(appId); long refreshPeriodTime = 36000L; //seconds為單位,10 hours String newJwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId, "token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512); // 將簽發(fā)的JWT存儲(chǔ)到Redis: {JWT-SESSION-{appID} , jwt} redisTemplate.opsForValue().set("JWT-SESSION-"+appId,newJwt,refreshPeriodTime, TimeUnit.SECONDS); Message message = new Message().ok(1005,"new jwt").addData("jwt",newJwt); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; }else { // jwt時(shí)間失效過(guò)期,jwt refresh time失效 返回jwt過(guò)期客戶端重新登錄 Message message = new Message().error(1006,"expired jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } } // 其他的判斷為JWT錯(cuò)誤無(wú)效 Message message = new Message().error(1007,"error Jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; }catch (Exception e) { // 其他錯(cuò)誤 LOGGER.warn(servletRequest.getRemoteAddr()+"JWT認(rèn)證"+e.getMessage(),e); // 告知客戶端JWT錯(cuò)誤1005,需重新登錄申請(qǐng)jwt Message message = new Message().error(1007,"error jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } }else { // 請(qǐng)求未攜帶jwt 判斷為無(wú)效請(qǐng)求 Message message = new Message().error(1111,"error request"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); return false; } } protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { Subject subject = getSubject(servletRequest,servletResponse); // 未認(rèn)證的情況 if (null == subject || !subject.isAuthenticated()) { // 告知客戶端JWT認(rèn)證失敗需跳轉(zhuǎn)到登錄頁(yè)面 Message message = new Message().error(1006,"error jwt"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); }else { // 已經(jīng)認(rèn)證但未授權(quán)的情況 // 告知客戶端JWT沒(méi)有權(quán)限訪問(wèn)此資源 Message message = new Message().error(1008,"no permission"); RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse); } // 過(guò)濾鏈終止 return false; } private boolean isJwtSubmission(ServletRequest request) { String jwt = RequestResponseUtil.getHeader(request,"authorization"); String appId = RequestResponseUtil.getHeader(request,"appId"); return (request instanceof HttpServletRequest) && !StringUtils.isEmpty(jwt) && !StringUtils.isEmpty(appId); } private AuthenticationToken createJwtToken(ServletRequest request) { Mapmaps = RequestResponseUtil.getRequestHeaders(request); String appId = maps.get("appId"); String ipHost = request.getRemoteAddr(); String jwt = maps.get("authorization"); String deviceInfo = maps.get("deviceInfo"); return new JwtToken(ipHost,deviceInfo,jwt,appId); } // 驗(yàn)證當(dāng)前用戶是否屬于mappedValue任意一個(gè)角色 private boolean checkRoles(Subject subject, Object mappedValue){ String[] rolesArray = (String[]) mappedValue; return rolesArray == null || rolesArray.length == 0 || Stream.of(rolesArray).anyMatch(role -> subject.hasRole(role.trim())); } // 驗(yàn)證當(dāng)前用戶是否擁有mappedValue任意一個(gè)權(quán)限 private boolean checkPerms(Subject subject, Object mappedValue){ String[] perms = (String[]) mappedValue; boolean isPermitted = true; if (perms != null && perms.length > 0) { if (perms.length == 1) { if (!subject.isPermitted(perms[0])) { isPermitted = false; } } else { if (!subject.isPermittedAll(perms)) { isPermitted = false; } } } return isPermitted; } public void setRedisTemplate(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public void setAccountService(AccountService accountService) { this.accountService = accountService; } }
realm數(shù)據(jù)源,數(shù)據(jù)提供service,匹配matchs,自定義token,spring集成shiro配置等其他詳見項(xiàng)目代碼。
最后項(xiàng)目實(shí)現(xiàn)了基于jwt的動(dòng)態(tài)restful api權(quán)限認(rèn)證。
github:
bootshiro
usthe
碼云:
bootshiro
usthe
分享一波阿里云代金券快速上云
轉(zhuǎn)載請(qǐng)注明 from tomsun28
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/69183.html
摘要:框架具有輕便,開源的優(yōu)點(diǎn),所以本譯見構(gòu)建用戶管理微服務(wù)五使用令牌和來(lái)實(shí)現(xiàn)身份驗(yàn)證往期譯見系列文章在賬號(hào)分享中持續(xù)連載,敬請(qǐng)查看在往期譯見系列的文章中,我們已經(jīng)建立了業(yè)務(wù)邏輯數(shù)據(jù)訪問(wèn)層和前端控制器但是忽略了對(duì)身份進(jìn)行驗(yàn)證。 重拾后端之Spring Boot(四):使用JWT和Spring Security保護(hù)REST API 重拾后端之Spring Boot(一):REST API的搭建...
摘要:自己在前后端分離上的實(shí)踐要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)管理,這次實(shí)踐包含兩個(gè)模塊基于搭建的權(quán)限管理系統(tǒng)后臺(tái)編寫的前端管理。 自己在前后端分離上的實(shí)踐 要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)restful api管理,這次實(shí)踐包含兩個(gè)模塊,基于springBoot + shiro搭建的權(quán)限管理系統(tǒng)后臺(tái)bootshir...
摘要:自己在前后端分離上的實(shí)踐要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)管理,這次實(shí)踐包含兩個(gè)模塊基于搭建的權(quán)限管理系統(tǒng)后臺(tái)編寫的前端管理。 自己在前后端分離上的實(shí)踐 要想實(shí)現(xiàn)完整的前后端分離,安全這塊是繞不開的,這個(gè)系統(tǒng)主要功能就是動(dòng)態(tài)restful api管理,這次實(shí)踐包含兩個(gè)模塊,基于springBoot + shiro搭建的權(quán)限管理系統(tǒng)后臺(tái)bootshir...
閱讀 2636·2021-11-11 16:55
閱讀 1279·2021-09-22 15:25
閱讀 1793·2019-08-29 16:26
閱讀 925·2019-08-29 13:21
閱讀 2306·2019-08-23 16:19
閱讀 2795·2019-08-23 15:10
閱讀 761·2019-08-23 14:24
閱讀 1850·2019-08-23 13:48