基于shiro的改造集成真正支持restful请求
基于shiro的改造集成真正支持restful請求
這個模塊分離至項目[api權限管理系統與前后端分離實踐]api權限管理系統與前后端分離實踐,感覺那樣太長了找不到重點,分離出來要好點。
首先說明設計的這個安全體系是是RBAC(基于角色的權限訪問控制)授權模型,即用戶--角色--資源,用戶不直接和權限打交道,角色擁有資源,用戶擁有這個角色就有權使用角色所用戶的資源。所有這里沒有權限一說,簽發jwt里面也就只有用戶所擁有的角色而沒有權限。
為啥說是真正的restful風格集成,雖說shiro對rest不友好但他本身是有支持rest集成的filter--HttpMethodPermissionFilter,這個shiro rest的 風格攔截器,會自動根據請求方法構建權限字符串( GET=read,POST=create,PUT=update,DELETE=delete)構建權限字符串;eg: /users=rest[user] , 會 自動拼接出user:read,user:create,user:update,user:delete”權限字符串進行權限匹配(所有都得匹配,isPermittedAll)。
但是這樣感覺不利于基于jwt的角色的權限控制,在細粒度上驗權url(即支持get,post,delete鑒別)就更沒法了(個人見解)。打個比方:我們對一個用戶簽發的jwt寫入角色列(role_admin,role_customer)。對不同request請求:url="api/resource/",httpMethod="GET",url="api/resource",httpMethod="POST",在基于角色-資源的授權模型中,這兩個url相同的請求對HttpMethodPermissionFilter是一種請求,用戶對應的角色擁有的資源url="api/resource",只要請求的url是"api/resource",不論它的請求方式是什么,都會判定通過這個請求,這在restful風格的api中肯定是不可取的,對同一資源有些角色可能只要查詢的權限而沒有修改增加的權限。
可能會說在jwt中再增加權限列就好了嘛,但是在基于用戶-資源的授權模型中,雖然能判別是不同的請求,但是太麻煩了,對每個資源我們都要設計對應的權限列然后再塞入到jwt中,對每個用戶都要單獨授權資源這也是不可取的。
對shiro的改造這里自定義了一些規則:
shiro過濾器鏈的url=url+"=="+httpMethod
eg:對于url="api/resource/",httpMethod="GET"的資源,其拼接出來的過濾器鏈匹配url=api/resource==GET
這樣對相同的url而不同的訪問方式,會判定為不同的資源,即資源不再簡單是url,而是url和httpMethod的組合?;诮巧氖跈嗄P椭?#xff0c;角色所擁有的資源形式為url+"=="+httpMethod。
這里改變了過濾器的過濾匹配url規則,重寫PathMatchingFilterChainResolver的getChain方法,增加對上述規則的url的支持。
重寫PathMatchingFilter的路徑匹配方法pathsMatch(),加入httpMethod支持。
/* ** @Author tomsun28* @Description 重寫過濾鏈路徑匹配規則,增加REST風格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*/@Overrideprotected boolean pathsMatch(String path, ServletRequest request) {String requestURI = this.getPathWithinApplication(request);// path: url==method eg: http://api/menu==GET 需要解析出path中的url和httpMethodString[] strings = path.split("==");if (strings.length <= 1) {// 分割出來只有URLreturn this.pathsMatch(strings[0], requestURI);} else {// 分割出url+httpMethod,判斷httpMethod和request請求的method是否一致,不一致直接falseString 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();}@Overrideprotected 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);}}}} }上面是一些核心的代碼片段,更多請看項目代碼。
對用戶賬戶登錄注冊的過濾filter:PasswordFilter
/* ** @Author tomsun28* @Description 基于 用戶名密碼 的認證過濾器* @Date 20:18 2018/2/10*/ public class PasswordFilter extends AccessControlFilter {private static final Logger LOGGER = LoggerFactory.getLogger(PasswordFilter.class);private StringRedisTemplate redisTemplate;@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {Subject subject = getSubject(request,response);// 如果其已經登錄,再此發送登錄請求if(null != subject && subject.isAuthenticated()){return true;}// 拒絕,統一交給 onAccessDenied 處理return false;}@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {// 判斷若為獲取登錄注冊加密動態秘鑰請求if (isPasswordTokenGet(request)) {//動態生成秘鑰,redis存儲秘鑰供之后秘鑰驗證使用,設置有效期5秒用完即丟棄String tokenKey = CommonUtil.getRandomString(16);try {redisTemplate.opsForValue().set("PASSWORD_TOKEN_KEY_"+request.getRemoteAddr().toUpperCase(),tokenKey,5, TimeUnit.SECONDS);// 動態秘鑰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);// 動態秘鑰response返回給前端Message message = new Message();message.ok(1000,"issued tokenKey fail");RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);}return false;}// 判斷是否是登錄請求if(isPasswordLoginPost(request)){AuthenticationToken authenticationToken = createPasswordToken(request);Subject subject = getSubject(request,response);try {subject.login(authenticationToken);//登錄認證成功,進入請求派發json web token url資源內return true;}catch (AuthenticationException e) {LOGGER.warn(authenticationToken.getPrincipal()+"::"+e.getMessage(),e);// 返回response告訴客戶端認證失敗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告訴客戶端認證失敗Message message = new Message().error(1002,"login fail");RequestResponseUtil.responseWrite(JSON.toJSONString(message),response);return false;}}// 判斷是否為注冊請求,若是通過過濾鏈進入controller注冊if (isAccountRegisterPost(request)) {return true;}// 之后添加對賬戶的找回等// response 告知無效請求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");Map<String ,String> map = 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<String ,String> 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<String ,String> 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風格的jwt鑒權filter:BJwtFilter
/* ** @Author tomsun28* @Description 支持restful url 的過濾鏈 JWT json web token 過濾器,無狀態驗證* @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認證請求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過期if (e.getMessage().equals("expiredJwt")) {// 這里初始方案先拋出令牌過期,之后設計為在Redis中查詢當前appId對應令牌,其設置的過期時間是JWT的兩倍,此作為JWT的refresh時間// 當JWT的有效時間過期后,查詢其refresh時間,refresh時間有效即重新派發新的JWT給客戶端,// refresh也過期則告知客戶端JWT時間過期重新認證// 當存儲在redis的JWT沒有過期,即refresh time 沒有過期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)) {// 重新申請新的JWT// 根據appId獲取其對應所擁有的角色(這里設計為角色對應資源,沒有權限對應資源)String roles = accountService.loadAccountRole(appId);long refreshPeriodTime = 36000L; //seconds為單位,10 hoursString newJwt = JsonWebTokenUtil.issueJWT(UUID.randomUUID().toString(),appId,"token-server",refreshPeriodTime >> 2,roles,null, SignatureAlgorithm.HS512);// 將簽發的JWT存儲到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時間失效過期,jwt refresh time失效 返回jwt過期客戶端重新登錄Message message = new Message().error(1006,"expired jwt");RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);return false;}}// 其他的判斷為JWT錯誤無效Message message = new Message().error(1007,"error Jwt");RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);return false;}catch (Exception e) {// 其他錯誤LOGGER.warn(servletRequest.getRemoteAddr()+"JWT認證"+e.getMessage(),e);// 告知客戶端JWT錯誤1005,需重新登錄申請jwtMessage message = new Message().error(1007,"error jwt");RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);return false;}}else {// 請求未攜帶jwt 判斷為無效請求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);// 未認證的情況if (null == subject || !subject.isAuthenticated()) {// 告知客戶端JWT認證失敗需跳轉到登錄頁面Message message = new Message().error(1006,"error jwt");RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);}else {// 已經認證但未授權的情況// 告知客戶端JWT沒有權限訪問此資源Message message = new Message().error(1008,"no permission");RequestResponseUtil.responseWrite(JSON.toJSONString(message),servletResponse);}// 過濾鏈終止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) {Map<String,String> maps = 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);}// 驗證當前用戶是否屬于mappedValue任意一個角色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()));}// 驗證當前用戶是否擁有mappedValue任意一個權限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數據源,數據提供service,匹配matchs,自定義token,spring集成shiro配置等其他詳見項目代碼。
最后項目實現了基于jwt的動態restful api權限認證。
效果展示
github:
bootshiro
usthe
碼云:
bootshiro
usthe
分享一波阿里云代金券快速上云
轉載請注明 from tomsun28
總結
以上是生活随笔為你收集整理的基于shiro的改造集成真正支持restful请求的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: eclipse 配置打开工作空间
- 下一篇: Java多线程专题一:并发所面临的问题