微服务项目如何实现登录校验

微服务项目如何实现登录校验


在单体架构的项目中,我们做登录校验的方式是让前端的请求携带登录令牌Token,后端定义一个拦截器来拦截前端的请求,并且解析令牌,如果解析生成则把用户信息保存到ThreadLocal中,如果令牌解析失败则返回未登录异常信息

这里以JWT校验为例:

解析JWT成功
解析JWT失败
前端请求携带JWT
后端拦截器拦截请求
保存用户信息到ThreadLocal
返回未登录异常信息
继续处理请求
前端提示未登录

但是在微服务项目中,各个模块部署在不同的服务器上,而请求也不会直接到达后端接口,而是请求到达网关,网关再路由到不同的微服务模块上,而各个模块之间也要进行通信,通信时部分业务也会要使用用户信息

在做登录校验时会遇到以下问题:

  • 各模块分别部署,无法共享同一个ThreadLocal
  • 多个模块都要定义自己的拦截器,但拦截器逻辑基本相同
  • 模块之间通信该如何携带用户信息

微服务架构:

前端请求
网关
微服务A
OpenFegin
微服务C
微服务B

那在微服务中我们改怎么去做登录校验呢

网关过滤器

如果前端的请求不再直接到达后端接口,而是网关的话,那我们的登录校验当然要放到网关中实现,只有当校验通过才能将请求路由到对应的接口上,那我们要在网关服务中定义一个拦截器吗

网关请求流程

当请求到达网关后,网关会按照自己的处理流程来处理请求:

FilterChain
PRE
POST
PRE
POST
PRE
POST
Filter1
Filter2
.....
NettyRoutingFilter
客户端
HandlerMapping
WebHandler
微服务
  • HandlerMapping:路由映射器,根据请求找到匹配的路由
  • WebHandler:请求处理器,加载配置的多个过滤器,放入集合中排序
  • FilterChain:过滤器链,一个过滤器的集合,过滤器会在请求路由到微服务之前和之后执行,于是分为两部分逻辑:PRE和POST
  • NettyRoutingFilter:负责将请求转发到微服务,当微服务返回后存入上下文

由此可见,网关有一条自己的过滤器链,那么我们就可以自定义一个过滤器将他加到过滤器链中完成登录校验

在网关的微服务模块中自定义一个过滤器,需要实现两个接口:GlobalFilter:全局过滤器,包含fillter过滤方法,Ordered:用来指定在过滤器链中的位置

@Component
@RequiredArgsConstructor
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) {
        //1.获取Request对象
        ServerHttpRequest request = exchange.getRequest();
        //2.判断是否需要拦截
        if (isExclude(request.getPath().toString())){
            //放行
            return chain.filter(exchange);
        }
        //3.获取Token
        String token = null;
        List<String> headers = request.getHeaders().get("authorization");
        if (headers!=null&&!headers.isEmpty()){
            token = headers.get(0);
        }
        //4.解析Token
        Long userId = null;
        try {
            userId = jwtTool.parseToken(token);
        }catch (UnauthorizedException e){
            //终止请求:设置响应状态码401
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }
        //5.传递用户信息
        System.out.println("userId:"+userId);
        String userInfo = userId.toString();
        ServerWebExchange swe = exchange.mutate()
                .request(builder -> builder.header("user-info", userInfo))
                .build();
        //6.放行
        return chain.filter(swe);
    }
    
    // 比对需要放行的请求
    private boolean isExclude(String path) {
        for (String pathPattern : authProperties.getExcludePaths()) {
            if (antPathMatcher.match(pathPattern, path)){
                return true;
            }
        }
        return false;
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

这里很重要的一步就是传递用户信息,其中获取令牌解析令牌的过程与单体架构基本无异,但是解析获取用户信息之后就不能再存入ThreadLocal中了,因为网关服务有自己的ThreadLocal,并不能与微服务模块共享,因此要把用户信息存放在请求头中

这里用到了exchangemutate方法构建一个新的请求头放入请求中,放行后需要传入更改后的ServerWebExchange对象


微服务拦截器

网关做完登录校验之后,请求就到达了各个微服务模块,现在就可以把用户信息放入ThreadLocal中了,方便当前模块随时获取用户信息,于是我们可以定义一个微服务拦截器

这个拦截器需要拦截到达每一个微服务的请求,所以要定义在一个共享资源模块(Common)中,这个拦截器不再需要校验登录,因为在网关过滤器中已经做过了,她所需要做的只是获取用户信息并放入ThreadLocal中,这里将ThreadLocal封装到了一个自定义UserContext类中

public class UserInfoInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取用户信息
        String userInfo = request.getHeader("user-info");
        //2.是否获取了用户信息
        if (StrUtil.isNotBlank(userInfo)){
            UserContext.setUser(Long.valueOf(userInfo));
        }
        //3.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //清理用户
        UserContext.removeUser();
    }
}

当然不要忘记在配置类中注册自定义拦截器

@Configuration
@ConditionalOnClass(DispatcherServlet.class) // 这个注解很重要!!!
//网关服务会找不到WebMvcConfigurer,因为网关底层并不是SpringMVC,需要排除网关服务
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}

但是此时我们的拦截器并没有生效,因为配置类要想生效必须被Spring包扫描组件扫描到,但共享资源模块肯定不与业务模块在同一个包下,因此我们需要利用Spring自动装配的原理来让拦截器生效

resources目录下的META-INF文件夹下定义一个spring.factories文件来记录配置类

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MvcConfig

现在我们的各个微服务模块就能获取到校验后的用户信息了


OpenFeign拦截器

虽然通过上面的代码,我们的微服务已经可以拿到用户信息了,但是微服务与微服务之间有时也需要通信,比如说在购物车模块中,通过购物车下单后,要通知订单模块来生成订单,这个订单中也要包含用户信息,那在订单模块中,用户信息还能通过微服务拦截器拿到吗

当然拿不到,微服务拦截器之所以能拿到用户信息是因为网关校验解析后将用户信息放入了请求头中,但是微服务模块之间的通信是基于OpenFeign为我们代理生成的请求,我们并没有将用户信息放入请求头中,因此微服务过滤器拿不到

还有一种办法就是将用户信息作为请求参数传递到下一个微服务,但是这种方法要在每一次请求都携带用户信息,并且浪费了我们上面定义的微服务过滤器,因此我们要想办法在每次通过OpenFeign发送请求时都将用户信息添加到请求头中

因此我们可以定义一个OpenFeign拦截器:

OpenFegin中提供了一个拦截器接口:RequestInterceptor,每次由OpenFegin发起的请求都会先调用拦截器处理请求,我们可以直接用匿名内部类返回接口实现,只需要把用户信息放入请求头中

public class DefaultFeignConfig {

    @Bean
    public RequestInterceptor userInfoRequestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userInfo = UserContext.getUser();
                if (userInfo != null) {
                    requestTemplate.header("user-info",userInfo.toString());
                }
            }
        };
    }
}

这样我们就完成了一个完整的微服务用户登录校验


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值