演进

  • 单体, 通过AuthFilter结合cookie和session搞定

image.png

  • web服务集群, 通过sticky session(粘性session), 即session携带实例信息, 然后始终访问同一个实例来搞定

image.png

  • web服务集群, 也可以通过集中化session, 即集中存储在第三方介质来搞定

image.png

  • 微服务, 采用单独的授权服务来搞定

image.png

  • 微服务, 为了避免每个服务都接入授权服务, 因此引入网关, 在网关统一处理认证授权

image.png

  • 微服务, 也可以通过无状态的jwt结合网关使流程更简洁

image.png

JWT补充

  • 令牌组成

image.png

  • 两种生成方式

image.png
image.png
image.png

示例项目

前端鉴权

  • staffjoy采用jwt(HMAC)+gateway的简易鉴权方式
  • 认证成功后会将token设置cookie写入session, 登录退出时直接清除cookie
  • 网关解析token, 并将用户信息设置到上下文, 以及设置为请求头转发至各服务
  • 各服务间调用采用feign拦截器的方式, 从上下文中取得用户信息继续往下传递

这部分比较简单, 没啥新意, 代码略

服务间鉴权

示例项目的服务间鉴权采用的是校验请求头的方式

被调用方

通过在controller上添加注解, 对请求来源的请求头进行校验

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface Authorize {
  5. // allowed consumers
  6. String[] value();
  7. }
  1. public class AuthorizeInterceptor extends HandlerInterceptorAdapter {
  2. @Override
  3. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  4. if (!(handler instanceof HandlerMethod)) {
  5. return true;
  6. }
  7. HandlerMethod handlerMethod = (HandlerMethod) handler;
  8. Authorize authorize = handlerMethod.getMethod().getAnnotation(Authorize.class);
  9. if (authorize == null) {
  10. return true; // no need to authorize
  11. }
  12. String[] allowedHeaders = authorize.value();
  13. String authzHeader = request.getHeader(AuthConstant.AUTHORIZATION_HEADER);
  14. if (StringUtils.isEmpty(authzHeader)) {
  15. throw new PermissionDeniedException(AuthConstant.ERROR_MSG_MISSING_AUTH_HEADER);
  16. }
  17. if (!Arrays.asList(allowedHeaders).contains(authzHeader)) {
  18. throw new PermissionDeniedException(AuthConstant.ERROR_MSG_DO_NOT_HAVE_ACCESS);
  19. }
  20. return true;
  21. }
  22. }
  1. @PostMapping(path = "/get_or_create")
  2. @Authorize(value = {
  3. AuthConstant.AUTHORIZATION_SUPPORT_USER,
  4. AuthConstant.AUTHORIZATION_WWW_SERVICE,
  5. AuthConstant.AUTHORIZATION_COMPANY_SERVICE
  6. })
  7. public GenericAccountResponse getOrCreate(@RequestBody @Valid GetOrCreateRequest request) {
  8. AccountDto accountDto = accountService.getOrCreate(request.getName(), request.getEmail(), request.getPhoneNumber());
  9. GenericAccountResponse genericAccountResponse = new GenericAccountResponse(accountDto);
  10. return genericAccountResponse;
  11. }

调用方

调用方在feign客户端上, 添加相应的请求头

  1. @FeignClient(name = AccountConstant.SERVICE_NAME, path = "/v1/account", url = "${staffjoy.account-service-endpoint}")
  2. // TODO Client side validation can be enabled as needed
  3. // @Validated
  4. public interface AccountClient {
  5. @PostMapping(path = "/create")
  6. GenericAccountResponse createAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid CreateAccountRequest request);
  7. ...}
  1. GenericAccountResponse genericAccountResponse = accountClient.createAccount(AuthConstant.AUTHORIZATION_WWW_SERVICE, createAccountRequest);

补充: 服务间调用的约定,是双方事先沟通好的,这块儿总感觉怪怪的。有其他实践方式吗 作者回复: 事先约定传递http header进行鉴权是一种简单方式。其它更严格的方式有: 1). ip白名单方式,服务器端通过过滤器filter校验客户端ip是否在授权范围内。 2). 运维网段隔离,通过运维手段隔离网段,例如支付服务在独立隔离生产网段内,且只有白名单内ip可以访问。 3). 通过内部反向代理集中鉴权,服务之间调用必须经过内部反向代理(例如nginx),然后在nginx上配授权规则。 4). 通过ServiceMesh实现服务授权,可以实现细粒度的流量权限控制。

角色鉴权

可以对接一个角色服务, 在jwt中携带角色信息, 在网关进行解析, 最后在各服务再详细校验
image.png

权限模型比较有趣

  • 角色只关联app
  • 用户需要先注册app, 才会与app产生关联, 注册后才能授权该app拥有的角色
  • 用户可以属于多个组, 一个组可以有多个角色, 也可以有多个app

image.png