1 Star 0 Fork 32

freestylewill / micro-svc

forked from yuxue / micro-svc 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
README
BSD-3-Clause

micro-svc

介绍

spring cloud + shiro框架; 博客地址: 手把手教你集成spring cloud + shiro微服务框架

背景

假设我们有很多java实现的项目,认证授权用的是shiro框架,可能还有一个sso单点登录平台

突然有一天,你的项目经理说要做微服务 :joy:

然后,你就给了你领导很多建议,什么dubbo、什么spring cloud等等;涉及的内容可能方方面面

但是! :sweat_smile: 该项目经理说:小明,你晚上加加班,花点时间来改造一下现有的项目就好了,我们现有的项目改造起来也不是很麻烦,另外,项目改造微服务不能影响原有的项目计划进度哦 :smile: 此时,你的心里万马奔腾

目标

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

方案设计

整体方案设计:

在这里插入图片描述

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

认证授权流程

在这里插入图片描述

  • 在网关处配置支持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

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

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

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


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<String> urlSet;
   // 请求资源类型白名单,不校验登录,在application-url配置
   private static Set<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, List<String>> requestQueryParams = ctx.getRequestQueryParams();
   	if (requestQueryParams==null) {
   		requestQueryParams=new HashMap<>();
   	}

   	//将要新增的参数添加进去,被调用的微服务可以直接 去取,就想普通的一样,框架会直接注入进去
   	ArrayList<String> 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<String> urlSet) {
   	this.urlSet = urlSet;
   }

   @Value("${whitelist.fileset}")
   public void setFileSet(Set<String> fileSet) {
   	this.fileSet = fileSet;
   }
}

详细代码,请下载附件查看

auth认证授权

用户登录认证、访问授权、会话管理等;开放接口给gateway的AuthFilter使用; 关键代码

	/**
	 * 权限判断接口:先查询到资源对应的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<Integer> 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<String, Integer> value, String url) {
		Integer result = 0;
		if(null != value && value.size() > 0) {
			Set<Integer> 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模块,供其他模块使用。 项目代码架构
  • 项目运行注册中心截图: 在这里插入图片描述
  • 接口调用演示: 在这里插入图片描述

在这里插入图片描述

  • auth服务接口文档,需要先登录才能打开: 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

其他说明

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

代码下载地址

spring cloud + shiro集成方案.zip

BSD 3-Clause License Copyright (c) 2020, yuxue All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

简介

spring cloud + shiro框架 展开 收起
Java
BSD-3-Clause
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
Java
1
https://gitee.com/freestylewill/micro-svc.git
git@gitee.com:freestylewill/micro-svc.git
freestylewill
micro-svc
micro-svc
master

搜索帮助