微服务中如何传递用户信息?
前提,有了一套微服务架构的服务,然后有一个统一鉴权的服务用户中心 userService
- 访问服务A ,在A 中执行用户登录逻辑,并将用户信息临时存入到A服务中心: 例如是token; 如何存放? 我们一般是通过空间换时间的方式-- ThreadLocal 存取
- 然后服务之间调用,A服务访问B服务,当进入B服务的时候,同样要验证是否登录,否则执行登录逻辑
- B服务拿到A服务传递的请求,以及参数,其实参数中要带着用户已经登录的信息,例如: token;
- 其实这里过程意味着: A服务要将自己登录后获取的token,当A服务通过接口访问B接口的时候要呆着这个token,一般我们是放入header中处理的,也就是当我们用feign请求的时候,要全局做一个配置来处理这个事情:
- 每个微服务对所有过来的Feign调用进行过滤,然后从请求头中获取User用户信息,并存在ThreadLocal变量中。
- 每个微服务在使用FeignClient调用别的微服务时,先从ThreadLocal里面取出user信息,并放在request的请求头中。
- 微服务之间的5/6 两步我们通常是统一处理, 也就是在网关(zuul 或者 gateway)中; 首先是获取当前的请求,然后从当前请求线程中ThreadLocal 获取用户信息,然后塞入header中,其次每个服务都要从请求中获取header信息,当获取到token,然后存入到当前服务的 ThreadLocal 中;
- 在gateway 获取请求线程的ThreadLocal 中获取 以及 在被调用线程中获取header的时候,其实是要开启一个东西的,就是线程池信号量的问题; 这里的线程池不是 编程中的线程池;不开启会报上下文找不到异常,为啥呢? 请看下面的线程池隔离以及信号量隔离
线程池隔离?
Hystrix的隔离策略有两种:分别是线程隔离和信号量隔离。
- THREAD(线程隔离):使用该方式,HystrixCommand将会在单独的线程上执行,并发请求受线程池中线程数量的限制。
- SEMAPHORE(信号量隔离):使用该方式,HystrixCommand将会在调用线程上执行,开销相对较小,并发请求受信号量的个数的限制。
**如何配置? **
直接在yml 中配置:
hystrix.command.default.execution.isolation.strategy:Semaphore
配置:hystrix.command.default.execution.isolation.strategy 隔离策略,默认是Thread, 可选Thread|Semaphore
(1) 什么是线程池隔离?
为每一个服务接口单独开辟一个线程池,保持与其他服务接口线程的隔离,提高该服务接口的独立性和高可用。
(2) 线程池隔离的优点是什么?
- 可以完全隔离依赖的服务(例如 A, B),请求线程可以快速放回
- 当出现问题时,线程池隔离是独立的,不会影响其他服务和接口
- 当服务失败后再次变为可用状态,线程池将清理并可以立即恢复,等待时间短
- 独立的线程池提高了并发性
(3) 线程池隔离的缺点是什么?
- 增加了计算开销,每个命令的执行,都会涉及排队/调度/上下文切换等,都是在一个单独的线程上运行的
- 无法传递header
信号量隔离?
(1) 什么是信号量隔离?
- 线程 之间的访问是在同一条线程中, 请求和调用 在一个线程中
- 无线程切换,开销低, 可以传递http header
线程池隔离 or 信号量隔离
- 请求并发大,耗时短 采用信号量隔离,因为这类服务响应快,不会占用太长时间,也减少了开销,提高了服务的效率
- 并发量大,耗时长,采用线程隔离策略,这样可以保证大部分的线程可用
hystrixCommand线程
线程池隔离:
1、调用线程和hystrixCommand线程不是同一个线程,并发请求数受到线程池(不是容器tomcat的线程池,而是hystrixCommand所属于线程组的线程池)中的线程数限制,默认是10。
2、这个是默认的隔离机制
3、hystrixCommand线程无法获取到调用线程中的ThreadLocal中的值
信号量隔离:
1、调用线程和hystrixCommand线程是同一个线程,默认最大并发请求数是10
2、调用数度快,开销小,由于和调用线程是处于同一个线程,所以必须确保调用的微服务可用性足够高并且返回快才用
注意:如果发生找不到上下文的运行时异常,可考虑将隔离策略设置为SEMAPHONE。
总结:
本文中由于涉及到了http header 传递,所以只能选择 SEMAPHONE, 一般我们只需要在 网关中开启即可;
一般在网关中做如下处理:
-
获取当前服务header中是否有token ,没有的话指引服务去登录,有的话取到tokne 并校验
-
然后将获取的token放入到请求的http header中
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取token
ServerHttpRequest request = exchange.getRequest();
List<String> accessTokens = request.getHeaders().get(HeaderConstant.ACCESS_TOKEN);
// 去鉴权中心,校验token是否合法....省略
boolean flag = true;
// 鉴权成功 放入http header 中
if(flag ){
ServerHttpRequest build = request.mutate().build();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(request.getHeaders());
httpHeaders.add(HeaderConstant.USER_ID, userId);
build = new ServerHttpRequestDecorator(build) {
@Override
public HttpHeaders getHeaders() {
return httpHeaders;
}
};
return chain.filter(exchange.mutate().request(build).build());
// 否则什么也不做,后续会由于获取不到header 中的token 导致请求错误
}else{
return chain.filter(exchange);
}
}
}
一般每个服务做如下处理:
通常可以写在公共依赖模块中,然后每个服务去引入即可
-
获取到请求中的token,获取到了,然后放入 ThreadLocal中,供之后服务中的其他位置获取
-
请求结束后,记得清除ThreadLocal 中的信息,避免发生内存溢出
-
一般来说,也可以在这里进行一次token合法校验
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
@Autowired
private UpmsUserFeignClient upmsFeignClient;
@Override
public boolean preHandle(HttpServletRequest httpRequest,
HttpServletResponse response, Object handler) throws Exception {
//获取请求头会话token,thread local中设置用户id,方便后续获取用户id
String userId = httpRequest.getHeader(HeaderConstant.USER_ID);
if (StringUtils.isNotBlank(userId)) {
//获取用户
String tenantId = null;
Map pathVariables = (Map) httpRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (pathVariables != null && pathVariables.get("tenant_id") != null ) {
tenantId = (String)pathVariables.get("tenant_id");
if (!checkUserTenant(userId,tenantId)){
throw new MixException(BusinessErrorEnum.AUTH_TENANT_IS_ILLEGAL);
}
}
CurrentUserUtil.setCurrentUser(userId, tenantId);
return true;
} else {
return true;
}
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
// 此次请求结束,及时清理用户信息
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
try {
CurrentUserUtil.removeCurrentUserInfo();
} catch (Exception e) {
log.error("afterCompletion,e="+e.getMessage(),e);
} finally {
CurrentUserUtil.removeCurrentUserInfo();
}
}
}
本文转自 jimolvxing.blog.csdn.net/article/det…,如有侵权,请联系删除。