# micro-svc **Repository Path**: overload__hcf/micro-svc ## Basic Information - **Project Name**: micro-svc - **Description**: spring cloud + shiro框架 - **Primary Language**: Java - **License**: BSD-3-Clause - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 32 - **Created**: 2021-12-20 - **Last Updated**: 2021-12-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # micro-svc #### 介绍 spring cloud + shiro框架; 博客地址: [手把手教你集成spring cloud + shiro微服务框架](https://blog.csdn.net/weixin_42686388/article/details/103084289) ## 背景 假设我们有很多java实现的项目,认证授权用的是shiro框架,可能还有一个sso单点登录平台 突然有一天,你的项目经理说要做微服务 :joy: 然后,你就给了你领导很多建议,什么dubbo、什么spring cloud等等;涉及的内容可能方方面面 但是! :sweat_smile: 该项目经理说:小明,你晚上加加班,花点时间来改造一下现有的项目就好了,我们现有的项目改造起来也不是很麻烦,另外,项目改造微服务不能影响原有的项目计划进度哦 :smile: 此时,你的心里万马奔腾 ## 目标 总的来说一句话:用最少的工作量,改造基于shiro安全框架的微服务项目,实现spring cloud + shiro 框架集成 PS:**当前博客描述的方案是小编根据公司实际情况设计实现、并且在生产环境正常运行的方案**,可能一些设计不太合理,又或者不满足你的需求,但是,这个方案还是有借鉴意义的。觉得有用的,给个评论点个赞鼓励下。 # 方案设计 ## 整体方案设计: ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_20d669fd_1855162.png) - zuul网关服务,主要用于同一系统的访问出入口; - zuul实现一个filter,用于过滤所有的请求,校验登录状态及权限;认证:校验用户是否登录;授权:校验用户是否有权限 - service-auth服务,主要实现认证、授权功能,因为所有的请求都需要经过该服务,所以集成的功能不能太多;必须要保证该模块高可用;该服务,使用feign的方式开发接口,提供给zuul调用 - service-auth服务,集成shiro-redis安全框架,其他服务模块可以不用集成shiro安全框架,如果非要集成也是可以的,但是要解决shiro的会话共享问题; ## 认证授权流程 ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_f644eeef_1855162.png) - 在网关处配置支持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 ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 配置 application.yml ```yaml server: port: 7001 spring: application: name: eureka main: allow-bean-definition-overriding: true eureka: instance: prefer-ip-address: true #hostname: svc-eureka #eureka服务端的实例名称 instance-id: ${spring.cloud.client.ip-address}:${server.port} server: enable-self-preservation: false ## 中小规模下,自我保护模式坑比好处多,所以关闭它 #renewal-threshold-update-interval-ms: 120000 ## 心跳阈值计算周期,如果开启自我保护模式,可以改一下这个配置 eviction-interval-timer-in-ms: 5000 ## 主动失效检测间隔,配置成5秒 use-read-only-response-cache: false ## 禁用readOnlyCacheMap client: register-with-eureka: false #false表示不向注册中心注册自己。 fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务 service-url: defaultZone: http://${spring.cloud.client.ip-address}:${server.port}/eureka/ info: app.name: eureka company.name: test.com build.artifactId: "@project.artifactId@" build.version: "@project.version@" ``` 详细代码,请下载附件查看 ## zuul网关 在网关实现了一个AuthFilter,用于过滤所有的请求,判断是否登录、是否有权限; 支持配置免登陆请求地址、免授权地址、兼容cookie跟token参数校验等。 兼容web端登陆、小程序、公众号登录等。 启动类 Application.java ```java import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; import org.springframework.cloud.openfeign.EnableFeignClients; @SpringBootApplication @EnableDiscoveryClient @EnableZuulProxy @EnableFeignClients @EnableEurekaClient public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 关键代码 AuthFilter.java ```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() { //pre型过滤器,路由到下级服务前执行 return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { //优先级,数字越大,优先级越低 return 0; } @Override public boolean shouldFilter() { //是否执行该过滤器,true代表需要过滤 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())) { return null; } // 判断需要放行的url或者静态资源文件 String url = request.getRequestURI(); String end = ""; if(url.lastIndexOf("/") >= 0 ) { // 判断需要放行的请求 end = url.substring(url.lastIndexOf("/")); if(urlSet.contains(end)) { return null; } } if(end.lastIndexOf(".") > 0) { //判断需要放行的静态文件 end = end.substring(end.lastIndexOf(".") + 1); if(fileSet.contains(end)) { return null; } } // 获取到用户的Token String cookie = request.getHeader("Cookie"); //获取到 JSESSIONID=值 if(StringUtils.isEmpty(cookie)) { cookie = ""; } String token = ctx.getRequest().getParameter("token"); //获取到 值 // 处理微信公众号登录业务,后端会重定向,生成的cookie是一个无效cookie,而后端重定向,又不能把有效cookie写到客户端 if(!StringUtils.isEmpty(token) && !"undefined".equals(token) && !cookie.contains(token)) { cookie = "JSESSIONID=" + (ctx.getRequest().getParameter("token")); } if(StringUtils.isEmpty(token)) { // 参数未空或者null的话,feign调用的接口会报错!!坑比 token = ""; } //过滤该请求,不往下级服务去转发请求,到此结束 if(StringUtils.isEmpty(cookie)) { // 会报跨域问题 this.setCORS(ctx); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(200); Map result = Maps.newHashMap(); result.put("code", 401); result.put("msg", "未登录"); result.put("obj", "来自网关的消息:未获取到有效的Token"); result.put("success", false); ctx.setResponseBody(JSONObject.toJSONString(result)); ctx.getResponse().setContentType("text/html;charset=UTF-8"); return null; } // 增加请求头 ctx.addZuulRequestHeader("Cookie", cookie); // 调用统一认证接口,判断是否登录 && 判断是否有功能权限 // 优先校验cookie,,不通过则校验token //cookie从request里面拿 Object check = loginCheckApi.checkPermission(token, this.getUrl(request)); if(check instanceof HashMap) { HashMap result = (HashMap) check; if(Boolean.parseBoolean(result.get("success").toString())) { // 添加序列化之后的用户信息 // 白名单url的请求,不能获取到该信息 setReqParams(ctx, request, "userEntity", result.get("obj").toString()); return null; } this.setCORS(ctx); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(200); // 权限校验接口异常 ctx.setResponseBody(JSONObject.toJSONString(check)); ctx.getResponse().setContentType("text/html;charset=UTF-8"); return null; } else { this.setCORS(ctx); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(200); Map result = Maps.newHashMap(); result.put("code", 401); result.put("msg", "无权限"); result.put("obj", "来自网关的消息:该用户无当前请求权限"); result.put("success", false); ctx.setResponseBody(JSONObject.toJSONString(result)); ctx.getResponse().setContentType("text/html;charset=UTF-8"); 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 { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); return request; } catch (Exception e) { e.printStackTrace(); 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) { 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; } } ``` 详细代码,请下载附件查看 ## auth认证授权 用户登录认证、访问授权、会话管理等;开放接口给gateway的AuthFilter使用; 关键代码 ```java /** * 权限判断接口:先查询到资源对应的id,然后根据用户权限判断 */ @Override public Result checkPermission(HttpServletRequest request, String cookie, String checkUrl) { UserEntity user = this.getUserInfo(request, cookie); if(null == user || user.getId() <=0) { 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) { return Result.ok(JSONObject.toJSONString(user)); } // 用户完全没有权限, 且请求资源不是开放资源 if(null == permissionSet || permissionSet.size() <= 0) { log.info("当前用户未分配权限:" + user.getLoginName()); return Result.error("无权限", 401); } // 获取系统指定模块资源 Integer resId = this.getIdByUrl(resMap.get(module), checkUrl); // 系统没有配置该权限,或者请求路径不存在 if(resId <= 0 && isPass) { // log.info("系统没有配置该资源对应的权限, 但是配置放行:" + uri); return Result.ok(JSONObject.toJSONString(user)); } // 系统配置了权限 if(permissionSet.contains(resId)) { 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) { Set resultSet = Sets.newHashSet(); if(value.containsKey(url)) { result = value.get(url); } else { // 遍历,匹配,处理@PathVariable注解的请求 value.entrySet().forEach(entry -> { String key1 = entry.getKey(); if(key1.contains("{")) { AntPathMatcher matcher = new AntPathMatcher(); if(matcher.match(key1, url)) { resultSet.add(entry.getValue()); } } }); } if(resultSet.size() > 0) { result = resultSet.stream().findFirst().get(); } } return result; } ``` - 认证服务,主要通过`服务名称+请求方式+请求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模块,供其他模块使用。 ![项目代码架构](https://images.gitee.com/uploads/images/2020/0309/092549_09938e0d_1855162.png) - 项目运行注册中心截图: ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_c00c4446_1855162.png) - 接口调用演示: ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_e7100ddd_1855162.png) ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_4f7173dc_1855162.png) - auth服务接口文档,需要先登录才能打开: ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_cada66ee_1855162.png) ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_64cc4bfb_1855162.png) ![在这里插入图片描述](https://images.gitee.com/uploads/images/2020/0309/092549_c7f82676_1855162.png) ## 其他说明 - **子系统后端开发过程中,不需要将服务注册eureka;**提交前端对接、或者测试部署服务器的时候注册即可 因为如果每位后端开发,都将服务组册到eureka,如果服务名称相同,在服务端会产生负载均衡,访问的接口,不一定是本地的接口,也可能是别人的接口 开发过程不控制权限,发布测试环境后,统一管理权限;仅需关注如何获取登录用户信息即可 - 集成shiro 自行实现登录接口,产生本地会话,从而实现获取用户信息;有现成案例参考,复制粘贴即可,几乎不用考虑工作量问题; 部署的时候,使用redis缓存共享会话即可; 具体实现,请查看示例项目代码demo1 - 不集成shiro 网关校验登录成功之后,转发请求的过程,会把用户登录信息携带转发;具体的服务项目,直接通过参数名称获取即可; 具体实现,请查看示例项目代码demo # 代码下载地址 [spring cloud + shiro集成方案.zip](https://download.csdn.net/download/weixin_42686388/12104812)