演进
- 单体, 通过AuthFilter结合cookie和session搞定
- web服务集群, 通过sticky session(粘性session), 即session携带实例信息, 然后始终访问同一个实例来搞定
- web服务集群, 也可以通过集中化session, 即集中存储在第三方介质来搞定
- 微服务, 采用单独的授权服务来搞定
- 微服务, 为了避免每个服务都接入授权服务, 因此引入网关, 在网关统一处理认证授权
- 微服务, 也可以通过无状态的jwt结合网关使流程更简洁
JWT补充
- 令牌组成
- 两种生成方式
示例项目
前端鉴权
- staffjoy采用jwt(HMAC)+gateway的简易鉴权方式
- 认证成功后会将token设置cookie写入session, 登录退出时直接清除cookie
- 网关解析token, 并将用户信息设置到上下文, 以及设置为请求头转发至各服务
- 各服务间调用采用feign拦截器的方式, 从上下文中取得用户信息继续往下传递
这部分比较简单, 没啥新意, 代码略
服务间鉴权
被调用方
通过在controller上添加注解, 对请求来源的请求头进行校验
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Authorize {
// allowed consumers
String[] value();
}
public class AuthorizeInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Authorize authorize = handlerMethod.getMethod().getAnnotation(Authorize.class);
if (authorize == null) {
return true; // no need to authorize
}
String[] allowedHeaders = authorize.value();
String authzHeader = request.getHeader(AuthConstant.AUTHORIZATION_HEADER);
if (StringUtils.isEmpty(authzHeader)) {
throw new PermissionDeniedException(AuthConstant.ERROR_MSG_MISSING_AUTH_HEADER);
}
if (!Arrays.asList(allowedHeaders).contains(authzHeader)) {
throw new PermissionDeniedException(AuthConstant.ERROR_MSG_DO_NOT_HAVE_ACCESS);
}
return true;
}
}
@PostMapping(path = "/get_or_create")
@Authorize(value = {
AuthConstant.AUTHORIZATION_SUPPORT_USER,
AuthConstant.AUTHORIZATION_WWW_SERVICE,
AuthConstant.AUTHORIZATION_COMPANY_SERVICE
})
public GenericAccountResponse getOrCreate(@RequestBody @Valid GetOrCreateRequest request) {
AccountDto accountDto = accountService.getOrCreate(request.getName(), request.getEmail(), request.getPhoneNumber());
GenericAccountResponse genericAccountResponse = new GenericAccountResponse(accountDto);
return genericAccountResponse;
}
调用方
调用方在feign客户端上, 添加相应的请求头
@FeignClient(name = AccountConstant.SERVICE_NAME, path = "/v1/account", url = "${staffjoy.account-service-endpoint}")
// TODO Client side validation can be enabled as needed
// @Validated
public interface AccountClient {
@PostMapping(path = "/create")
GenericAccountResponse createAccount(@RequestHeader(AuthConstant.AUTHORIZATION_HEADER) String authz, @RequestBody @Valid CreateAccountRequest request);
...}
GenericAccountResponse genericAccountResponse = accountClient.createAccount(AuthConstant.AUTHORIZATION_WWW_SERVICE, createAccountRequest);
补充: 服务间调用的约定,是双方事先沟通好的,这块儿总感觉怪怪的。有其他实践方式吗 作者回复: 事先约定传递http header进行鉴权是一种简单方式。其它更严格的方式有: 1). ip白名单方式,服务器端通过过滤器filter校验客户端ip是否在授权范围内。 2). 运维网段隔离,通过运维手段隔离网段,例如支付服务在独立隔离生产网段内,且只有白名单内ip可以访问。 3). 通过内部反向代理集中鉴权,服务之间调用必须经过内部反向代理(例如nginx),然后在nginx上配授权规则。 4). 通过ServiceMesh实现服务授权,可以实现细粒度的流量权限控制。
角色鉴权
可以对接一个角色服务, 在jwt中携带角色信息, 在网关进行解析, 最后在各服务再详细校验
权限模型比较有趣
- 角色只关联app
- 用户需要先注册app, 才会与app产生关联, 注册后才能授权该app拥有的角色
- 用户可以属于多个组, 一个组可以有多个角色, 也可以有多个app