网关校验token
两种方式,但是一般采用GlobalFilter来做校验,
网关里面存在一系列的过滤器,先执行下图的pre,校验都通过后再发给微服务,最后由post执行,本质上是责任链模式。而我们的GlobalFilter需要放在下图最后一个过滤器Nettty...前面。学过责任链的朋友知道,我们可以让方法继承ordered接口来控制责任链上过滤器的执行顺序,接着往下看代码。
GlobalFilter代码
@Component
public class LoginFilter implements GlobalFilter, Ordered {
//这个是不参加过滤的路径,需要自己去写这个类。
@Resource
private AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//exchange是上下文参数,例如request、response、session等都在里面
ServerHttpRequest request = exchange.getRequest();
//获取路径查看是否需要放行
RequestPath path = request.getPath();
if (checkPath(path.toString())) {
return chain.filter(exchange);
}
//获取token
String token=null;
List<String> authorization = request.getHeaders().get("token");
if(authorization != null && !authorization.isEmpty()){
token=authorization.get(0);
}
Long userId = null;
try {
boolean valid = JwtUtil.validate(token);
if (!valid) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String string = token.toString();
//把token放在请求头传给下一个服务
ServerWebExchange swe = exchange.mutate().request(
builder -> builder.header("token", string)
).build();
return chain.filter(swe);
}
@Override
public int getOrder() {
return 0;
}
public boolean checkPath(String path){
for (String excludePath : authProperties.getExcludePaths()) {
if (antPathMatcher.match(excludePath, path)) {
return true;
}
}
return false;
}
}
这段代码放在网关里面,由于有@Component注解,spring会自动把他加入到责任链里面去,根据order顺序执行,上面order是0,优先级最高。
服务又如何处理token
现在有个下图需求,需要在拦截器里面把网关传来的token放在threadlocal里面,而且整个微服务只写一遍这个拦截器逻辑,达到通用效果。
只写一遍,我们就可以把这个 拦截器写在common包下面。
拦截器逻辑
由于网关已经做校验,这里只需要把数据放入ThreadLocal,然后之间放行即可。
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader(CommonCanstant.TOKEN_HEADER);
if (StrUtil.isNotBlank(token)){
Long id = JwtUtil.getJSONObject(token).getLong(CommonCanstant.USER_ID);
Map<String, Long> map = new HashMap<>();
map.put(CommonCanstant.USER_ID,id);
ThreadLocalContext.set(map);
}
return true;
}
}
加入拦截器
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor());
}
}
大家应该看见上面有个@Component和@Configuration注解,这是spring会自动扫描的,但是这种结构下面可扫描不到,springboot自动装配只能扫描到启动类同级目录及子目录的注解,像上图common里面的注解根本扫描不到,该如何解决呢?
要像扫描到非同级目录的包,就需要定义下图文件(springboot 2.7以下用这个,如果是2.7以上往下看,还有另一种方法)
文件内把common包下的文件加进去就行。反斜杠是为了换行用的。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ccc.config.LoginInterceptor,\
com.ccc.config.WebMvcConfiguration
Spring Boot 2.7中不推荐使用/META-INF/spring.factories文件,并且在Spring Boot 3将移除对/META-INF/spring.factories的支持。
新的写法是创建一个新的文件:
/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
内容直接放配置类即可(注意有spring目录),比如这样:
com.ccc.config.LoginInterceptor
com.ccc.config.WebMvcConfiguration
到这里,好没有结束,我们知道网关模块不能引用springmvc的,但是上述添加拦截器的那个类 extends WebMvcConfigurationSupport,而WebMvcConfigurationSupport属于springmvc的,恰巧网关又添加了common的依赖,这个冲突该如何解决呢?
<dependency>
<groupId>com.ccc</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<exclusions>
<!-- Spring Cloud Gateway 项目中不能使用 spring-boot-starter-web 依赖-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</exclusion>
</exclusions>
</dependency>
在WebMvcConfiguration里加上下面注解就行了,这个注解是进行有条件的装配,不符合条件的不进行自动装配,以springmvc作为条件,网关没用springmvc,而springmvc核心类是DispatcherServlet.class。现在问题完美解决。