SpringCloud系列教程:微服务的未来(十五)实现登录校验、网关传递用户、OpenFeign传递用户
前言
在微服务架构中,服务间的通信和身份验证是至关重要的环节。为了确保各个服务能够正确识别和验证用户信息,通常需要在请求中传递用户身份信息。本文将详细介绍如何实现登录校验、如何通过网关将用户信息传递到后端服务,并通过 OpenFeign 客户端在服务间进行用户信息的传递。通过这些步骤,我们能够确保在微服务之间传递的每个请求都能包含正确的用户身份信息,提升系统的安全性和一致性。
实现登录校验
需求:在网关中基于过滤器实现登录校验功能
提示:黑马商城是基于JWT实现的登录校验,目前相关功能在hm-service模块。我们可以将其中的WT工具拷贝到gateway模块,然后基于GlobalFilter来实现登录校验。
- uthProperties:配置登录校验需要拦截的路径,因为不是所有的路径都需要登录才能访问
- JwtProperties:定义与JWT工具有关的属性,比如秘钥文件位置
- SecurityConfig:工具的自动装配
- JwtTool:JWT工具,其中包含了校验和解析token的功能
- hmall.jks:秘钥文件
AuthProperties和JwtProperties所需的属性要在application.yaml中配置:
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi
创建全局过滤器 AuthGlobalFilter,用于处理 HTTP 请求时的认证和权限校验。代码实现了 GlobalFilter 接口以及 Ordered 接口,并通过注入 AuthProperties 和 JwtTool 实现认证相关的逻辑。
package com.hmall.gateway.filters;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import org.springframework.util.AntPathMatcher;
import java.util.List;
@RequiredArgsConstructor
@Component
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter , Ordered {
private final AuthProperties authProperties;
private final JwtTool jwtTool;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取request对象
ServerHttpRequest request = exchange.getRequest();
//判断是否需要做登录拦截
if(isExclude(request.getPath().toString())){
//放行
return chain.filter(exchange);
}
//获取token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if(headers != null && !headers.isEmpty()){
token = headers.get(0);
}
//校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
//拦截,设置响应状态码为401
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//TODO 传递用户信息
System.out.println(userId);
//放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
public boolean isExclude(String path){
// /search/**
for(String pathPattern : authProperties.getExcludePaths()){
//需要使用专门的匹配器
if(antPathMatcher.match(pathPattern, path)){
return true;
}
}
return false;
}
}
-
authProperties: 这是一个由 @EnableConfigurationProperties 注解自动加载的配置类对象,包含了来自配置文件的属性,主要用于获取 excludePaths(需要排除认证的路径)。
-
jwtTool: 一个工具类,用来解析和验证 JWT(JSON Web Token)并提取用户信息。
-
antPathMatcher: 一个 AntPathMatcher 对象,用于根据 Ant 风格的路径模式匹配 URL 路径。
-
请求路径匹配判断:
首先获取请求的路径 request.getPath().toString(),然后通过 isExclude 方法检查该路径是否在 authProperties.getExcludePaths() 列表中。如果该路径需要排除认证(例如公开接口),则直接调用 chain.filter(exchange) 放行请求,不进行进一步的认证。 -
获取 Token:
如果请求路径没有被排除,那么就从请求头中获取 authorization 字段的值(即 Token)。request.getHeaders().get(“authorization”) 会返回包含 Token 的列表,取第一个值作为 Token。 -
Token 校验:
使用 jwtTool.parseToken(token) 方法来解析 Token,并从中提取用户信息(如 userId)。如果 Token 无效或解析失败,会抛出 UnauthorizedException 异常。 -
认证失败处理:
如果 UnauthorizedException 被抛出,表示认证失败,则设置响应的状态码为 401 Unauthorized,并终止请求的处理,返回给客户端一个未授权的响应。 -
TODO 传递用户信息:
在实际项目中,可以将该信息存储在请求的上下文中,以便后续的服务调用使用。 -
放行请求:
如果认证成功,则调用 chain.filter(exchange) 放行请求,继续处理后续的过滤器。
未登录的url被成功拦截。
网关传递用户
一、在网关的登录校验过滤器中,把获取到的用户写入请求头
需求:修改gateway模块中的登录校验拦截器,在校验成功后保存用户到下游请求的请求头中。
提示:要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API,示例如下:
exchange.mutate()//mutate就是对下游请求做更改
.request(builder->builder.header("user-info",userInfo).build();
修改后的过滤器AuthGlobalFilter
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
.......
//传递用户信息
String userinfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userinfo))
.build();
//放行
return chain.filter(swe);
}
二、在hm-common中编写SpringMVC拦截器,获取登录用户
需求:由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器这样微服务只需要引入依赖即可生效,无需重复编写。
用于保存登录用户的ThreadLocal工具类UserContext类
package com.hmall.common.utils;
public class UserContext {
private static final ThreadLocal<Long> tl = new ThreadLocal<>();
/**
* 保存当前登录用户信息到ThreadLocal
* @param userId 用户id
*/
public static void setUser(Long userId) {
tl.set(userId);
}
/**
* 获取当前登录用户信息
* @return 用户id
*/
public static Long getUser() {
return tl.get();
}
/**
* 移除当前登录用户信息
*/
public static void removeUser(){
tl.remove();
}
}
定义拦截器UserInfoInterceptor类
package com.hmall.common.interceptors;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取登录用户信息
String userInfo = request.getHeader("user-info");
//判断是否获取了用户,如果有,存入ThreadLocal
if(StrUtil.isNotBlank(userInfo) ){
UserContext.setUser(Long.valueOf(userInfo));
}
//放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.removeUser();
}
}
同时定义配置类MvcConfig将拦截器UserInfoInterceptor添加到SpringMVC
package com.hmall.common.config;
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
//只在微服务里面生效,不在网关中生效
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
@ConditionalOnClass 是 Spring Boot 提供的一个条件注解,它表示只有当指定的类(在这里是 DispatcherServlet)存在时,才会加载此配置类。在这种情况下,DispatcherServlet 是 Spring MVC 的核心类,它是处理 HTTP 请求和响应的核心组件,因此这个配置类只有在 Spring MVC 环境下才会被加载和应用。
只不过这个配置类默认是不会生效的,因为它所在的包是com.hmall.common.config,与其它微服务的扫描包不一致,无法被扫描到。基于Springboot的自动装配原理,可以将其添加到resources目录下的META-INF/spring.factories文件中:
spring.factories文件内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
OpenFeign传递用户
微服务项目中的很多业务要多个微服务共同合作完成,而这个过程中也需要传递登录用户信息,例如:
交易服务提交订单后,需要清空购物车内的物品,此时需要知道用户id才可以清空对应的购物车,但订单服务调用购物车时并没有传送用户信息;通过网关的全局过滤器传送的请求头的用户信息只会到交易服务。
微服务之间的调用是通过OpenFeign来实现。
OpenFeign中提供了一个拦截器接口,所有由0penFeign发起的请求都会先调用拦截器处理请求:
public interface RequestInterceptor {
/**
* Called for every request.
* Add data using methods on the supplied {@link RequestTemplate}.
*/
void apply(RequestTemplate template);
}
package com.hmall.api.config;
import com.hmall.common.utils.UserContext;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public RequestInterceptor userInfoRequestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = UserContext.getUser();
if (userId != null) {
requestTemplate.header("user-info",userId.toString() );
}
}
};
}
}
- RequestInterceptor 是 Feign 客户端提供的一个接口,允许你在每个 Feign 请求发起之前进行拦截操作。你可以在 apply 方法中修改请求头、请求参数等内容。
- RequestTemplate 是 Feign 请求的模板对象,它允许你修改请求的各种属性,包括 URL、请求方法、请求体和请求头等。
- 在 RequestInterceptor 中,apply 方法会在每次 Feign 请求发起时被调用。
- 通过 requestTemplate.header 方法,可以动态地向请求头中添加信息。
为每个 Feign 请求自动附加一个名为 user-info 的请求头,值为当前用户的 ID。这通常用于服务间的身份信息传递,允许接收方知道是谁发起的请求,以进行身份验证或其他处理。
总结
本文通过讲解如何在微服务中实现登录校验、通过网关传递用户信息以及利用 OpenFeign 传递用户信息的方式,展示了用户身份在分布式架构中的重要性。通过这些方法,不仅能够确保服务间的身份验证,还能保证系统在进行跨服务调用时的一致性和安全性。在实际开发中,合理配置用户信息传递和验证机制,能够有效提升系统的安全性并减少潜在的安全风险。