token鉴权
token刷新
token传递
前言
上篇主要介绍了登录服务的扩展及多端登录的实现。本篇接着讲网关鉴权,以及剩下几个未解答的问题。
-
如何处理token过期,刷新问题
-
token在异步场景中如何传递
-
定时任务场景token如何使用
-
第三方服务如何接入
1.网关扩展
网关,作为系统的统一入口,提供内部服务的路由跳转,给客户端提供统一的服务,可以实现一些和业务没有耦合的公用逻辑,主要功能包含认证、鉴权、路由转发、安全策略、防刷、流量控制、监控日志等。
1.1 资源服务器配置
@Configuration
@EnableResourceServer
public class GatewayResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(customerAuthenticationEntryPoint())
.accessDeniedHandler(customerOAuth2AccessDeniedHandler())
.and()
.headers().contentTypeOptions().disable()
.and()
.authorizeRequests()
.anyRequest()
//❶自定义认证管理器AuthenticatedManager
.access("@authenticatedManager.doAuthenticate(request,authentication)")
.antMatchers("/**")
.authenticated()
;
}
❶这里使用的认证管理器是我们自己定义的AuthenticatedManager,
其中表达式@authenticatedManager.doAuthenticate(request,authentication) 是El表达式的扩展。
spring Security 提供的默认表达式有
例如:
1.2 自定义认证管理器
public class AuthenticatedManager {
private AuthenticatedHandler authenticatedHandler;
public AuthenticatedManager(AuthenticatedHandler authenticatedHandler){
this.authenticatedHandler = authenticatedHandler;
}
public boolean doAuthenticate(HttpServletRequest request, Authentication authentication) {
try {
authenticatedHandler.doAuthenticate(request, authentication);
}catch (BizException e){
throw new AuthenticatedException(e.getResultCode());
}
return true;
}
}
1.3 服务细粒度鉴权配置
根据服务serverId来区分,每个服务可以单独配置豁免token鉴权的url.
@Data
@AutoConfig
@ConfigurationProperties(prefix = GatewayProperties.PREFIX)
public class GatewayProperties {
private Map<String, GatewayProperties.GatewayTokenConfig> token = Maps.newHashMap ();
@Data
public static class GatewayTokenConfig {
/**
* 是否启用Token状态校验,默认 true
*/
private boolean enable = Boolean.TRUE;
/**
* Token状态校验 需要豁免的url
*/
private List<String> excludeUrls = Lists.newArrayList();
}
}
2.token鉴权
上一篇在登录服务讲解中,我们已经介绍了多端账号登录时,token 存在redis中的状态。
当请求进入api网关时,会从header中取出Authorization,即jwt token,
2.1 token为空
2.2 从redis中获取token
从header中拿到jwt token后,用jwtHelper工具类解析token,获取用户账号userName
2.2.1 token revoke
首先会判断token是否revoke
2.2.2 token 过期
3.token刷新
到这一步说明token状态正常,那么如何刷新token了?如何保证用户在操作中token不会过期呢?
目前用到的方案有:客户端主动刷新,服务端定时任务刷新,redis expire。我们采用的是redis。
3.1 客户端主动刷新
流程一般如下:
3.2 redis刷新
采用redis来维护token状态时,那么只需要保证用户在使用过程中token永不过期即可。即不用刷新token,只用延长token。这样只要用户在操作,那么token的有效期就会不断的刷新。 只用当用户不操作时,超过有效期时间,token才会在redis中过期。(这里同时还需要延长tokenSet 的key,来保证登录服务获取 tokenSet时能够拿到该token.)
只用使用redis expire 命令即可。
以下是我们的整个token鉴权方案。
3.3 两种刷新方案的对比
每种方案都不是绝对的,要根据业务来定。
3.4 redis使用不当导致的生产事故
所以要严格遵守阿里编程规范。
4.token传递
4.1 token在异步线程中传递
示例代码
public class TokenContext {
private static final ThreadLocal<String> tokenHolder = new TransmittableThreadLocal<>();
// 设置当前线程的token
public static void setToken(String token) {
tokenHolder.set(token);
}
// 获取当前线程的token
public static String getToken() {
return tokenHolder.get();
}
// 清除当前线程的token
public static void clearToken() {
tokenHolder.remove();
}
}
// 在异步线程中设置和获取token
CompletableFuture.runAsync(() -> {
String token = TokenContext.getToken();
// 使用token进行操作
}).whenComplete((result, throwable) -> {
TokenContext.clearToken();
});
为什么用TransmittableThreadLocal,可以看从threadlocal到TransmittableThreadLocal
4.2 token在消息队列中传递
以kafka为例
生产者:示例代码
public class CustomerKafkaProducer<K, V> implements Producer<K, V> {
@Override
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
//发送前回调处理消息
try{
//❶ 发送前将token当如kafka header中
messageSendProcessor.beforeProcessMessage(token);
return producer.send(record);
}finally {
tracers.stream().forEach(messageSendProcessor -> {
messageSendProcessor.afterProcessorMessage(record);
});
}
}
}
消费者:示例代码(需要配置切面去拦截Onmessage方法)
public class MessageListenerMethodInterceptor implements MethodInterceptor {
private static final String LISTENER_METHOD_NAME = "onMessage";
@Override
public Object invoke(@Nonnull MethodInvocation invocation) throws Throwable {
if (!StrUtil.equals(LISTENER_METHOD_NAME, invocation.getMethod().getName())) {
return invocation.proceed();
}
try {
String token = invocation.getArguments().get("token");
//❶将token存入本地线程中
tokenHolder.set(token);
return invocation.proceed();
} catch (Exception e) {
throw new Exception(e);
} finally {
//❷从本地线程中移除token
tokenHolder.remove();
}
}
}
这样在消息消费中,可以直接通过tokenHolder.get()拿到token。
4.3 token在feign中传递
示例代码:
public class FeignHeaderInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
Token token = tokenHolder.getToken();
headers.put("token","token")
template.headers(headers);
}
}
}
然后在web请求时添加过滤器
示例代码:
public class customerRequestFilter extends OncePerRequestFilter implements Ordered {
@Override
protected void doFilterInternal(HttpServletRequest httpRequest, HttpServletResponse httpResponse,
FilterChain filterChain)
throws ServletException, IOException {
try {
// 从header中获取Token信息
Token token = getHeader(httpRequest);
//❶将token存入本地线程中
tokenHolder.set(token);
filterChain.doFilter(httpRequest, httpResponse);
} finally {
//❷从本地线程中移除token
tokenHolder.remove();
}
}
这样,我们也可以直接在业务代码里通过tokenHolder.get()拿到token。
整个Oauth2鉴权体系大体上讲完了,事实上我只是分析了其中最简单的密码流程。实际业务要远比这个复杂的多。再次致敬大佬们。
后面将介绍新版的认证服务Spring Authorization Server的认证流程。敬请期待!!!
原文始发于微信公众号(小李的源码图):某安某大型国企项目Oauth2实践(三)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/145303.html