最近在学习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 filter(ServerWebExchange exchange, GatewayFilterChain chain) {

String token = exchange.getRequest().getHeaders().getFirst(GET_TOKEN);

if (token == null || token.isEmpty()) {

return unAuthorize(exchange);

}

Map 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 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 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注册问题

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

参考文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。