微服务架构下如何获取用户信息并认证?

Posted 智慧浩海

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微服务架构下如何获取用户信息并认证?相关的知识,希望对你有一定的参考价值。

在传统的单体项目中,我们对用户的认证通常就在项目里面,当拆分成微服务之后,一个业务操作会涉及多个服务。那么怎么对用户做认证?服务中又是如何获取用户信息的?这些操作都可以在 API 网关中实现。

动态管理不需要拦截的 API 请求

并不是所有的 API 都需要认证,比如登录接口。我们需要一个能够动态添加 API 白名单的功能,凡是在这个白名单当中的,我们就不做认证。这个配置信息需要能够实时生效,这就用上了我们的配置管理 Apollo。

在 API 网关中创建一个 Apollo 的配置类,代码如下所示。

  1. @Data
  2. @Configuration
  3. public class BasicConf
  4. // API接口白名单, 多个用逗号分隔
  5. @Value("$apiWhiteStr:/zuul-extend-user-service/user/login")
  6. private String apiWhiteStr;

编写认证的 Filter,代码如下所示。

  1. /**
  2. * 认证过滤器
  3. **/
  4. public class AuthFilter extends ZuulFilter
  5. @Autowired
  6. private BasicConf basicConf;
  7. public AuthFilter()
  8. super();
  9. @Override
  10. public boolean shouldFilter()
  11. return true;
  12. @Override
  13. public String filterType()
  14. return "pre";
  15. @Override
  16. public int filterOrder()
  17. return 1;
  18. @Override
  19. public Object run()
  20. RequestContext ctx = RequestContext.getCurrentContext();
  21. String apis = basicConf.getApiWhiteStr();
  22. // 白名单,放过
  23. List<String> whileApis = Arrays.asList(apis.split(","));
  24. String uri = ctx.getRequest().getRequestURI();
  25. if (whileApis.contains(uri))
  26. return null;
  27. // path uri 处 理
  28. for (String wapi : whileApis)
  29. if (wapi.contains("" && wapi.contains(")"))
  30. if (wapi.split("/").length == uri.split("/").length)
  31. String reg = wapi.replaceAll("\\\\.*", ".*1,");
  32. Pattern r = Pattern.compile(reg);
  33. Matcher m = r.matcher(uri);
  34. if (m.find())
  35. return null;
  36. return null;

在 Filter 中注入我们的 BasicConf 配置,在 run 方法里面执行判断的逻辑,将配置的白名单信息转成 List,然后判断当前请求的 URI 是否在白名单中,存在则放过。

下面还有一段是 Path URI 的处理,这是解决 /user/userId 这种类型的 URI,URI 中有动态的参数,直接匹配是否相等肯定是不行的。

最后配置 Filter 即可启用,代码如下所示。

  1. @Bean
  2. public AuthFilter authFilter()
  3. return new AuthFilter();

当有不需要认证的接口时,直接在 Apollo 后台修改一下配置信息即可实时生效。

创建认证的用户服务

用户服务是每个产品必备的一个服务,可以管理这个产品的用户信息。我们用到的用户服务只是演示认证,所以只提供一个登录的接口即可。

登录接口代码如下所示。

  1. /**
  2. * 用户登录
  3. *
  4. * @param query
  5. * @return
  6. */
  7. @ApiOperation(value = "用户登录", notes = "企业用户认证接口,参数为必填项")
  8. @PostMapping("/login")
  9. public ResponseData login(@ApiParam(value = "登录参数", required = true) @RequestBody LoginQuery query)
  10. if (query == null || query.getEid() == null || StringUtils.isBlank(query.getUid()))
  11. return ResponseData.failByParam("eid 和 uid 不能为空");
  12. return ResponseData.ok(enterpriseProductUserService.login(query.getEid(), query.getUid()));

Service 中的 login 方法用来判断是否成功登录,成功则用 JWT 将用户 ID 加密返回一个 Token。此处只是为了模拟,真实环境中需要去查数据库,代码如下所示。

  1. public String login(Long eid, String uid)
  2. JWTUtils jwtUtils = JWTUtils.getInstance();
  3. if (eid.equals(1L) && uid.equals("1001"))
  4. return jwtUtils.getToken(uid);
  5. return null;

路由之前的认证

除了我们之前讲解的,一些 API 由于特殊的需求,不需要做认证,我们可以用配置的方式来放行,其余的都需要认证,只有合法登录后的用户才能调用。当用户调用用户服务中的登录接口,登录成功之后就能拿到 Token,在请求其他的接口时带上 Token,就可以在 Zuul 的 Filter 中对这个 Token 进行认证。

验证逻辑和之前的 API 白名单是在一个 Filter 中进行的,在 path uri 处理之后进行认证,代码如下所示。

  1. // 验证 TOKEN
  2. if (!StringUtils.hasText(token))
  3. ctx.setSendZuulResponse(false);
  4. ctx.set("isSuccess", false);
  5. ResponseData data = ResponseData.fail("非法请求【缺少 Authorization 信息】", ResponseCode.NO_AUTH_CODE.getCode());
  6. ctx.setResponseBody(JsonUtils.toJson(data));
  7. ctx.getResponse().setContentType("application/json; charset=utf-8");
  8. return null;
  9. JWTUtils.JWTResult jwt = jwtUtils.checkToken(token);
  10. if (!jwt.isStatus())
  11. ctx.setSendZuulResponse(false);
  12. ctx.set("isSuccess", false);
  13. ResponseData data = ResponseData.fail(jwt.getMsg(), jwt.getCode());
  14. ctx.setResponseBody(JsonUtils.toJson(data));
  15. ctx.getResponse().setContentType("application/json; charset=utf-8");
  16. return null;
  17. ctx.addZuulRequestHeader("uid", jwt.getUid());

从请求头中获取 Token,如果没有就拦截并给出友好提示,设置 isSuccess=false 告诉下面的 Filter 不需要执行了。有 Token 则验证 Token 的合法性,合法则放行,不合法就拦截并给出友好提示。

向下游微服务中传递认证之后的用户信息

传统的单体项目中我们通常都是使用 Session 来存储登录后的用户信息,但这样会导致做了集群后的用户信息有问题,在 A 服务上登录了,下次被转发到 B 服务区,又得重新登录一次。为了解决这个问题,通常采用 Session 共享的方式来解决,比如 Spring Session 这种框架。

在微服务下如何解决这个问题呢?为了提高并发性能,方便快速扩容,服务都被设计成了无状态的,不需要对每个服务都进行用户是否登录的判断,只需要统一在 API 网关中认证好即可。

在 API 网关中认证之后如何把用户信息传递给下方的服务就是我们需要关注的了,在 Zuul 中可以将认证之后的用户信息通过请求头的方式传递给下方服务,比如如下代码所示的方式。

  1. ctx.addZuulRequestHeader("uid", jwt.getUid());

在具体的服务中就可以通过 request 对象来获取传递过来的用户信息,代码如下所示。

  1. @GetMapping("/article/callHello")
  2. public String callHello()
  3. System.err.println("用户ID:" + request.getHeader("uid"));
  4. return userRemoteClient.hello();

内部服务间的用户信息传递

关于用户信息的传递问题,我们知道从 API 网关过来的请求,经过认证之后是可以拿到认证后的用户 ID,这时候我们可以通过 addZuulRequestHeader 的方式将用户 ID 传递到我们转发的服务上去,但如果从网关转发到 A 服务,A 服务需要调用 B 服务的接口,那么我想在 B 服务中也能通过 request.getHeader(“uid”) 去获取用户 ID,这个时候该怎么处理?

关于这种需求,我的建议是直接通过网关转发过去的接口。我们可以通过 request.getHeader(“uid”) 来获取网关带过来的用户 ID,然后服务之前调用的话可以通过参数的方式告诉被调用的服务,A 服务调用 B 服务的 hello 接口,那么 hello 接口中增加一个 uid 的参数即可,此时的用户 ID 是网关给我们的,已经是认证过的了,可以直接使用。

如果想做成类似于 Session 共享的方式也可以,那么当 A 服务调用 B 服务时,你就得通过在框架层面将用户 ID 传递到 B 服务当中,但是这个不能让每个开发人员去关心,必须封装成统一的处理。

我们可以这样做,首先我们的场景是 API 网关中会通过请求头将用户 ID 传递到转发的服务中,那么我们可以通过过滤器来获取这个值,然后进行传递操作,代码如下所示。

  1. public class HttpHeaderParamFilter implements Filter
  2. @Override
  3. public void init(FilterConfig filterConfig) throws ServletException
  4. @Override
  5. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  6. throws IOException, ServletException
  7. HttpServletRequest httpRequest = (HttpServletRequest) request;
  8. HttpServletResponse httpResponse = (HttpServletResponse) response;
  9. httpResponse.setCharacterEncoding("UTF-8");
  10. httpResponse.setContentType("application/json; charset=utf-8");
  11. String uid = httpRequest.getHeader("uid");
  12. RibbonFilterContextHolder.getCurrentContext().add("uid", uid);
  13. chain.doFilter(httpRequest, response);
  14. @Override
  15. public void destroy()

RibbonFilterContextHolder 是通过 InheritableThreadLocal 在线程之间进行数据传递的。这步走完后请求就转发到了我们具体的接口上面,然后这个接口中就会用 Feign 去调用 B 服务的接口,所以接下来需要用 Feign 的拦截器将刚刚获取的用户 ID 重新传递到 B 服务中,代码如下所示。

  1. public class FeignBasicAuthRequestInterceptor implements RequestInterceptor
  2. public FeignBasicAuthRequestInterceptor()
  3. @Override
  4. public void apply(RequestTemplate template)
  5. Map<String, String> attributes = RibbonFilterContextHolder.getCurrentContext().getAttributes();
  6. for (String key : attributes.keySet())
  7. String value = attributes.get(key);
  8. template.header(key, value);

通过获取 InheritableThreadLocal 中的数据添加到请求头中,这里不用具体的名字去获取数据是为了扩展,这样后面添加任何的参数都能直接传递过去了。

Feign 的拦截器使用需要在 @FeignClient 注解中指定 Feign 的自定义配置,自定义配置类中配置 Feign 的拦截器即可。

拦截器只需要注册下就可以使用了,本套方案不用改变当前任何业务代码,代码如下所示。

  1. public class FeignBasicAuthRequestInterceptor implements RequestInterceptor
  2. @Bean
  3. public FilterRegistrationBean filterRegistrationBean()
  4. FilterRegistrationBean registrationBean = new FilterRegistrationBean();
  5. HttpHeaderParamFilter httpHeaderParamFilter = new HttpHeaderParamFilter();
  6. registrationBean.setFilter(httpHeaderParamFilter);
  7. List<String> urlPatterns = new ArrayList<String>(1);
  8. urlPatterns.add("/*");
  9. registrationBean.setUrlPatterns(urlPatterns);
  10. return registrationBean;

微服务架构中的认证授权设计

【中文标题】微服务架构中的认证授权设计【英文标题】:Authentication and Authorization Design in Microservice Architecture 【发布时间】:2021-06-04 18:33:25 【问题描述】:

我正在使用微服务架构开发应用程序。需要实施安全性。

所以我计划使用 3 项服务来实现这一目标。

    API 网关 用户服务 订单服务

第一步: 客户端将用户名和密码发送到 API Gateway 以获取令牌。 API 网关 应该调用用户服务来验证凭据,如果凭据有效API 网关 创建一个令牌并将其发送给客户端。

第二步: 客户端尝试使用令牌(API Gateway 在步骤 1 中发送)访问订单服务,因此 API Gateway 必须调用用户服务来验证令牌。

我正在考虑在我的 API 网关 微服务中包含所有授权和身份验证逻辑。因此,当我从 API Gateway 的消费者那里获得 JWT 令牌时,我应该调用 Users Service 来根据用户名和密码验证它,因为我将所有与用户相关的数据存储在用户服务。

我相信这将是实现微服务架构安全性的更好方法之一。

如果有更优雅的方式,请提出建议。

提前致谢。

【问题讨论】:

您能否澄清一下“当我从 API 网关的消费者那里获得 JWT 令牌时,我应该调用用户服务以根据用户名和密码对其进行验证”是什么意思?您不会同时获得 JWT 和用户名/密码,对吧? @DenizAcay,我编辑了问题,如果您有任何疑问,请让我继续。 【参考方案1】:

我认为你走在正确的道路上。但是每个操作都依赖于用户服务,这使得用户服务成为可能的单点故障,其他服务的可用性将取决于用户服务。

请阅读有关 Service Fuse 反模式的更多信息:https://akfpartners.com/growth-blog/microservice-anti-pattern-service-fuse

对于第一次身份验证调用,将用户名和密码的身份验证委托给用户服务是有意义的。但对于其他调用,您只需在 API 网关上验证 JWT。

我建议使用公钥加密来签署 JWT,这样您就可以在用户服务上使用私钥对 JWT 进行签名,并将公钥部署到 API Gateway 进行验证。这样,API Gateway 或任何其他服务将能够验证令牌,而无需敏感的共享密钥。

【讨论】:

在这种情况下 1. Users Service 是身份验证服务器权限(Kind Of),2. 我们可以在 Order Service 中具有方法级别的安全性吗? 是的,但是由于客户端需要与API Gateway 通信才能访问Order Service,您也可以在API Gateway 中验证JWT 令牌,然后才能访问Order Service。

以上是关于微服务架构下如何获取用户信息并认证?的主要内容,如果未能解决你的问题,请参考以下文章

微服务架构认证鉴权方案

微服务架构中的授权/认证

微服务架构下的身份认证

权限设计系列「认证授权专题」微服务架构的登陆认证问题

微服务架构中的认证授权设计

深入聊聊微服务架构的身份认证问题