这个模块分离至项目[api权限管理系统与先后端分离实践]api权限管理系统与先后端分离实践,感受那样太长了找不到重点,分离出来要好点。html
首先说明设计的这个安全体系是是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)。 java
可是这样感受不利于基于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中确定是不可取的,对同一资源有些角色可能只要查询的权限而没有修改增长的权限。 git
可能会说在jwt中再增长权限列就行了嘛,可是在基于用户-资源的受权模型中,虽然能判别是不一样的请求,可是太麻烦了,对每一个资源咱们都要设计对应的权限列而后再塞入到jwt中,对每一个用户都要单独受权资源这也是不可取的。 github
对shiro的改造这里自定义了一些规则:
shiro过滤器链的url=url+"=="+httpMethod
eg:对于url="api/resource/",httpMethod="GET"
的资源,其拼接出来的过滤器链匹配url=api/resource==GET
这样对相同的url而不一样的访问方式,会断定为不一样的资源,即资源再也不简单是url,而是url和httpMethod的组合。基于角色的受权模型中,角色所拥有的资源形式为url+"=="+httpMethod
。
这里改变了过滤器的过滤匹配url规则,重写PathMatchingFilterChainResolver的getChain方法,增长对上述规则的url的支持。web
/* * * @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请求的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支持。redis
/* * * @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 */ @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) { // 分割出来只有URL return this.pathsMatch(strings[0], requestURI); } else { // 分割出url+httpMethod,判断httpMethod和request请求的method是否一致,不一致直接false String httpMethod = WebUtils.toHttp(request).getMethod().toUpperCase(); return httpMethod.equals(strings[1].toUpperCase()) && this.pathsMatch(strings[0], requestURI); } } }
这样增长httpMethod的改造就完成了,重写ShiroFilterFactoryBean使其使用改造后的chainResolver:RestPathMatchingFilterChainResolverspring
/* * * @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); } } } } }
上面是一些核心的代码片断,更多请看项目代码。 json
对用户帐户登陆注册的过滤filter:PasswordFiltersegmentfault
/* * * @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; @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { Subject subject = getSubject(request,response); // 若是其已经登陆,再此发送登陆请求 if(null != subject && subject.isAuthenticated()){ return true; } // 拒绝,统一交给 onAccessDenied 处理 return false; } @Override protected 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 hours String 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,需从新登陆申请jwt Message 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权限认证。
分享一波阿里云代金券快速上云
转载请注明 from tomsun28