原文链接:
https://www.freesion.com/article/7246187280/

背景

假设我们有很多java实现的项目,认证授权用的是shiro框架,可能还有一个sso单点登录平台
突然有一天,你的项目经理说要做微服务,可能这个项目经理并不懂什么是微服务 😂
然后,你就给了你领导很多建议,什么dubbo、什么spring cloud等等;涉及的内容可能方方面面
但是! 😅
该项目经理说:小明,你晚上加加班,花点时间来改造一下现有的项目就好了,我们现有的项目改造起来也不是很麻烦,另外,项目改造微服务不能影响原有的项目计划进度哦 😄
此时,你的心里万马奔腾 🐎🐎🐎🐎🐎🐎

目标

总的来说一句话:用最少的工作量,改造基于shiro安全框架的微服务项目,实现spring cloud + shiro 框架集成
PS:当前博客描述的方案是小编根据公司实际情况设计实现、并且在生产环境正常运行的方案,可能一些设计不太合理,又或者不满足你的需求,但是,这个方案还是有借鉴意义的。觉得有用的,给个评论点个赞鼓励下。

方案设计

整体方案设计:

手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图1

  • zuul网关服务,主要用于同一系统的访问出入口;
  • zuul实现一个filter,用于过滤所有的请求,校验登录状态及权限;认证:校验用户是否登录;授权:校验用户是否有权限
  • service-auth服务,主要实现认证、授权功能,因为所有的请求都需要经过该服务,所以集成的功能不能太多;必须要保证该模块高可用;该服务,使用feign的方式开发接口,提供给zuul调用
  • service-auth服务,集成shiro-redis安全框架,其他服务模块可以不用集成shiro安全框架,如果非要集成也是可以的,但是要解决shiro的会话共享问题;

    认证授权流程

    手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图2

  • 在网关处配置支持https协议请求,则所有的服务均可以同时支持http、https协议请求

  • 优先从cookie获取会话ID,同时需要支持token参数方式校验,因为在做公众号登录的时候,要求在后端登录接口进行重定向,所以需要提供token参数确定客户端身份
  • 授权功能,通过url鉴权;服务名称、请求路径、请求方式三者确定唯一;当然也可以使用requirePermissions,根据自己的需要吧,我这里是根据公司项目实际需求根据url来识别权限的。

    方案实现

    版本:spring boot 2.1.5.RELEASE spring cloud Greenwich.SR2 jdk1.8以上 postgresql-10 redis-2.8.17

    EUREKA注册中心

    简简单单的一个注册中心,没有啥特殊的配置。
    启动类 Application.java
    1. import org.springframework.boot.SpringApplication;
    2. import org.springframework.boot.autoconfigure.SpringBootApplication;
    3. import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
    4. @SpringBootApplication
    5. @EnableEurekaServer
    6. public class Application {
    7. public static void main(String[] args) {
    8. SpringApplication.run(Application.class, args);
    9. }
    10. }
    11. 1
    12. 2
    13. 3
    14. 4
    15. 5
    16. 6
    17. 7
    18. 8
    19. 9
    20. 10
    21. 11
    配置 application.yml
    1. server:
    2. port: 7001
    3. spring:
    4. application:
    5. name: eureka
    6. main:
    7. allow-bean-definition-overriding: true
    8. eureka:
    9. instance:
    10. prefer-ip-address: true
    11. #hostname: svc-eureka #eureka服务端的实例名称
    12. instance-id: ${spring.cloud.client.ip-address}:${server.port}
    13. server:
    14. enable-self-preservation: false ## 中小规模下,自我保护模式坑比好处多,所以关闭它
    15. #renewal-threshold-update-interval-ms: 120000 ## 心跳阈值计算周期,如果开启自我保护模式,可以改一下这个配置
    16. eviction-interval-timer-in-ms: 5000 ## 主动失效检测间隔,配置成5秒
    17. use-read-only-response-cache: false ## 禁用readOnlyCacheMap
    18. client:
    19. register-with-eureka: false #false表示不向注册中心注册自己。
    20. fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    21. service-url:
    22. defaultZone: http://${spring.cloud.client.ip-address}:${server.port}/eureka/
    23. info:
    24. app.name: eureka
    25. company.name: test.com
    26. build.artifactId: "@project.artifactId@"
    27. build.version: "@project.version@"
    28. 1
    29. 2
    30. 3
    31. 4
    32. 5
    33. 6
    34. 7
    35. 8
    36. 9
    37. 10
    38. 11
    39. 12
    40. 13
    41. 14
    42. 15
    43. 16
    44. 17
    45. 18
    46. 19
    47. 20
    48. 21
    49. 22
    50. 23
    51. 24
    52. 25
    53. 26
    54. 27
    详细代码,请下载附件查看

    ZUUL网关

    在网关实现了一个AuthFilter,用于过滤所有的请求,判断是否登录、是否有权限;
    支持配置免登陆请求地址、免授权地址、兼容cookie跟token参数校验等。
    兼容web端登陆、小程序、公众号登录等。
    启动类 Application.java
    1. import org.springframework.boot.SpringApplication;
    2. import org.springframework.boot.autoconfigure.SpringBootApplication;
    3. import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    4. import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    5. import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
    6. import org.springframework.cloud.openfeign.EnableFeignClients;
    7. @SpringBootApplication
    8. @EnableDiscoveryClient
    9. @EnableZuulProxy
    10. @EnableFeignClients
    11. @EnableEurekaClient
    12. public class Application {
    13. public static void main(String[] args) {
    14. SpringApplication.run(Application.class, args);
    15. }
    16. }
    17. 1
    18. 2
    19. 3
    20. 4
    21. 5
    22. 6
    23. 7
    24. 8
    25. 9
    26. 10
    27. 11
    28. 12
    29. 13
    30. 14
    31. 15
    32. 16
    33. 17
    关键代码 AuthFilter.java ``` import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.alibaba.fastjson.JSONObject; import com.fundway.auth.api.LoginCheckApi; import com.google.common.collect.Maps; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; /**
  • 自定义过滤器,向下游服务请求加header认证信息.
  • 与敏感头(设置向内部服务不传递哪些header正好相反),
  • 这种方式好像不能传递名称为 Authorization,Cookie,Set-Cookie 的请求头,这三个传递不到下游服务,这三个由敏感头管理,只能传递token这种自定义的头 */ @Component public class AuthFilter extends ZuulFilter{ @Autowired(required=true) private LoginCheckApi loginCheckApi; // 请求路径白名单,不校验登录,在application-url配置 private static Set urlSet; // 请求资源类型白名单,不校验登录,在application-url配置 private static Set fileSet; @Override public String filterType() {

    1. //pre型过滤器,路由到下级服务前执行
    2. return FilterConstants.PRE_TYPE;

    } @Override public int filterOrder() {

    1. //优先级,数字越大,优先级越低
    2. return 0;

    } @Override public boolean shouldFilter() {

    1. //是否执行该过滤器,true代表需要过滤
    2. return true;

    } /**

    • 过滤逻辑
    • pre过滤器在route过滤前执行,RequestContext负责通信包含了请求等信息,debug发现,context.addZuulRequestHeader,
    • 但在RibbonRoutingFilter 这个向下游服务发起请求的路由过滤器,自定义的header没有添加上。
    • RibbonRoutingFilter是默认的过滤器,run方法可以看到,逻辑是从原来的RequestContext生产新的RibbonCommandContext发起请求
    • @return
    • @throws ZuulException */ @Override public Object run() { //Zull的Filter链间通过RequestContext传递通信,内部采用ThreadLocal 保存每个请求的信息, //包括请求路由、错误信息、HttpServletRequest、response等 RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = this.getHttpServletRequest(); // option请求,直接放行 if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {

      1. return null;

      } // 判断需要放行的url或者静态资源文件 String url = request.getRequestURI(); String end = “”; if(url.lastIndexOf(“/“) >= 0 ) { // 判断需要放行的请求

      1. end = url.substring(url.lastIndexOf("/"));
      2. if(urlSet.contains(end)) {
      3. return null;
      4. }

      } if(end.lastIndexOf(“.”) > 0) { //判断需要放行的静态文件

      1. end = end.substring(end.lastIndexOf(".") + 1);
      2. if(fileSet.contains(end)) {
      3. return null;
      4. }

      } // 获取到用户的Token String cookie = request.getHeader(“Cookie”); //获取到 JSESSIONID=值 if(StringUtils.isEmpty(cookie)) {

      1. cookie = "";

      } String token = ctx.getRequest().getParameter(“token”); //获取到 值 // 处理微信公众号登录业务,后端会重定向,生成的cookie是一个无效cookie,而后端重定向,又不能把有效cookie写到客户端 if(!StringUtils.isEmpty(token) && !”undefined”.equals(token) && !cookie.contains(token)) {

      1. cookie = "JSESSIONID=" + (ctx.getRequest().getParameter("token"));

      } if(StringUtils.isEmpty(token)) { // 参数未空或者null的话,feign调用的接口会报错!!坑比

      1. token = "";

      } //过滤该请求,不往下级服务去转发请求,到此结束
      if(StringUtils.isEmpty(cookie)) { // 会报跨域问题

      1. this.setCORS(ctx);
      2. ctx.setSendZuulResponse(false);
      3. ctx.setResponseStatusCode(200);
      4. Map<String, Object> result = Maps.newHashMap();
      5. result.put("code", 401);
      6. result.put("msg", "未登录");
      7. result.put("obj", "来自网关的消息:未获取到有效的Token");
      8. result.put("success", false);
      9. ctx.setResponseBody(JSONObject.toJSONString(result));
      10. ctx.getResponse().setContentType("text/html;charset=UTF-8");
      11. return null;

      } // 增加请求头 ctx.addZuulRequestHeader(“Cookie”, cookie); // 调用统一认证接口,判断是否登录 && 判断是否有功能权限 // 优先校验cookie,,不通过则校验token //cookie从request里面拿 Object check = loginCheckApi.checkPermission(token, this.getUrl(request)); if(check instanceof HashMap) {

      1. HashMap<String, Object> result = (HashMap) check;
      2. if(Boolean.parseBoolean(result.get("success").toString())) {
      3. // 添加序列化之后的用户信息
      4. // 白名单url的请求,不能获取到该信息
      5. setReqParams(ctx, request, "userEntity", result.get("obj").toString());
      6. return null;
      7. }
      8. this.setCORS(ctx);
      9. ctx.setSendZuulResponse(false);
      10. ctx.setResponseStatusCode(200);
      11. // 权限校验接口异常
      12. ctx.setResponseBody(JSONObject.toJSONString(check));
      13. ctx.getResponse().setContentType("text/html;charset=UTF-8");
      14. return null;

      } else {

      1. this.setCORS(ctx);
      2. ctx.setSendZuulResponse(false);
      3. ctx.setResponseStatusCode(200);
      4. Map<String, Object> result = Maps.newHashMap();
      5. result.put("code", 401);
      6. result.put("msg", "无权限");
      7. result.put("obj", "来自网关的消息:该用户无当前请求权限");
      8. result.put("success", false);
      9. ctx.setResponseBody(JSONObject.toJSONString(result));
      10. ctx.getResponse().setContentType("text/html;charset=UTF-8");
      11. return null;

      } } private String getUrl(HttpServletRequest request) { // 获取到请求的相关数据 uri是斜杠开头 String uri = request.getRequestURI().toLowerCase().replaceAll(“//“, “/“); String method = request.getMethod().toLowerCase(); return method.concat(uri); } private HttpServletRequest getHttpServletRequest() { try {

      1. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
      2. HttpServletRequest request = attributes.getRequest();
      3. return request;

      } catch (Exception e) {

      1. e.printStackTrace();
      2. return null;

      } } public static void setReqParams(RequestContext ctx, HttpServletRequest request, String key, String value) { // 一定要get一下,下面这行代码才能取到值… [注1] request.getParameterMap(); Map> requestQueryParams = ctx.getRequestQueryParams(); if (requestQueryParams==null) {

      1. requestQueryParams=new HashMap<>();

      } //将要新增的参数添加进去,被调用的微服务可以直接 去取,就想普通的一样,框架会直接注入进去 ArrayList arrayList = new ArrayList<>(); arrayList.add(value); requestQueryParams.put(key, arrayList); ctx.setRequestQueryParams(requestQueryParams); } private void setCORS(RequestContext ctx) { //处理跨域问题 HttpServletRequest request = ctx.getRequest(); HttpServletResponse response = ctx.getResponse(); // 这些是对请求头的匹配,网上有很多解释 response.setHeader(“Access-Control-Allow-Origin”,request.getHeader(“Origin”)); response.setHeader(“Access-Control-Allow-Credentials”,”true”); response.setHeader(“Access-Control-Allow-Methods”,”GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH”); response.setHeader(“Access-Control-Allow-Headers”,”authorization, content-type”); response.setHeader(“Access-Control-Expose-Headers”,”X-forwared-port, X-forwarded-host”); response.setHeader(“Vary”,”Origin,Access-Control-Request-Method,Access-Control-Request-Headers”); } @Value(“${whitelist.urlset}”) public void setUtlSet(Set urlSet) { this.urlSet = urlSet; } @Value(“${whitelist.fileset}”) public void setFileSet(Set fileSet) { this.fileSet = fileSet; } } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21 • 22 • 23 • 24 • 25 • 26 • 27 • 28 • 29 • 30 • 31 • 32 • 33 • 34 • 35 • 36 • 37 • 38 • 39 • 40 • 41 • 42 • 43 • 44 • 45 • 46 • 47 • 48 • 49 • 50 • 51 • 52 • 53 • 54 • 55 • 56 • 57 • 58 • 59 • 60 • 61 • 62 • 63 • 64 • 65 • 66 • 67 • 68 • 69 • 70 • 71 • 72 • 73 • 74 • 75 • 76 • 77 • 78 • 79 • 80 • 81 • 82 • 83 • 84 • 85 • 86 • 87 • 88 • 89 • 90 • 91 • 92 • 93 • 94 • 95 • 96 • 97 • 98 • 99 • 100 • 101 • 102 • 103 • 104 • 105 • 106 • 107 • 108 • 109 • 110 • 111 • 112 • 113 • 114 • 115 • 116 • 117 • 118 • 119 • 120 • 121 • 122 • 123 • 124 • 125 • 126 • 127 • 128 • 129 • 130 • 131 • 132 • 133 • 134 • 135 • 136 • 137 • 138 • 139 • 140 • 141 • 142 • 143 • 144 • 145 • 146 • 147 • 148 • 149 • 150 • 151 • 152 • 153 • 154 • 155 • 156 • 157 • 158 • 159 • 160 • 161 • 162 • 163 • 164 • 165 • 166 • 167 • 168 • 169 • 170 • 171 • 172 • 173 • 174 • 175 • 176 • 177 • 178 • 179 • 180 • 181 • 182 • 183 • 184 • 185 • 186 • 187 • 188 • 189 • 190 • 191 • 192 • 193 • 194 • 195 • 196 • 197 • 198 • 199 • 200 • 201 • 202 • 203 • 204 • 205 • 206 • 207 • 208 • 209 • 210 • 211 • 212 • 213 • 214 • 215 • 216 • 217 • 218 • 219 • 220 • 221 • 222

      1. 详细代码,请下载附件查看
      2. <a name="zN0TW"></a>
      3. ## AUTH认证授权
      4. 用户登录认证、访问授权、会话管理等;开放接口给gatewayAuthFilter使用;<br />关键代码

      /**

      • 权限判断接口:先查询到资源对应的id,然后根据用户权限判断 */ @Override public Result checkPermission(HttpServletRequest request, String cookie, String checkUrl) { UserEntity user = this.getUserInfo(request, cookie); if(null == user || user.getId() <=0) {

        1. return Result.error("未登录", 401);

        } // 获取用户功能权限ID集合 Set permissionSet = user.getPermissionId(); // 减少放到请求中的属性 user.setPermission(null); user.setPermissionId(null); // 获取微服务名称 String[] str = checkUrl.split(“/“); String module = str[1];

        // 判断是否是免校验资源 if(this.getIdByUrl(unCheckResMap.get(module), checkUrl) > 0) {

        1. return Result.ok(JSONObject.toJSONString(user));

        } // 用户完全没有权限, 且请求资源不是开放资源 if(null == permissionSet || permissionSet.size() <= 0) {

        1. log.info("当前用户未分配权限:" + user.getLoginName());
        2. return Result.error("无权限", 401);

        } // 获取系统指定模块资源 Integer resId = this.getIdByUrl(resMap.get(module), checkUrl);

        // 系统没有配置该权限,或者请求路径不存在 if(resId <= 0 && isPass) {

        1. // log.info("系统没有配置该资源对应的权限, 但是配置放行:" + uri);
        2. return Result.ok(JSONObject.toJSONString(user));

        }

        // 系统配置了权限 if(permissionSet.contains(resId)) {

        1. return Result.ok(JSONObject.toJSONString(user));

        } return Result.error(“无权限”, 401); }

      public Integer getIdByUrl(HashMap value, String url) { Integer result = 0; if(null != value && value.size() > 0) {

      1. Set<Integer> resultSet = Sets.newHashSet();
      2. if(value.containsKey(url)) {
      3. result = value.get(url);
      4. } else {
      5. // 遍历,匹配,处理@PathVariable注解的请求
      6. value.entrySet().forEach(entry -> {
      7. String key1 = entry.getKey();
      8. if(key1.contains("{")) {
      9. AntPathMatcher matcher = new AntPathMatcher();
      10. if(matcher.match(key1, url)) {
      11. resultSet.add(entry.getValue());
      12. }
      13. }
      14. });
      15. }
      16. if(resultSet.size() > 0) {
      17. result = resultSet.stream().findFirst().get();
      18. }

      } return result; } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21 • 22 • 23 • 24 • 25 • 26 • 27 • 28 • 29 • 30 • 31 • 32 • 33 • 34 • 35 • 36 • 37 • 38 • 39 • 40 • 41 • 42 • 43 • 44 • 45 • 46 • 47 • 48 • 49 • 50 • 51 • 52 • 53 • 54 • 55 • 56 • 57 • 58 • 59 • 60 • 61 • 62 • 63 • 64 • 65 • 66 • 67 • 68 • 69 • 70 • 71 • 72 • 73 ```

  • 认证服务,主要通过服务名称+请求方式+请求url来判定唯一的权限,比如post/service-demo1/user/getSystemUserInfo 其中post是请求方式,service-demo1是服务名称,/user/getSystemUserInfo是接口请求路径;兼容如 {id} 由@PathVariable注解标注的请求。
  • 当然也可以使用shiro的权限编码方式,如 user:getSystemUserInfo ,但是使用这种方式,需要处理好多个服务权限集中管理,权限编码不能冲突的问题。
    详细代码,请下载附件查看

    相关截图

  • project结构:auth:认证服务;auth-api:使用feign开放的接口声明;demo:微服务项目demo,不集成shiro;demo1:微服务项目demo,集成shiro;shiro:独立出来的shiro模块,供其他模块使用。
    手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图3

  • 项目运行注册中心截图:
    手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图4
  • 接口调用演示:
    手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图5

手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图6

  • auth服务接口文档,需要先登录才能打开:
    手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图7

手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图8
手把手教你集成SPRING CLOUD + SHIRO微服务框架 - 图9

其他说明

  • 子系统后端开发过程中,不需要将服务注册eureka;提交前端对接、或者测试部署服务器的时候注册即可
    因为如果每位后端开发,都将服务组册到eureka,如果服务名称相同,在服务端会产生负载均衡,访问的接口,不一定是本地的接口,也可能是别人的接口
    开发过程不控制权限,发布测试环境后,统一管理权限;仅需关注如何获取登录用户信息即可
  • 集成shiro
    自行实现登录接口,产生本地会话,从而实现获取用户信息;有现成案例参考,复制粘贴即可,几乎不用考虑工作量问题;
    部署的时候,使用redis缓存共享会话即可;
    具体实现,请查看示例项目代码demo1
  • 不集成shiro
    网关校验登录成功之后,转发请求的过程,会把用户登录信息携带转发;具体的服务项目,直接通过参数名称获取即可;
    具体实现,请查看示例项目代码demo

    代码下载地址

    spring cloud + shiro集成方案.zip
    版权声明:本文为weixin_42686388原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
    本文链接:https://blog.csdn.net/weixin_42686388/article/details/103084289