基于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的组合。基于角色的授权模型中,角色所拥有的资源形式为url+"=="+httpMethod
这里改变了过滤器的过滤匹配url规则,重写PathMatchingFilterChainResolver的getChain方法,增加对上述规则的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*/@Overridepublic 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是否一致,不一致直接falseif (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 重写过滤链路径匹配规则,增加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请求相关推荐

  1. 基于shiro+jwt的真正rest url权限管理,前后端分离

    代码地址如下: http://www.demodashi.com/demo/13277.html bootshiro & usthe bootshiro是基于springboot+shiro+ ...

  2. 分享一个基于 Shiro 的权限管理系统,亮点是支持 restful 风格 URL.

    Shiro-Action 本项目使用 Spring Boot 搭建, 用于加深对 Spring Boot 与 Shiro 的学习, 项目特色是支持 restful 风格权限控制, 支持对同一 URL, ...

  3. shiro教程(4)-shiro与项目集成开发

    1 shiro与项目集成开发 这里我们主要以用户登录的例子来演示,先给出一个时序图: 点击打开链接(点击查看) 1.1 shiro与spring web项目整合 shiro与springweb项目整合 ...

  4. 如何在基于Bytom开发过程中集成IPFS

    本文介绍了基于Bytom开发过程中集成IPFS. step1: 搭建bytom节点 比原相关资料:https://github.com/Bytom-Community/Bytom_Docs 搭建byt ...

  5. Spring Boot 集成 Swagger 生成 RESTful API 文档

    原文链接: Spring Boot 集成 Swagger 生成 RESTful API 文档 简介 Swagger 官网是这么描述它的:The Best APIs are Built with Swa ...

  6. 项目一:第十二天 1、常见权限控制方式 2、基于shiro提供url拦截方式验证权限 3、在realm中授权 5、总结验证权限方式(四种) 6、用户注销7、基于treegrid实现菜单展示...

    1 课程计划 1. 常见权限控制方式 2. 基于shiro提供url拦截方式验证权限 3. 在realm中授权 4. 基于shiro提供注解方式验证权限 5. 总结验证权限方式(四种) 6. 用户注销 ...

  7. 基于shiro实现session持久化和分布式共享

    前言 本文写下session持久化和分布式共享 基于shiro框架对session的管理机制来实现 必要性 一直处于登陆状态:你登陆微信 不可能三天两头就让你重新登陆吧?而是一直处于登陆状态 除非主动 ...

  8. Maven实战(四)——基于Maven的持续集成实践

    相信很多读者和我一样,最早接触到持续集成的概念是来自Martin的著名文章<持续集成>,该文最早发布于2000年9月,之后在2006年进行了一次修订,它清晰地解释了持续集成的概念,并总结了 ...

  9. 集成OpenLDAP与Kerberos实现统一认证(三):基于SASL/GSSAPI深度集成

    文章目录 1. 写作背景 2. 既定目标 3. 重要概念 3.1 SASL 3.2 GSSAPI 3.3 SASL与GSSAPI的关系 3.4 saslauthd 3.5 Kerberos化 4. 核 ...

最新文章

  1. React组件通信技巧
  2. matlab 非线性曲线拟合
  3. Sublime Text for Windows的快捷键
  4. httos双向认证配置_idou老师教你学Istio 15:Istio实现双向TLS的迁移
  5. macos mysql8_macOS + MySql8 问题
  6. Mysql在window下的表现_Mysql在windows系统下的配置
  7. Linux命令 ls -l s输出内容含义详解
  8. Java Tomcat SSL 服务端/客户端双向认证
  9. 拉普拉斯变换学习笔记
  10. 微动探测原理及仪器介绍
  11. DolphinScheduler 调度系统
  12. 禁止百度云盘p2p后台上传
  13. 医疗行业缩写所表示含义
  14. 谷歌金山词霸/搜狗、QQ、google输入法模式分析及展望
  15. 64位 计算机 最大内存,Windows32位/64位系统最大支持多大内存及不支持的原因
  16. GTD时间管理-节假日时间安排 | 每天成就更大成功
  17. pytorch训练时前一次迭代的loss正常后一次迭代却报nan
  18. 基于OpenCV的混凝土裂纹检测
  19. 蔚来手机一年内发布/ 微信内测图片视频拖动一键转发/ 马斯克携推特威胁苹果...今日更多新鲜事在此...
  20. 关于金蝶K3WISE【15.0、15.1】销售出库单购货单位不显示问题

热门文章

  1. html5媒体对象居中,媒体对象 - Media Objects
  2. Linux脚本关联,shell数组和关联数组
  3. led灯条维修_常见的LED透明屏型号规格,影响LED透明屏价格因素
  4. java.lang.IllegalArgumentException: pointerIndex out of range
  5. ClassLoader工作机制
  6. 如何使用数据库SCHEDULER来执行清归档脚本
  7. linux基本网络IP自动、手动配置
  8. php 域名验证系统_PHP授权验证系统(域名+IP双重验证一键更新授权系统)
  9. px word 表格宽度_word怎样批量修改表格的宽度(2)
  10. 时间排序python_算法导论 第八章 线性时间排序(python)