微服务之间如何进行用户信息传递

微服务中如何传递用户信息?

前提,有了一套微服务架构的服务,然后有一个统一鉴权的服务用户中心 userService

  1. 访问服务A ,在A 中执行用户登录逻辑,并将用户信息临时存入到A服务中心: 例如是token; 如何存放? 我们一般是通过空间换时间的方式-- ThreadLocal 存取
  2. 然后服务之间调用,A服务访问B服务,当进入B服务的时候,同样要验证是否登录,否则执行登录逻辑
  3. B服务拿到A服务传递的请求,以及参数,其实参数中要带着用户已经登录的信息,例如: token;
  4. 其实这里过程意味着: A服务要将自己登录后获取的token,当A服务通过接口访问B接口的时候要呆着这个token,一般我们是放入header中处理的,也就是当我们用feign请求的时候,要全局做一个配置来处理这个事情:
  5. 每个微服务对所有过来的Feign调用进行过滤,然后从请求头中获取User用户信息,并存在ThreadLocal变量中。
  6. 每个微服务在使用FeignClient调用别的微服务时,先从ThreadLocal里面取出user信息,并放在request的请求头中。
  7. 微服务之间的5/6 两步我们通常是统一处理, 也就是在网关(zuul 或者 gateway)中; 首先是获取当前的请求,然后从当前请求线程中ThreadLocal 获取用户信息,然后塞入header中,其次每个服务都要从请求中获取header信息,当获取到token,然后存入到当前服务的 ThreadLocal 中;
  8. 在gateway 获取请求线程的ThreadLocal 中获取 以及 在被调用线程中获取header的时候,其实是要开启一个东西的,就是线程池信号量的问题; 这里的线程池不是 编程中的线程池;不开启会报上下文找不到异常,为啥呢? 请看下面的线程池隔离以及信号量隔离

线程池隔离?

Hystrix的隔离策略有两种:分别是线程隔离和信号量隔离。

  1. THREAD(线程隔离):使用该方式,HystrixCommand将会在单独的线程上执行,并发请求受线程池中线程数量的限制。
  2. 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, 一般我们只需要在 网关中开启即可;

一般在网关中做如下处理: 
  1. 获取当前服务header中是否有token ,没有的话指引服务去登录,有的话取到tokne 并校验
  2. 然后将获取的token放入到请求的http header中
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

  @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取token
        ServerHttpRequest request = exchange.getRequest();
        List 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);
         }
    }
}
一般每个服务做如下处理: 

通常可以写在公共依赖模块中,然后每个服务去引入即可

  1. 获取到请求中的token,获取到了,然后放入 ThreadLocal中,供之后服务中的其他位置获取
  2. 请求结束后,记得清除ThreadLocal 中的信息,避免发生内存溢出
  3. 一般来说,也可以在这里进行一次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();
        }

    }
}

你可能感兴趣的:(springcloud,mysql,java,leetcode,python,html)