前言
Oauth2的授权流程是客户端向认证服务器提交认证获取Token,认证服务器颁发JWT格式的Token客户端进行Token的存储,接着客户端带着Token请求资源服务器,资源服务器校验Token并对资源授权,授权成功返回资源
有这么一种情况,就是客户端的请求可能需要多个资源服务器共同完成,即:一个请求过来到达资源服务器A,资源服务器A需要调用资源服务器B才能完成请求,如果资源服务器B也需要做授权,那我们的请求可能会失败,因为我们的Token通过请求到达了资源服务器A,默认情况下资源服务A调用资源服务器B并不能把Token转发过去,所以资源服务器B可能会授权失败 , 所以我们要做服务之间的授权
1.服务之间授权方案
原理其实很简单,我们的调用关系是客户端(浏览器)调用资源服务器A通过请求头传递Token,资源服务器A通过Feign调用资源服务器B请求是没有Token的,我们只需要编写一个Feign的拦截器,将客户端请求A的请求头中的Token设置到资源服务器A调用资源服务器B的Feign的请求头中即可
2.搭建第二个资源服务器
这里需要搭建第二个资源服务器来演示服务器之间的授权,只需要把security-resource-server 复制一份命名为 security-resource2-server 即可,当然security-resource-server 需要集成Feign去调用security-resource2-server ,这里就省略了,没有思路??您可以参照 《负载均衡Feign》
这里还要注意一个问题,就是用户如果要能访问到资源服务器A和资源服务器B,那么它应该同时拥有资源服务器A和资源服务器B的访问权限(permisson),所以不要忘记给用户把需要的权限都配置上
3.Feign的拦截器
为了通用,我们可以搭建一个公共的模块编写Feign的拦截器,谁需要转发Token就只需要依赖它即可 , 搭建模块 “security-resource-common” ,
3.1.导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.2.定义Feign的拦截器
RequestInterceptor 是Feign的拦截器接口,提供了apply方法让我们可以通过RequestTemplate 对请求进行自定义,注意:该拦截器类需要给Spirng扫描到
@Component
@Slf4j
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
//请求头中的token
private final String AUTHORIZATION_HEADER = "Authorization";
@Override
public void apply(RequestTemplate requestTemplate) {
//requestTemplate:feign底层用来发封装请求的对象
//1.获取请求对象
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//2.获取请求头中的Token
String token = requestAttributes.getRequest().getHeader(AUTHORIZATION_HEADER);
log.info("Feign拦截器添加请求头:{}",token);
//3.添加到Feign的请求头
requestTemplate.header(AUTHORIZATION_HEADER,token);
}
}
代码挺简单的,通过RequestContextHolder得到请求对象,获取请求头中的Token设置到RequestTemplate 的header中即可
4.修改Hystrix并发策略
4.1.问题描述
理论上来说做了如上配置即可完成Token的转发了,但如果我们集成了Hystrix那么在Feign的拦截器中是没办法获取到请求对象的,这是因为Hystrix默认的隔离策略是线程池,每个请都会被分配到一个新的线程执行,导致请对象无法获取,不知道如何降级?见《Feign开启Hystrix》
4.2.解决方案
解决方案有两种,一是使用信号量隔离,在配置文件中加入如下配置:“hystrix.command.default.execution.isolation.strategy=SEMAPHORE”,解决方案二是修改Hystrix的隔离策略,我们这里使用第二种方式,因为使用信号量隔离会让请求变成单线程执行,官方也不推荐。
如何修改Hystrix的隔离策略呢?原理就是把调用线程A中的请求通过RequestContextHolder获取到,放到新的线程B中的RequestContextHolder中即可 。
4.3.定义Hystrix并发策略配置
@Configuration
public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
private HystrixConcurrencyStrategy hystrixConcurrencyStrategy;
public FeignHystrixConcurrencyStrategy() {
try {
this.hystrixConcurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
if (this.hystrixConcurrencyStrategy instanceof FeignHystrixConcurrencyStrategy) {
return;
}
HystrixCommandExecutionHook commandExecutionHook =
HystrixPlugins.getInstance().getCommandExecutionHook();
HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
HystrixPropertiesStrategy propertiesStrategy =
HystrixPlugins.getInstance().getPropertiesStrategy();
HystrixPlugins.reset();
HystrixPlugins instance = HystrixPlugins.getInstance();
instance.registerConcurrencyStrategy(this);
instance.registerCommandExecutionHook(commandExecutionHook);
instance.registerEventNotifier(eventNotifier);
instance.registerMetricsPublisher(metricsPublisher);
instance.registerPropertiesStrategy(propertiesStrategy);
} catch (Exception e) {
System.out.println("策略注册失败");
}
}
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
//线程A获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//把请求对象放到新的线程中
return new WrappedCallable<>(callable, requestAttributes);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixProperty<Integer> corePoolSize,
HystrixProperty<Integer> maximumPoolSize,
HystrixProperty<Integer> keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
unit, workQueue);
}
@Override
public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
HystrixThreadPoolProperties threadPoolProperties) {
return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, threadPoolProperties);
}
@Override
public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
return this.hystrixConcurrencyStrategy.getBlockingQueue(maxQueueSize);
}
@Override
public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
return this.hystrixConcurrencyStrategy.getRequestVariable(rv);
}
static class WrappedCallable<T> implements Callable<T> {
private final Callable<T> target;
private final RequestAttributes requestAttributes;
public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
this.target = target;
this.requestAttributes = requestAttributes;
}
@Override
public T call() throws Exception {
try {
//把A线程传入过来的请求对象,设置到B线程的RequestContextHolder
RequestContextHolder.setRequestAttributes(requestAttributes);
return target.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
}
到这里拦截器模块就搭建结束了,你还需要把这个模块依赖到资源服务器中,即给“security-resource-server” 导入依赖 “security-resource-common”,然后启动服务测试。
5.服务之间授权-客户端模式
5.1.问题描述
上面服务之间授权的场景是用户发起的请求需求多个服务完成,然后有了服务之间的授权,有一种场景就是服务之间的调用可能和用户上下文无关。
比如有这样一个场景,由于项目设计的问题,在做微服务重构时导致认证中心AuthServer在做认证时需要调用另外一个微服务SystemServer来加载用来的权限列表,即:用户认证表和用户权限表不在同一个数据库中,当然解决方案也有很多,比如强行把权限和认证表放在一个库,这个不是今天讨论的重点。
现在的问题是SystemServer是做了权限控制的,也就是说客户端向AuthServer提交认证请求获取Token,而AuthServer需要调用SystemServer加载用户的权限才能生成Token,而加载权限又需要先得有一个Token才行,这不就矛盾了么?
这里我要说的解决方案就是给AuthServer添加一个Feign的拦截器,在拦截器中采用客户端模式生成一个临时的Token向SystemServer发起请求加载权限列表然后再为客户端颁发正式的Token
为什么采用客户端模式?因为服务之间是绝对信任的,而且只是需要颁发一个临时的Token而已,和用户上下文无关,这个Token并不是为用户生成的Token,只是服务之间调用需要的临时Token
5.2.Feign的拦截器
这次的拦截器和上一篇文章的拦截器的定义方式一样,只不过上一次是转发用户的Token,而这一次是自己生成一个临时的Token,代码如下:
@Component
@Slf4j
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
private static String TEMPTOKENURL = "http://localhost:3000/oauth/token?client_id=%s&client_secret=%s&grant_type=client_credentials";
//请求头中的token
private final String AUTHORIZATION_HEADER = "Authorization";
@Override
public void apply(RequestTemplate requestTemplate) {
//requestTemplate:feign底层用来发请求的http客户端
//1.使用客户端模式生成一个临时的Token
Map<String, String> tokenMap = HttpUtil.sendPost(String.format(TEMPTOKENURL, "temp", "123"));
ValidUtils.assertNotNull(tokenMap,"服务调用失败[临时Token获取失败]");
log.info("Feign拦截器添加临时Token到请求头:{}",tokenMap);
//2.添加到Feign的请求头
requestTemplate.header(AUTHORIZATION_HEADER,"Bearer "+tokenMap.get("access_token"));
}
}
5.4.Http工具类
http工具用到了httpclient包
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.59</version>
</dependency>
</dependencies>
工具类
public class HttpUtil {
//发送Post请求,注意:参数使用?方式带着URL后面
public static Map<String,String> sendPost(String url) {
// 获得Http客户端(可以理解为:你得先有一个浏览器;注意:实际上HttpClient与浏览器是不一样的)
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
// 创建Post请求
HttpPost httpPost = new HttpPost(url);
// 响应模型
CloseableHttpResponse response = null;
try {
// 由客户端执行(发送)Post请求
response = httpClient.execute(httpPost);
// 从响应模型中获取响应实体
HttpEntity responseEntity = response.getEntity();
if (responseEntity != null) {
return JSON.parseObject(EntityUtils.toString(responseEntity),Map.class);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (httpClient != null) {
httpClient.close();
}
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
总结
到这里就结束了,文章介绍了两种微服务之间的授权方式,一种是和用户上下文有关的请求,通过Feign的拦截器转发用户请求中的Token的给下游微服务,二种是微服务之间调用可能和当前用户上下文无关,我们采用在拦截器中采用客户端模式生成一个Token转发给下游服务的方式,希望对你有所帮助