当前位置: 首页 > article >正文

[Java]微服务体系下的用户身份认证方案

需求分析

网关请求处理流程

网关登录校验的核心

  1. 如何在网关转发之前做登录校验?
  • 通过自定义过滤器, 请求到达微服务之前, 进行登录校验
  1. 网关如何将用户信息传递给微服务?
  • 通过在请求头中携带JWT令牌, 向后传递用户信息(网关内部的请求工具)
  1. 如何在微服务之间传递用户信息?
  • 通过在请求头中携带JWT令牌, 向后传递用户信息(请求工具OpenFeign)

自定义过滤器

网关过滤器有两种,分别是:

  1. GatewayFilter: 路由过滤器,作用于任意指定的路由; 默认不生效,要配置到路由后生效
  2. GlobalFilter: 全局过滤器,作用范围是所有路由; 声明后自动生效
  3. 两种过滤器的过滤方法签名完全一致:

解读全局过滤器的信息

定义全局过滤器: 两种过滤器的定义存在差异, 全局过滤器的定义简单一些, 实现GlobalFilter接口即可

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求
        ServerHttpRequest request = exchange.getRequest();
        // 过滤器业务处理
        System.out.println("GlobalFilter pre阶段执行了");
        HttpHeaders headers = request.getHeaders();
        System.out.println("headers = " + headers);
        // 反放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 过滤器执行顺序, 值越小, 优先级越高
        return 0;
    }
}

自定义GatewavFilter是实现 AbstractGatewayFilterFactory 工厂,示例如下:

  1. 优势: 可以自由的指定作用的返回, 而且在配置时可以指定自定义参数, 非常灵活
  2. 劣势: 定义起来比较麻烦
  3. 定义无参的GatewavFilter过滤器

  1. 定义无参的GatewavFilter过滤器, 并设置执行顺序

  1. 定义有参的GatewavFilter过滤器

实现登录校验

需求: 在网关中基于过滤器实现登录校验功能

  • 黑马商城是基于IWT实现的登录校验,目前相关功能在hm-service模块

  1. 将JWT工具类拷贝到gateway模块

  • 拷贝过来的AuthProperties类会报错, 表示该类没有生效, 添加@Component注解就可以了

  1. 将工具方法拷贝到gateway模块

  1. 把JWT的一些配置复制到配置文件中
hm:
  jwt:
    location: classpath:hmall.jks
    alias: hmall
    password: hmall123
    tokenTTL: 30m
  auth:
    excludePaths:
      - /search/**
      - /users/login
      - /items/**
      - /hi
  1. 创建GlobalFilter过滤器, 实现登录校验
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    // 注入配置文件中的属性 (排除路径)
    private final AuthProperties authProperties;
    // 注入jwt工具类
    private final JwtTool jwtTool;
    // Spring提供的专门用于路径匹配的工具
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //1. 获取request
        ServerHttpRequest request = exchange.getRequest();
        //2. 判断是否需要做登录拦截
        String path = request.getPath().toString();
        if (isExclude(path)){
            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);
        //6. 放行
        return chain.filter(exchange);
    }

    private boolean isExclude(String path) {
      for (String excludePath : authProperties.getExcludePaths()) {
            if (antPathMatcher.match(excludePath, path)) {
                return true;
            }
        }

        return false;
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
  1. 访问需要校验的接口

网关传递用户信息

网关传递用户信息到微服务的流程

  1. 网关在自定义过滤器中拿到请求头中的用户信息, 并进行校验, 没问题后, 把用户信息传递给后面的微服务
  2. 在微服务中定义拦截器, 拿到用户信息, 然后保存到ThreadLocal中, 后续业务就可以随时使用了

在网关的登录校验过滤器中, 把获取到的用户写入请求头

@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
   ... ...

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ... ...
        
        //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);
    }

   ... ...
}
  1. 需求: 修改gateway模块中的登录校验拦截器,在校验成功后保存用户到下游请求的请求头中
  2. 提示: 要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API
  3. 使用 exchange 上下文对象的mutate 方法对请求做修改
  4. mutate对象返回构建器对象builder, 使用builder中的request方法对请求做修改
  5. request方法会返回requestBuild, 可以使用lambda表达式, 对请求头或者其他部分做处理
  6. 定义的请求头的名字可以随便写, 但是取值时也要用这个名字, 建议定义为常量, 请求头的内容需要是字符串
  7. 请求修改完成后会返回新的ServerWebExchange, 放行时把最新的请求上下文传递过去, 修改就生效了
  8. 测试一下: 在购物车微服务中, 修改查询购物车的controller, 拿到请求头中用户信息
@Api(tags = "购物车相关接口")
@RestController
@RequestMapping("/carts")
@RequiredArgsConstructor
public class CartController {
    @ApiOperation("查询购物车列表")
    @GetMapping
    public List<CartVO> queryMyCarts(@RequestHeader(value = "user-info", required = false) String userInfo) {
        System.out.println("userInfo =" + userInfo);
        return cartService.queryMyCarts();
    }

}

  1. 说明网关成功从token中解析出用户信息, 并且通过请求头传递给了下游微服务
  2. 在controller中通过注解形式获取用户信息, 比较麻烦

在hm-common中编写SpringMVC拦截器,获取登录用户

  1. 由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器
  2. 这样微服务只需要引入依赖即可生效,无需重复编写
  3. 定义拦截器, 储存用户信息

public class UserInfoInterceptor implements HandlerInterceptor {
    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.parseLong(userInfo));
        }

        // 3.放行
        return  true;
    }

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // controller执行完成后清理用户
        UserContext.removeUser();
    }
}
  • StrUtil.isNotBlan()时hutool工具包提供的判断字符串的方法, 可以判断null, 空字符串, 空格等多种情况, 比较严谨
  • UserContext工具类中的getUser()使用的非常多, 可以使用ctrl + alt + f7 查看使用的地方

  1. 把cart-service中写死的用户id改造为动态获取
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {

    @Override
    public List<CartVO> queryMyCarts() {
        // 1.查询我的购物车列表
        // todo: UserContext.getUser() => 写死1 ,待优化
        List<Cart> carts = lambdaQuery().eq(Cart::getUserId,1L ).list();
        List<Cart> carts = lambdaQuery().eq(Cart::getUserId,UserContext.getUser()).list();

        ... ...
    }
}
  1. 注册拦截器, 让拦截器生效
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}
  1. 拦截器的配置是在hm-common模块下, 其他微服务默认是扫描不到的, 所以并不会生效, 所以要把配置类放到META-INF文件下的spring.factories中, 就能实现自动装配了

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.hmall.common.config.MyBatisConfig,\
  com.hmall.common.config.MvcConfig,\
  com.hmall.common.config.JsonConfig
  1. WebMvcConfigurer属于SpringConfigMvc包下, 网关底层不是SpringMvc那一套, 而是非阻塞的响应式编程技术, 基于WebFlux, 所以我们在网关中引入hm-common时, 拦截器的配置类会被加载, 就会报错
  2. 所以我们要让拦截器的配置类按需加载, SpringBoot的自动装配是可以带条件的, 让配置类在微服务中生效, 在网关中不生效, 判断条件就是是否存在SpringMvc, SpringMvc的核心是DispatcherServlet
@Configuration
@ConditionalOnClass(DispatcherServlet.class) // springboot自动配置的条件注解
public class MvcConfig implements WebMvcConfigurer {

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor());
    }
}
  1. 测试一下: 动态拿到用户id, 查询购物车数据

OpenFeign传递用户

微服务项目中的很多业务要多个微服务共同合作完成,而这个过程中也需要传递登录用户信息,例如:

OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求:

其中的RequestTemplate类中提供了一些方法可以让我们修改请求头:

解决方案: 使用OpenFeign调用其他微服务时, 在请求头中携带用户信息, 其他服务就可以拿到用户信息了

  1. 在hm-api引入common模块, 因为要使用ThreadLocal工具类
    <!-- common-->
    <dependency>
        <groupId>com.heima</groupId>
        <artifactId>hm-common</artifactId>
        <version>1.0.0</version>
    </dependency>
  1. 在hm-api中添加OpenFeign的拦截器接口
public class DefaultFeignConfig {

    @Bean
    public RequestInterceptor feignInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                Long userId = UserContext.getUser();
                if (userId != null) {
                    requestTemplate.header("user-info", userId.toString());
                }
            }
        };
    }
}
  • 注意: 交易服务的请求是从网关转发过来的, 所以交易服务是一定可以拿到请求头中的用户信息的
  1. DefaultFeignConfig这个配置生效的前提是要加在OpenFeign的启动类上
/**
 交易服务
*/
@MapperScan("com.hmall.trade.mapper")
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@SpringBootApplication
public class TradeApplication {
    public static void main(String[] args) {
        SpringApplication.run(TradeApplication.class, args);
    }
}
  1. 重启交易服务, 提交订单后, 会删除购物车中已经结算的商品

小结

为了解决微服务体系下的用户登录校验和用户信息传递, 使用了几种拦截器, 现在梳理一下


http://www.kler.cn/a/409883.html

相关文章:

  • librdns一个开源DNS解析库
  • CTFHUB--yeeclass-web
  • docker pull命令拉取镜像失败的解决方案
  • 《第十部分》1.STM32之通信接口《精讲》之IIC通信---介绍
  • ROS2 报错记录
  • Spring Boot3远程调用工具RestClient
  • 【MySQL】数据库精细化讲解:内置函数知识穿透与深度学习解析
  • C++:用红黑树封装map与set-2
  • 数据结构每日一题|判断链表环形结构并返回环的起始节点
  • QT6 android生成release版本注意事项
  • 【VRChat 改模】着色器(shader)简介、预制体(prefab)简介
  • 日志抽取工具——flume的安装与使用教程
  • 学习路之压力测试--jmeter安装教程
  • 施密特正交化与单位化的情形
  • 排序算法1
  • C++设计模式-策略模式-StrategyMethod
  • 如何在 PyTorch 分布式训练中使用 TORCH_DISTRIBUTED_DEBUG=INFO 进行调试
  • Spring Boot 同时接受文件和实体及 Postman 测试实战
  • Vue3(JavaScript框架)(响应式数据ref,v-on、v-show、v-is、v-for、v-bind)
  • Linux网络——NAT/代理服务器
  • DAMODEL丹摩| 智谱清影 -CogVideoX-2b-部署与使用
  • 使用 Maven 构建一个简单的 Java 项目
  • C#水仙花
  • 请求响应(学习笔记)
  • 亚信安全发布《2024年第三季度网络安全威胁报告》
  • SPFA算法