【基于 Swoole+Hyperf 的微服务实战】第九周·周二:为电商核心系统填充第一个业务服务——用户服务
今天我们进入第九周周二,开始为电商核心系统填充第一个业务服务——用户服务。昨天我们已经设计好了微服务架构、Proto 契约和数据库 Schema,今天将亲手实现用户注册、登录和 JWT 签发,让用户服务真正运行起来,注册到 Consul,并通过网关对外提供服务。这是整个电商系统的认证基石,后续所有服务的用户身份都将依赖于此。
今日目标
- 创建
user-service项目骨架,配置数据库、Redis、Consul 注册。 - 根据 Proto 契约实现
UserServiceRPC 接口:Register、Login、GetUserById。 - 集成
hyperf/jwt在用户服务端签发 Token,登录时返回 JWT。 - 编写数据库迁移和模型,保证密码加密存储(Bcrypt)。
- 启动用户服务,验证 Consul 注册成功,并通过网关和 RPC 直接测试接口。
- 在网关层为
/auth/*路由配置反向代理到用户服务,初步实现统一入口。
一、项目创建与配置(约 45 分钟)
我们将在services/user目录下创建独立的 Hyperf 项目,并将其加入到 Docker Compose 编排中。
1. 创建项目
进入容器(或直接在宿主机利用 Composer):
docker-composeexecswoolebashcd/var/www/servicescomposercreate-project hyperf/hyperf-skeleton usercduser2. 安装必要组件
composerrequire hyperf/database hyperf/redis hyperf/json-rpc hyperf/jwt hyperf/service-governance-consul3. 配置数据库
编辑.env:
DB_DRIVER=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=user_db DB_USERNAME=root DB_PASSWORD=root配置config/autoload/databases.php指向上述环境变量。
4. 配置 Redis、Consul 注册
config/autoload/redis.php使用默认的redis容器。config/autoload/services.php配置 Consul 驱动(参考第四周配置):
'drivers'=>['consul'=>['driver'=>Hyperf\ServiceGovernance\Driver\ConsulDriver::class,'uri'=>'http://consul:8500','check'=>['ttl'=>10,'deregister_critical_service_after'=>'30s',],],],5. 配置 JWT
发布配置:php bin/hyperf.php vendor:publish hyperf/jwt
编辑config/autoload/jwt.php,设置密钥和有效期。
6. 创建数据库表
在user_db中执行昨天编写的users表,或通过迁移文件创建。
二、知识核心:密码加密、JWT 与 RPC 实现(约 1 小时)
1. 密码存储
绝不能存储明文密码。使用password_hash(Bcrypt) 加密,password_verify验证。Laravel 的Hashfacade 在 Hyperf 中可通过hyperf/hashing组件获得,或直接使用 PHP 原生函数。我们将采用原生方式,简单可靠。
2. JWT 签发
登录成功后,使用hyperf/jwt生成包含用户 ID、用户名等信息的 Token,返回给客户端。该 Token 可在网关层被验证,实现无状态认证。
3. RPC 服务发布
通过#[RpcService]注解将用户服务注册为 JSON-RPC 服务端,端口 9502。消费者(网关或其他服务)通过 Consul 发现并调用。
三、实战:编码用户服务接口(约 2.5 小时)
步骤 1:定义服务契约接口
根据 Proto 定义,创建app/JsonRpc/UserServiceInterface.php:
<?phpnamespaceApp\JsonRpc;useHyperf\RpcServer\Annotation\RpcService;#[RpcService(name:"UserService",protocol:"jsonrpc-http",server:"jsonrpc")]interfaceUserServiceInterface{publicfunctionregister(array$data):array;publicfunctionlogin(string$username,string$password):array;publicfunctiongetUserById(int$id):array;publicfunctionbatchGetUsers(array$ids):array;}步骤 2:实现服务
创建app/JsonRpc/UserService.php:
<?phpnamespaceApp\JsonRpc;useApp\Model\User;useHyperf\Jwt\JwtFactory;useHyperf\Di\Annotation\Inject;#[RpcService(name:"UserService",protocol:"jsonrpc-http",server:"jsonrpc")]classUserServiceimplementsUserServiceInterface{#[Inject]privateJwtFactory$jwtFactory;publicfunctionregister(array$data):array{// 验证(简化,实际应使用 Validator)if(empty($data['username'])||empty($data['password'])){thrownew\InvalidArgumentException('Username and password required');}// 检查唯一性if(User::where('username',$data['username'])->exists()){thrownew\InvalidArgumentException('Username already exists');}$user=User::create(['username'=>$data['username'],'password'=>password_hash($data['password'],PASSWORD_BCRYPT),'email'=>$data['email']??'',]);return$this->formatUser($user);}publicfunctionlogin(string$username,string$password):array{$user=User::where('username',$username)->first();if(!$user||!password_verify($password,$user->password)){thrownew\InvalidArgumentException('Invalid credentials');}$token=(string)$this->jwtFactory->make(['id'=>$user->id,'username'=>$user->username,]);return['token'=>$token,'user'=>$this->formatUser($user),];}publicfunctiongetUserById(int$id):array{$user=User::find($id);return$user?$this->formatUser($user):[];}publicfunctionbatchGetUsers(array$ids):array{$users=User::findMany($ids);returnarray_map([$this,'formatUser'],$users);}privatefunctionformatUser($user):array{return['id'=>$user->id,'username'=>$user->username,'email'=>$user->email,'avatar'=>$user->avatar,'created_at'=>$user->created_at,];}}步骤 3:创建 User 模型
app/Model/User.php:
<?phpnamespaceApp\Model;useHyperf\DbConnection\Model\Model;classUserextendsModel{protected$table='users';protected$fillable=['username','password','email','avatar'];protected$hidden=['password'];}步骤 4:配置 JSON-RPC 服务端
编辑config/autoload/server.php,添加jsonrpc服务器:
'servers'=>[['name'=>'jsonrpc','type'=>Server::SERVER_HTTP,'host'=>'0.0.0.0','port'=>9502,'sock_type'=>SWOOLE_SOCK_TCP,'callbacks'=>[Event::ON_REQUEST=>[\Hyperf\JsonRpc\HttpServer::class,'onRequest'],],],],步骤 5:配置依赖注入
config/autoload/dependencies.php中绑定接口到实现:
App\JsonRpc\UserServiceInterface::class=>App\JsonRpc\UserService::class,步骤 6:编写数据库迁移(可选)
创建迁移文件或直接使用 SQL 初始化。由于我们昨天已通过docker-entrypoint-initdb.d自动创建了user_db和users表,此处无需额外操作。
四、启动与测试(约 1 小时)
1. 启动用户服务
php bin/hyperf.php start确认控制台输出Service registered successfully或类似信息,并在 Consul UI 中看到UserService服务。
2. 直接测试 RPC 接口
使用curl调用 JSON-RPC 端点:
# 注册curl-XPOST http://localhost:9502\-H"Content-Type: application/json"\-d'{"jsonrpc":"2.0","method":"user/register","params":[{"username":"alice","password":"123456","email":"alice@example.com"}],"id":1}'预期返回用户信息。
# 登录curl-XPOST http://localhost:9502\-H"Content-Type: application/json"\-d'{"jsonrpc":"2.0","method":"user/login","params":["alice","123456"],"id":2}'预期返回 Token 和用户信息。
# 获取用户curl-XPOST http://localhost:9502\-H"Content-Type: application/json"\-d'{"jsonrpc":"2.0","method":"user/getUserById","params":[1],"id":3}'3. 通过网关访问(后续步骤)
修改网关hyperf-gateway的 Nacosgateway-routes配置,增加:
{"prefix":"/auth","target":"http://user-service:9502"}重启网关后,客户端可以通过http://localhost:9500/auth/register等路由访问用户服务(注意路径转换,网关目前是透明转发,JSON-RPC 路径需匹配)。为使 RESTful 友好,我们可以在网关编写一个/auth/register的聚合控制器,但今天先保证 RPC 调用正常。
4. 验证服务注册与发现
在 Consul UI 中确认UserService健康。其他服务(如将来的订单服务)可通过配置services.php消费者从 Consul 发现用户服务,实现 RPC 调用。
五、成果测试与记录(约 30 分钟)
测试清单
| 检验项 | 方法 | 通过标准 |
|---|---|---|
| 用户注册 | RPC 调用 register | 用户数据入库,密码加密 |
| 重复用户名 | 再次注册相同用户名 | 抛出异常或返回错误 |
| 登录成功 | 正确凭证调用 login | 返回有效 JWT |
| 登录失败 | 错误密码调用 login | 返回错误 |
| JWT 验证 | 使用返回的 Token 在网关测试(若已对接) | 网关解析成功 |
| GetUserById | 传入有效 ID | 返回用户信息 |
| Consul 注册 | Consul UI 查看 | UserService 出现,健康检查通过 |
| 密码加密 | 查看数据库 users 表 | password 字段非明文 |
常见问题
- JWT 签名失败:检查
jwt.php密钥长度,确保 HS256 至少 32 字节。 - 数据库连接失败:确认
databases.php主机名为mysql,并且user_db已创建。 - RPC 服务未注册:检查
@RpcService注解和server.php配置,确认没有命名冲突。
六、今日作业与学习产出
- 提交代码:将
services/user项目代码提交到 Git。 - 完善接口:
- 添加参数验证(使用
hyperf/validation),增强健壮性。 - 实现用户信息更新接口(修改密码、邮箱等)。
- 添加参数验证(使用
- 学习笔记:
- 绘制用户服务启动和注册到 Consul 的流程图。
- 对比直接在服务内使用 JWT 与在网关统一鉴权的利弊。
- 挑战任务:
- 编写一个用户服务消费者(可在网关中实现),通过 Consul 发现调用用户服务,实现统一认证入口。
- 为用户服务添加链路追踪 Span(集成
hyperf/tracer),上报到 Jaeger。
通过今天的学习,电商系统拥有了认证基石——用户服务。明天我们将实现商品服务,并打通与网关、Consul 的完整链路,让商品数据能够被查询和管理。