背景

在我工作的项目中使用了RuoYi-Vue-Plus,是一套分布式的框架,支持多租户。

而这次要讲到的案例是一个关于登录的流程,该流程涉及到鉴权模块(Auth)系统模块(System),前端在执行登录操作时,先携带登录信息向Auth发起请求。Auth拿到用户名再调用Sytem模块Feign接口,通过userId获取用户信息的,最终比对密码是否正确。

示例代码

Auth模块

  • 实体

@Data
public class LoginBody {
    private String username;
    private String password;
}
  • Controller

@RestController
@RequiredArgsConstructor
public class AuthColler {

    private final RemoteUserService remoteUserService;

    /**
     * 登录方法
     */
    @PostMapping("/login")
    public R<LoginVo> login(@RequestBody LoginBody body) {
        // 调用Feign接口获取用户信息
        SysUser user = remoteUserService.getUserInfo(username);
        
        // 判断是否有效
        if(user == null) {
          throw new RuntimeException("用户" + body.getUsername + "不存在");
        }
        if(user.getTenantId == null) {
          throw new RuntimeException("用户" + body.getUsername + "租户id不应该为空");
        }

        // 封装成vo并返回前端
        LoginVo loginVo = BeanUtils.copyProperties(user, LoginVo.class);
        return R.ok(loginVo);
    }
}

System模块

  • Feign接口

@FeignClient("system")
public interface RemoteUserService {
    @GetMapping({"/user/getUserInfoByUserName"})
    LoginUser getUserInfo(String username)
}
  • Controller

/**
 * 用户信息
 */
@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class SysUserController {

    private final SysUserService userService;

    /**
     * 通过用户名查询用户信息
     */
    @GetMapping("/getUserInfoByUserName")
    public SysUser getUserInfo(String username) {
        // 假设Service层调用Dao层查找了数据
        SysUser sysUser = userService.getUserInfoByUserName(username);

        if (ObjectUtil.isNull(sysUser)) {
            throw new RuntimeException("用户" + username + "不存在");
        }

        return sysUser;
    }
}

出现的问题

当使用不存在的用户名测试登录时,登录接口报的错不是用户不存在,而是租户id不应该为空

如果查找System模块的日志,却看到System模块正确抛出了用户不存在的异常,那为什么Auth模块没能正确处理?

问题点

问题出现的原因就在System模块接口的返回值上,该接口并没有使用类似Auth模块代码里的R<?>作为统一返回。并且Controller使用了@RestController注解,意味着调用结果会通过Json字符串的格式返回了用户不存在的异常数据。

那么Feign接口的调用方在收到响应的Json字符串会做什么?如果字符串不为空,会创建对象并尝试映射值到对象的每一个字段。问题点就在这,既然对象已经创建,判空就没有意义,自然不能正确处理用户不存在的情况。

反思

从这个案例不难看出,远程过程调用的接口选择在Controller层的原因,以及统一返回的重要性。一方面远程过程调用的结果不像是在同个进程内部的代码一样可控,这种调用更接近前端到后端的两端调用关系。另一方面,使用统一返回和自定义的状态码,可以更加灵活地判断调用结果,根据结果不同进行相应的处理。