商品微服务与用户微服务通信

本节继续讲解微服务架构下商品微服务的编码改造。由于微服务化的原因,各个功能模块都被拆分为独立的微服务,如已经开发了一部分的用户微服务和当前正在开发的商品微服务。独立的微服务有独立的数据库,想要获取不是本服务中的数据,肯定要与另外一个微服务实例进行通信。比如,本节将要讲解的就是在其他微服务实例中,通过与用户微服务通信获取当前登录用户的信息及鉴权的编码。

本节的源代码是在 newbee-mall-cloud-dev-step05 工程的基础上改造的,将工程命名为 newbee-mall-cloud-dev-step06

为什么需要调用用户微服务

以单体模式 newbee-mall-api 项目中后台管理系统商品模块中接口的定义为例,在商品列表接口方法定义中,有一个标注了 @TokenToAdminUser 注解的参数 adminUser,如图 4-8 所示。读者对这个参数的含义应该不陌生,它就是通过处理请求头中的 token 字段,来鉴权和获取当前登录管理员用户的身份信息。在单体模式下,所有功能模块的表都在一个数据库中,所有功能模块的编码也都在一个工程中,因此完成对 token 字段的处理并不复杂。

image 2025 04 23 13 34 11 107
Figure 1. 图4-8 newbee-mall-api项目商品列表接口源代码

而到了微服务架构下,对 token 字段的处理过程就变了。商品微服务是无法直接连接用户微服务数据库的,因此想要完成对请求头中 token 字段的处理就必须与用户微服务进行通信。实现方式是通过调用用户微服务暴露的接口来验证 token 字段是否正确,同时也能够通过 token 字段来获取当前登录的用户信息。当然,本小节讲解的是后台管理系统中管理员用户的处理步骤,后续改造中想要获取商城用户的信息,也需要与用户微服务进行通信,与这里所讲解的处理步骤类似。

不过,肯定会有读者有如下疑问:网关层不是已经做了鉴权吗?微服务实例中为什么又要做一次?

网关层确实做了一次鉴权的操作,实现方式为验证 token 字段。而微服务实例中对 token 字段的处理不仅仅是鉴权,大部分接口都需要用户的身份信息,即当前登录用户的数据 login_user_data。根据这个数据可以完成更细粒度的身份验证,如对于某些数据,A用户可以修改,但是B用户不可以修改;某个表中需要记录操作者信息,能够直接使用 login_user_data 中的数据;需要在日志中输出当前用户的身份信息,也可以直接使用 login_user_data 中的数据。因此,其他微服务实例调用用户微服务,主要是为了完成更细致的业务操作,以及获取当前登录用户的数据并用于后续的业务逻辑。

当然,也有读者会想另一个问题:如果某个接口中不需要获取当前登录用户的数据,网关层已经做了一次鉴权,是不是可以不处理 token 字段了呢?

在这种情况下是可以不处理 token 字段的,毕竟网关层做的鉴权操作已经满足基本要求了。不过,在真实的企业开发中,日志在输出时带上当前登录用户的信息是必不可少的步骤,有些开发团队的开发规范中也规定要获取当前登录用户的数据。因此,笔者建议最好不要省略对用户身份信息的处理。

商品微服务调用用户微服务编码实践

接下来进行实际的编码,在商品微服务中完成对用户微服务的调用及处理 token 字段。

第一步,修改用户微服务中暴露接口的 FeignClient 代码。

打开 newbee-mall-cloud-user-api 工程下的 pom.xml 文件,增加公用模块的依赖配置,代码如下:

<dependency>
    <groupId>ltd.newbee.cloud</groupId>
    <artifactId>newbee-mall-cloud-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

修改 NewBeeCloudAdminUserServiceFeign,把 getAdminUserByToken() 方法的返回类型修改为 Result,代码修改如下:

package ltd.user.cloud.newbee.openfeign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import ltd.common.cloud.newbee.dto.Result;

@FeignClient(value = "newbee-mall-cloud-user-service", path = "/users")
public interface NewBeeCloudAdminUserServiceFeign {

    @GetMapping(value = "/admin/{token}")
    Result getAdminUserByToken(@PathVariable(value = "token") String token);
}

第二步,修改商品微服务,引入 OpenFeignuser-api 依赖。

打开 newbee-mall-cloud-goods-web 工程下的 pom.xml 文件,增加远程通信所需的依赖配置和 newbee-mall-cloud-user-api 模块,新增配置代码如下:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<dependency>
    <groupId>ltd.user.newbee.cloud</groupId>
    <artifactId>newbee-mall-cloud-user-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

这里主要引入 LoadBalancer 依赖和 user-api 依赖,由于 user-api 中已经含有 OpenFeign 依赖配置,因此这里不需要额外增加。

第三步,增加配置,启用 OpenFeign 并使 FeignClient 类生效。

打开 newbee-mall-cloud-goods-web 工程,在项目的启动类 NewBeeMallCloudGoodsServiceApplication 上添加 @EnableFeignClients 注解,并配置相关的 FeignClient 类,代码如下:

@EnableFeignClients(basePackageClasses = {ltd.user.cloud.newbee.openfeign.
NewBeeCloudAdminUserServiceFeign.class})

这里使用 basePackageClasses 配置了需要使用的 FeignClient 类,即 NewBeeCloudAdminUserServiceFeign 类。接下来就可以直接使用 NewBeeCloudAdminUserServiceFeign 与用户微服务进行远程通信了。

第四步,修改 token 字段处理类中的逻辑代码。

打开 newbee-mall-cloud-goods-web 工程,修改 TokenToAdminUserMethodArgumentResolver 类中对 token 字段处理的逻辑代码,主要引入 NewBeeCloudAdminUserServiceFeign 类,通过调用用户微服务来获取管理员用户的数据。

修改后的代码如下:

@Component
public class TokenToAdminUserMethodArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private NewBeeCloudAdminUserServiceFeign newBeeCloudAdminUserService;

    public TokenToAdminUserMethodArgumentResolver() {
    }

    public boolean supportsParameter(MethodParameter parameter) {
        if (parameter.hasParameterAnnotation(TokenToAdminUser.class)) {
            return true;
        }
        return false;
    }

    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) throws Exception {
        if (parameter.getParameterAnnotation(TokenToAdminUser.class) != null) {
            String token = webRequest.getHeader("token");
            if (null != token && !"".equals(token) && token.length() == 32) {
                // 通过微服务获取用户信息
                Result result = newBeeCloudAdminUserService.getAdminUserByToken(token);

                if (result == null || result.getResultCode() != 200 || result.getData() == null) {
                    NewBeeMallException.fail("ADMIN_NOT_LOGIN_ERROR");
                }

                LinkedHashMap resultData = (LinkedHashMap) result.getData();
                // 将返回的字段封装到 LoginAdminUser 对象中
                LoginAdminUser loginAdminUser = new LoginAdminUser();
                loginAdminUser.setAdminUserId(Long.valueOf(resultData.get("adminUserId").toString()));
                loginAdminUser.setLoginUserName((String) resultData.get("loginUserName"));
                loginAdminUser.setNickName((String) resultData.get("nickName"));
                loginAdminUser.setLocked(Byte.valueOf(resultData.get("locked").toString()));
                return loginAdminUser;
            } else {
                NewBeeMallException.fail("ADMIN_NOT_LOGIN_ERROR");
            }
        }
        return null;
    }
}

执行逻辑如下。

  1. 获取请求头中的 token 字段,若不存在,则返回错误信息给前端;若存在,则继续后续流程。

  2. 通过 token 字段的值远程调用用户微服务,查询管理员用户的数据。如果远程调用时返回的结果类为空或未成功调用,则返回错误信息给前端;如果成功,则继续后续流程。

  3. 如果正确获取用户微服务响应的结果类,则将其转换为 LoginAdminUser 对象并配置到方法参数中供后续流程使用。

当然,这部分代码可以进行优化,即在 NewBeeCloudAdminUserServiceFeign 中通过注解的形式添加返回结果类中的类型,代码如下:

Result<LoginAdminUser> getAdminUserByToken(@PathVariable(value = "token") String token);

此,就不需要在远程通信的调用后使用 LinkedHashMap 接收返回结果,之后再一个字段接一个字段地转换和设置了。管理员用户 token 字段处理代码如此写的原因是为章节编排考虑,让读者更加清晰地了解这个步骤做了哪些事情,后续处理商城用户 token 字段时就会使用注解结果类的编码方式了。

至此,便完成了在商品微服务中通过远程通信获取当前登录用户的功能。

功能测试

为了让读者理解得更深刻,笔者新增了一个测试接口。在 newbee-mall-cloud-goods-web 工程中新建 ltd.goods.cloud.newbee.controller 包,并新建 NewBeeAdminGoodsCategoryController 测试类,新增代码如下:

package ltd.goods.cloud.newbee.controller;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import ltd.common.cloud.newbee.dto.Result;
import ltd.common.cloud.newbee.dto.ResultGenerator;
import ltd.goods.cloud.newbee.config.annotation.TokenToAdminUser;
import ltd.goods.cloud.newbee.entity.LoginAdminUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(value = "v1", tags = "后台管理系统分类模块接口")
@RequestMapping("/goods/admin")
public class NewBeeAdminGoodsCategoryController {

    private static final Logger logger = LoggerFactory.getLogger(NewBeeAdminGoodsCategoryController.class);

    @RequestMapping(value = "/testLoginAdminUser", method = RequestMethod.GET)
    @ApiOperation(value = "测试", notes = "测试")
    public Result testLoginAdminUser(@TokenToAdminUser LoginAdminUser adminUser) {
        logger.info("adminUser:{}", adminUser.toString());
        return ResultGenerator.genSuccessResult();
    }
}

因为测试接口的定义中标注了 @TokenToAdminUser 注解,所以 TokenToAdminUserMethodArgumentResolver 类会对 testLoginAdminUser() 方法中的 adminUser 参数进行前置处理。获取当前请求头中的 token 字段,并通过 OpenFeign 调用用户微服务中暴露的 /users/admin/{token} 接口获取用户数据,之后组装到当前方法的 adminUser 参数中。

编码完成后,准备好数据库和表就可以启动项目了。当然,在项目启动前需要启动 Nacos ServerRedis Server,之后依次启动 newbee-mall-cloud-goods-web 工程和 newbee-mall-cloud-goods-web 工程下的主类。启动成功后,就可以进行本章的功能测试了。如果能正常通过用户微服务获取用户信息并用于身份验证,则功能测试完成。

先打开用户微服务的 Swagger 页面,在浏览器中输入如下网址: http://localhost:29000/swagger-ui/index.html。

然后在该页面使用登录接口获取一个 token 值用于后续的功能测试,如笔者在测试时获取了一个值为 “6b9c0062841e2c9fd118002176b45cb7” 的 token 字段。

接下来打开商品微服务的 Swagger 页面,在浏览器中输入如下网址: http://localhost:29010/swagger-ui/index.html。

最后就可以在 Swagger 提供的 UI 页面进行接口测试了。

如果没有在请求头中设置 token 值或设置了错误的 token 值,则会收到一个错误的响应结果,如图 4-9 所示。

image 2025 04 23 13 55 37 828
Figure 2. 图4-9 未登录状态访问接口时的响应结果

请求结果中的 resultCode419messageADMIN_NOT_LOGIN_ERROR,表示当前请求被禁止访问了,因为当前用户的登录状态不正确。

如果在请求头中设置了正确的 token 值,则会收到一个正确的响应结果,如图 4-10 所示。

image 2025 04 23 13 56 22 547
Figure 3. 图4-10 正常登录状态访问接口时的响应结果

请求结果中的 resultCode200messageSUCCESS,表示当前用户的登录状态正常,并且获得了正常的结果响应。同时,在控制台上会看到一条打印当前登录用户信息的日志,如下所示:

2023-07-05 15:53:17.246 INFO 79019 --- [io-29010-exec-5] c.n.c.NewBee
AdminGoodsCategoryController : adminUser:LoginAdminUser(adminUserId=1,
loginUserName=admin, loginPassword=null, nickName=十三, locked=0)

功能测试通过,商品微服务可以正常与用户微服务通信,并实现微服务中用户的鉴权等功能。当然,读者在测试时,也可以用 debug 模式分别启动用户微服务和商品微服务,在涉及的代码中打上几个断点,在测试接口时就可以更好地观察整个流程。

本节主要介绍了调用用户微服务的原因,以及其他微服务该如何借助与用户微服务的远程通信来完成获取登录用户信息、鉴权等操作。不只是商品微服务,本实战项目中的其他微服务亦是如此,并且实现方式和编码思路基本一致。希望读者能够根据笔者提供的开发步骤顺利地完成本节的项目改造,后续章节中会继续完善商品微服务的功能。