某安某大型国企项目Oauth2实践(三)

前言
网关扩展
token鉴权

token刷新
token传递


前言

上篇主要介绍了登录服务的扩展及多端登录的实现。本篇接着讲网关鉴权,以及剩下几个未解答的问题。

  1. 如何处理token过期,刷新问题

  2. token在异步场景中如何传递

  3. 定时任务场景token如何使用


  4. 第三方服务如何接入


某安某大型国企项目Oauth2实践(三)


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()
            ;
    }
基于Oauth2方法,继承ResourceServerConfigurerAdapter。
❶这里使用的认证管理器是我们自己定义的AuthenticatedManager,
其中表达式@authenticatedManager.doAuthenticate(request,authentication)  是El表达式的扩展。
spring Security 提供的默认表达式有

某安某大型国企项目Oauth2实践(三)

例如:

某安某大型国企项目Oauth2实践(三)

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;
    }
}

这里的doAuthenticate 方法即上面资源服务配配置的El表达式调用的方法。
AuthenticatedHandler 也是我们自己定义的认证处理器接口类。如token鉴权,功能鉴权等处理逻辑实现AuthenticatedHandler即可。

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为空

如果jwt为空,并且该请求不是豁免token的请求,则会路由到登录页面。

2.2 从redis中获取token

从header中拿到jwt token后,用jwtHelper工具类解析token,获取用户账号userName

2.2.1 token revoke

首先会判断token是否revoke

某安某大型国企项目Oauth2实践(三)

根据key的生成规则,从redis中获取,如果能够找到该key,说明该用户被挤下线了,然后提示用户已被挤下线。同时移除该revoke key.

2.2.2 token 过期

  如果token没有revoke,再判断token有没有过期

某安某大型国企项目Oauth2实践(三)

如果在redis中没有找到对应的token key,说明该token已经过期,然后提示用户token已过期。

3.token刷新


到这一步说明token状态正常,那么如何刷新token了?如何保证用户在操作中token不会过期呢?

目前用到的方案有:客户端主动刷新服务端定时任务刷新redis expire。我们采用的是redis。

3.1 客户端主动刷新

服务端生成token后,会连同token的有效时间和截止失效时间返回给客户端。客户端在每次发起请求时都要先判断token是否即将过期,如果即将过期,则调用token刷新接口重新获取token,再使用新的token去访问系统。
但这种也存在用户在操作过程中(如耗时的操作),token过期的问题。

流程一般如下:


某安某大型国企项目Oauth2实践(三)

3.2 redis刷新

采用redis来维护token状态时,那么只需要保证用户在使用过程中token永不过期即可。即不用刷新token,只用延长token。这样只要用户在操作,那么token的有效期就会不断的刷新。 只用当用户不操作时,超过有效期时间,token才会在redis中过期。(这里同时还需要延长tokenSet 的key,来保证登录服务获取 tokenSet时能够拿到该token.)

只用使用redis expire 命令即可

以下是我们的整个token鉴权方案。

某安某大型国企项目Oauth2实践(三)

3.3 两种刷新方案的对比

每种方案都不是绝对的,要根据业务来定。

某安某大型国企项目Oauth2实践(三)

3.4 redis使用不当导致的生产事故

最开始在匹配所有相同的username 的token时,用的是keys操作,每个请求经过网关时,都要校验token,同时也会有keys操作,当碰到用户访问高峰期时,严重拖垮了redis性能,redis慢查询增多,读写大量超时,导致整个系统崩溃后面改成用set集合存储相同的username的token key这样就避免使用了keys操作。
       所以要严格遵守阿里编程规范。

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在消息队列中传递

这个时候需要在发送消息前切入,将token存入消息的header,在消费前将token从消息的header中取出。

以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中传递

我们在使用微服务时,必然会调用其他微服务,如何在调用过程中保证token的传递。
示例代码:
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实践(三)
关注我的你,是最香哒!


原文始发于微信公众号(小李的源码图):某安某大型国企项目Oauth2实践(三)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/145303.html

(0)
青莲明月的头像青莲明月

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!