【SpringCloud Gateway】SpringCloud各微服务之间用户登录信息共享的实现思路——gateway网关token校验以及向微服务发送请求携带token

        最近在学习SpringCloud项目时,想到了一些问题,各个微服务分别部署在不同的服务上,由naocs作为注册中心实现负载均衡,彼此之间通过Feign相互调用通信,信息同步并不像单体项目那样方便,传统单体项目的登录验证方式似乎在SpringCloud中不能满足项目的需求。那么当用户完成登录后,各微服务该如何确认用户的登录状态呢?

        下面有几种实现思路:

  • 统一认证中心:建立一个单独的认证中心,例如使用Spring Security或者基于OAuth的认证服务。每个微服务都需要将用户的登录请求导向认证中心,认证中心负责验证用户身份。认证中心可以颁发访问令牌,微服务通过访问令牌进行鉴权。
  • JWT (JSON Web Tokens):使用JWT来实现身份验证和授权。认证中心颁发包含用户信息的JWT令牌,微服务在收到请求时验证JWT令牌的有效性,并提取其中的用户信息。这样,用户信息可以在不同微服务之间共享。
  • 消息队列:使用消息队列(如RabbitMQ、Kafka)来在微服务之间传递用户登录信息。当用户登录或注销时,认证中心可以发布消息,其他微服务订阅这些消息以更新用户状态。
  • 分布式缓存:使用分布式缓存(如Redis)来存储用户登录信息。当用户登录时,在认证中心将用户信息缓存到Redis中,其他微服务可以查询Redis以获取用户信息。

        这里为大家提供一种较为简单的方式:使用Redis分布式缓存储存用户登录信息。在gateway微服务中配置过滤器,在过滤器中获取到达网关的请求所携带的token信息,如果token为空或token对应的key在Redis中不存在,向用户返回401 UNAUTHORIZED的状态码;如果token验证正确,便刷新Redis中对应key的TTL,并继续向负载均衡的请求地址发送请求且携带相应的token信息。在接收请求的微服务中编写拦截器,在拦截器中获取token并通过Redis拿取对应的用户信息。

        gateway中的过滤器代码实现:

@Order(1)
@Configuration
public class GlobalFilterConfig implements GlobalFilter {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst(GET_TOKEN);
        if (token == null || token.isEmpty()) {
            return unAuthorize(exchange);
            
        }
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(LOGIN_TOKEN_KEY + token);
        if (map.isEmpty()) {
            return unAuthorize(exchange);
            
        }
        
        // 刷新TTL
        stringRedisTemplate.expire(LOGIN_TOKEN_KEY + token, 30, TimeUnit.MINUTES);
        
        //把新的 exchange放回到过滤链
        ServerHttpRequest request = exchange.getRequest().mutate().header(GATEWAY_TOKEN, token).build();
        ServerWebExchange newExchange = exchange.mutate().request(request).build();
        return chain.filter(newExchange);
        
    }
    
    // 返回未登录的自定义错误
    private Mono<Void> unAuthorize(ServerWebExchange exchange) {
        // 设置错误状态码为401
        exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
        // 设置返回的信息为JSON类型
        exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
        // 自定义错误信息
        String errorMsg = "{\"error\": \"" + "用户未登录或登录超时,请重新登录" + "\"}";
        // 将自定义错误响应写入响应体
        return exchange.getResponse()
                .writeWith(Mono.just(exchange.getResponse().bufferFactory().wrap(errorMsg.getBytes())));
    }
}

        注意需要使用@Order注解为该全局过滤器设置优先级。当过滤器的order值一致时,过滤器的执行顺序为:defaultFilter>路由过滤器>GlobalFilter,因此该过滤器的order值应当设置为较小值,以确保该全局过滤器的正确执行。(order值越小,优先级越高,执行顺序越靠前)

        微服务中拦截器的代码实现:

public class LoginHandlerInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    
    // 由于该类未交给spring管理,因此不能使用自动装配的方式获取RedisTemplate对象
    public LoginHandlerInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader(GATEWAY_TOKEN);
        if (token == null || token.isEmpty()) {
            return false;
        }
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(LOGIN_TOKEN_KEY + token);
        if (map.isEmpty()) {
            return false;
        }
        UserDto userDto = BeanUtil.toBean(map, UserDto.class);
        UserContext.saveUser(userDto); // 将用户信息放入线程中
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.removeUser();
    }
}
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginHandlerInterceptor(stringRedisTemplate))
                .addPathPatterns("/**");
    }
}

        到这里我们就完成了gateway到微服务的用户登录信息传递。接下来就需要解决微服务与微服务之间的登录信息传递问题。在这个项目中各微服务通过分Feign实现相互调用通信,那么我们只需要在调取Feign时携带token信息就好:

@FeignClient(name = "test-gateway")
public interface ExampleClient {

    @GetMapping("/api/example")
    String getExampleData(@RequestHeader("token") String token);
}

        但每次调用该Feign接口时都需要我们手动传入token值,不太优雅,因此采用下面的方式来配置Feign,每当Feign接口被调用时就会携带token信息:

public class FeignRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header(GET_TOKEN, TokenContext.getToken());
    }
}
@FeignClient(name = "test-gateway", configuration = FeignRequestInterceptor.class)
public interface ExampleClient {

    @GetMapping("/api/example")
    String getExampleData(@RequestHeader("token") String token);
}

        若遇到启动时报错A bean with that name has already been defined and overriding is disabled可以看这篇文章:【SpringCloud】使用OpenFeign的spring项目启动时报错bean注册问题

        至此就完成了最基础的微服务登录信息传递。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狐笙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值