用户注册、登录与基础会话
用户注册、登录与基础会话
目标
- 完成
user-service微服务的基础搭建 - 理解短信验证码在注册流程中的作用
- 完成用户注册接口
- 完成用户登录接口
- 完成根据
token获取当前用户信息接口 - 学会使用全局异常处理统一返回错误结果
从这一篇开始,我们正式进入商城前台的用户模块。
前面的商品首页、分类、详情等接口,大多可以在“未登录”状态下访问;但从购物车、收藏、地址、下单开始,系统就必须知道“当前操作的人是谁 ”。
因此,用户模块的地位非常关键。它不只是多了一个“登录页面”,而是给整个商城补上“身份体系”。
本篇会先把最基础的三件事打通:
- 获取短信验证码
- 用户注册
- 用户登录并生成
token
1.用户微服务搭建
前言
用户相关的能力,通常会单独拆成一个 user-service 微服务,原因很简单:
- 注册、登录、验证码、用户资料,都是高频独立业务
- 后续购物车、订单、收藏、地址都会依赖用户身份
- 把用户模块单独拆分后,职责更清晰,也便于后续扩展第三方登录、会员体系等能力
1.1.模块结构
注意:user-service 的父工程是 zx-service。

建议模块结构保持简洁:
user-service
com.zx.user
├─ controller 表现层,对外提供接口
├─ service 业务层,编写真正的业务逻辑
├─ mapper 持久层,操作数据库
└─ utils 当前服务专属工具类(可选)
1.2.依赖说明
- 如果短信发送工具类只在
user-service中使用,那么第三方依赖放在user-service即可。 - 如果你准备把短信工具抽到
zx-common供多个微服务复用,那么相关依赖也应该跟着放到zx-common。
课堂工程里,短信验证码目前只给用户模块使用,放在 user-service 更容易理解。
依赖示例:
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.13.1</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
</dependencies>
1.3.配置文件与启动类
application.yml
server:
port: 9002
spring:
data:
redis:
host: localhost
port: 6379
application:
name: user-service
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_zx?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true
username: root
password: 1234
cloud:
nacos:
server-addr: 127.0.0.1:8848
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.zx.domain.entity.user
global-config:
datacenter-id: 1 # 数据中心
workerId: 1 # 工作机器ID
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名
cache-enabled: false # 禁用二级缓存
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
secretKey: abc # 密钥
expirationMinutes: 600 # 过期时间,单位:分钟
UserServiceApplication
package com.zx.user;
@Slf4j
@SpringBootApplication
@MapperScan("com.zx.user.mapper")
@ServletComponentScan("com.zx.common")
public class UserServiceApplication {
public static void main(String[] args) throws UnknownHostException {
SpringApplication app = new SpringApplication(UserServiceApplication.class);
ConfigurableApplicationContext application = app.run(args);
Environment env = application.getEnvironment();
log.info("\n----------------------------------------------------------\n\t" +
"Application '{}' is running! Access URLs:\n\t" +
"Local: \t\thttp://localhost:{}\n\t" +
"External: \thttp://{}:{}\n\t" +
"Doc: \t\thttp://{}:{}/doc.html\n" +
"----------------------------------------------------------",
env.getProperty("spring.application.name"),
env.getProperty("server.port"),
InetAddress.getLocalHost().getHostAddress(),
env.getProperty("server.port"),
InetAddress.getLocalHost().getHostAddress(),
env.getProperty("server.port"));
}
//分页插件对象
@Bean
public PaginationInnerInterceptor paginationInnerInterceptor() {
return new PaginationInnerInterceptor();
}
}
为什么这里要保留 @ServletComponentScan("com.zx.common")?
- 因为后面我们会把一部分公共过滤器、异常处理等能力放到
zx-common - 如果不扫描公共包,这些组件不会生效
2.用户注册
在完成购物车、收藏、地址等功能之前,必须先让系统具备“创建用户”和“识别用户”的能力。
而注册功能里最关键的一步,就是先证明这个手机号确实归当前用户所有,所以要先做短信验证码。
使用MybatisX完成userinfo表的mapper,service,impl代码生成
记得将domain中的实体类删掉,因为实体类都在model模块中
前言
2.1.需求分析
当前课程采用“手机号注册”的方式完成会员注册。
完整注册流程如下:
- 前端输入手机号
- 点击“获取验证码”
- 后端生成验证码,保存到
Redis - 调用短信服务发送验证码
- 用户填写验证码、昵称、密码
- 后端校验验证码并保存用户
如图所示: 

涉及两个接口:
获取验证码:
GET /api/user/sms/sendCode/{phone}
提交注册:
POST /api/user/userInfo/register
{
"username": "15019685678",
"password": "111111",
"nickName": "晴天",
"code": "6799"
}
2.2.发送短信验证码
2.2.1.为什么验证码要放 Redis
验证码属于典型的“短期有效临时数据”:
- 有效期短,一般 5 分钟左右
- 查询频繁,但不需要长期持久化
- 注册成功后可以立即删除
因此它非常适合放在 Redis 中,而不是放入数据库。
建议把验证码的 key 设计得更清晰一点,例如:
user:register:code:15019685678
2.2.2.短信发送流程
发送验证码的核心流程如下:

这里有一个很重要的设计思想:
发短信不是注册本身发短信只是为了给注册流程提供“验证码凭证”
所以在代码层面,短信发送接口尽量只做三件事:
- 生成验证码
- 保存验证码
- 发送短信
不要把注册逻辑写进发送短信接口里。
2.2.3.SmsController
表现层代码:
package com.zx.user.controller;
@Tag(name = "用户短信相关接口")
@RestController
@RequestMapping("/user")
public class SmsController {
@Autowired
private UserInfoService userInfoService;
@GetMapping("/sms/sendCode/{phone}")
@Operation(summary = "发送短信验证码")
@CrossOrigin
public Result<Void> sendValidateCode(@PathVariable String phone) {
return userInfoService.sendPhoneSmsCode(phone);
}
}
2.2.4.UserInfoService
业务接口:
void sendPhoneSmsCode(String phone);
业务实现:
package com.zx.user.service.impl;
@Service
@Slf4j
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService{
private static final String REGISTER_CODE_PREFIX = "user:register:code:";
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Result sendPhoneSmsCode(String phone) {
if (StringUtils.isBlank(phone)) {
return Result.build(null, ResultCodeEnum.DATA_ERROR);
}
// 1. 生成4位验证码
String code = RandomStringUtils.randomNumeric(4);
log.info("手机号:{},验证码:{}", phone, code);
// 2. 保存到 Redis,5分钟过期
redisTemplate.opsForValue().set(
REGISTER_CODE_PREFIX + phone,
code,
5,
TimeUnit.MINUTES
);
//todo 3. 调用第三方短信服务
//
return Result.build(null, ResultCodeEnum.SUCCESS);
}
}
2.3.调用三方短信服务
2.3.1 云市场-短信API
开通三网106短信
在阿里云云市场搜索“短信”,一般都可用,选择一个即可,例如如下:点击“立即购买”开通
这里开通的是【短信验证码- 快速报备签名】

获取开发参数
登录云市场控制台,在已购买的服务中可以查看到所有购买成功的API商品情况,下图红框中的就是AppKey/AppSecret,AppCode的信息。

API方式使用云市场服务
参考如下例子,复制代码在test目录进行测试

2.3.2 发送短信流程说明
发送短信验证码的流程如下所示:

查看接口文档:
get /api/user/sms/sendCode/{phone}
2.3.4 发送短信接口开发
2.3.4.1 拷贝依赖
zx-common模块
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.13.1</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.32</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.15</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.2.1</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util</artifactId>
<version>9.3.7.v20160115</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.5</version>
<scope>test</scope>
</dependency>
2.3.4.2 拷贝工具类(暂时没用,后期登录成功后发送token用)
复制甄选资料\资料\day07-甄选-用户注册和登录\资料\utils相关工具类到zx-common模块
包名com.zx.utils
将MyJwtUtils工具类的全限定名,复制到
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中
com.zx.common.FastJsonLongToStringConfiguration
com.zx.common.Knife4jConfig
com.zx.utils.MyJwtUtils
将三方短信平台上的,粘贴到MySendMessageUtils中

接着在UserInfoServiceImpl的sendPhoneSmsCode方法中,调用MySendMessageUtils.sendSms方法发送短信
package com.zx.user.service.impl;
@Service
@Slf4j
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService{
private static final String REGISTER_CODE_PREFIX = "user:register:code:";
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Result sendPhoneSmsCode(String phone) {
if (StringUtils.isBlank(phone)) {
return Result.build(null, ResultCodeEnum.DATA_ERROR);
}
// 1. 生成4位验证码
String code = RandomStringUtils.randomNumeric(4);
log.info("手机号:{},验证码:{}", phone, code);
// 2. 保存到 Redis,5分钟过期
redisTemplate.opsForValue().set(
REGISTER_CODE_PREFIX + phone,
code,
5,
TimeUnit.MINUTES
);
//todo 3. 调用第三方短信服务
MySendMessageUtils.sendMessage(code, phone);
return Result.build(null, ResultCodeEnum.SUCCESS);
}
}
启动user-service模块,访问 http://localhost:9002/user/sms/sendCode/你的手机号码, 观察你的手机是否收到号码?

2.4.用户注册接口
2.4.1.注册流程说明
注册流程如下:

注意这个流程背后的业务意义:
先查用户名是否重复:避免重复注册再校验验证码:确保手机号归属真实最后保存用户:正式入库
2.4.2.UserInfo 实体类
用户实体类示例:
@Data
@Schema(description = "用户实体类")
public class UserInfo extends BaseEntity {
private static final long serialVersionUID = 1L;
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "昵称")
private String nickName;
@Schema(description = "头像")
private String avatar;
@Schema(description = "性别")
private Integer sex;
@Schema(description = "电话号码")
private String phone;
@Schema(description = "备注")
private String memo;
@Schema(description = "微信open id")
private String openId;
@Schema(description = "微信开放平台unionID")
private String unionId;
@Schema(description = "最后一次登录ip")
private String lastLoginIp;
@Schema(description = "最后一次登录时间")
private Date lastLoginTime;
@Schema(description = "状态:1正常,0禁用")
private Integer status;
}
2.4.3.UserRegisterDto
@Data
@Schema(description = "注册对象")
public class UserRegisterDto {
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
@Schema(description = "昵称")
private String nickName;
@Schema(description = "手机验证码")
private String code;
}
这里用 DTO 的意义要理解清楚:
- 前端传来的注册参数,不一定和数据库实体一一对应
DTO用来接收入参Entity用来持久化数据
不要直接让前端把数据库实体对象原样提交上来。
2.4.4.UserInfoController
package com.zx.user.controller;
@Tag(name = "会员用户接口")
@RestController
@RequestMapping("/user/userInfo")
@CrossOrigin(origins = "*")
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@Operation(summary = "用户注册")
@PostMapping("/register")
public Result<Void> register(@RequestBody UserRegisterDto dto) {
userInfoService.register(dto);
return Result.build(null, ResultCodeEnum.SUCCESS);
}
}
2.4.5.UserInfoServiceImpl
@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo>
implements UserInfoService {
private static final String REGISTER_CODE_PREFIX = "user:register:code:";
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public Result register(UserRegisterDto dto) {
// 1. 参数校验
if (dto == null
|| StringUtils.isBlank(dto.getUsername())
|| StringUtils.isBlank(dto.getPassword())
|| StringUtils.isBlank(dto.getCode())
|| StringUtils.isBlank(dto.getNickName())) {
return Result.build(null, ResultCodeEnum.DATA_ERROR);
}
// 2. 用户名去重校验
UserInfo existsUser = lambdaQuery()
.eq(UserInfo::getUsername, dto.getUsername())
.one();
if (existsUser != null) {
return Result.build(null, ResultCodeEnum.USER_NON_EXISTS);
}
// 3. 校验验证码
String redisCode = redisTemplate.opsForValue().get(REGISTER_CODE_PREFIX + dto.getUsername());
if (StringUtils.isBlank(redisCode)) {
return Result.build(null, ResultCodeEnum.VALIDATECODE_ERROR);
}
if (!redisCode.equals(dto.getCode())) {
return Result.build(null, ResultCodeEnum.VALIDATECODE_ERROR);
}
// 4. 密码加密
// 课堂项目中先用 MD5 跑通流程;生产环境建议改为 BCrypt
String passwordMd5 = DigestUtils.md5DigestAsHex(dto.getPassword().getBytes(StandardCharsets.UTF_8));
// 5. 保存用户
UserInfo userInfo = new UserInfo();
BeanUtils.copyProperties(dto, userInfo);
userInfo.setPassword(passwordMd5);
userInfo.setPhone(dto.getUsername());
userInfo.setStatus(1);
userInfo.setAvatar("http://139.198.127.41:9000/sph/20230505/default_handsome.jpg");
save(userInfo);
// 6. 注册成功后删除验证码,避免重复使用
redisTemplate.delete(REGISTER_CODE_PREFIX + dto.getUsername());
return Result.build(null, ResultCodeEnum.SUCCESS);
}
}
启动项目,打开前端页面,修改端口,测试注册页面。 
点击设置,点击注册,输入手机号码,发送验证码,输入密码,昵称,点击注册


3.全局异常处理
如果接口里每次都 return Result.build(...),短期看起来没问题,但业务一复杂就会出现两个问题:
- 控制层、业务层到处充满重复判断
- 一旦某个地方直接抛异常,前端拿到的就是不友好的
500
因此更合理的做法是: 👍
- 业务层发现问题时直接抛异常
- 全局异常处理器统一拦截并封装响应结果
前言
在zx-common模块中创建exception包,并定义两个类:CustomException和GlobalExceptionHandler。
package com.zx.exception;
import com.zx.domain.vo.common.ResultCodeEnum;
public class CustomException extends RuntimeException {
private final Integer code;
private final String message;
public CustomException(ResultCodeEnum resultCodeEnum) {
this.code = resultCodeEnum.getCode();
this.message = resultCodeEnum.getMessage();
}
public CustomException(String message, Integer code) {
this.code = code;
this.message = message;
}
@Override
public String getMessage() {
return message;
}
public Integer getCode() {
return code;
}
}
package com.zx.exception;
import com.zx.domain.vo.common.Result;
import com.zx.domain.vo.common.ResultCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
public Result<Void> handleCustomException(CustomException e) {
if (e.getCode() != null) {
return Result.build(null, e.getCode(), e.getMessage());
}
return Result.build(null, 500, e.getMessage());
}
@ExceptionHandler(BindException.class)
public Result<Void> handleBindException(BindException e) {
log.error("参数绑定异常", e);
return Result.build(null, ResultCodeEnum.PARAM_ERROR);
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.build(null, ResultCodeEnum.SYSTEM_ERROR);
}
}
3.3.公共模块扫描说明
如果 GlobalExceptionHandler 放在 zx-common 中,就要确保它能被 Spring 扫描到。常见做法有两种:
- 启动类扩大扫描包范围
- 在
zx-common中提供自动装配配置类
这里采用在 zx-common 中提供自动装配配置类
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.zx.common.FastJsonLongToStringConfiguration
com.zx.common.Knife4jConfig
com.zx.utils.MyJwtUtils
com.zx.exception.GlobalExceptionHandler
代码改造:

测试
未使用全局异常处理返回结果

使用全局异常处理返回结果

4.用户登录
注册完成之后,系统里只是“有了这个人”。
登录完成之后,系统才能在后续请求中持续识别“当前就是这个人”。
因此登录的核心不是“查库成功”,而是“生成凭证并维持会话状态”。
前言
4.1.登录流程说明
登录流程如下:

流程:
- 用户输入用户名和密码
- 后端校验账号密码
- 校验通过后生成
token - 把用户信息写入
Redis - 把
token返回给前端 - 前端后续访问接口时,把
token放到请求头里
4.2.UserLoginDto
@Data
@Schema(description = "用户登录请求参数")
public class UserLoginDto {
@Schema(description = "用户名")
private String username;
@Schema(description = "密码")
private String password;
}
4.3.登录接口
控制层:
@Operation(summary = "用户登录")
@PostMapping("/login")
public Result<String> login(@RequestBody UserLoginDto dto) {
String token = userInfoService.login(dto);
return Result.build(token, ResultCodeEnum.SUCCESS);
}
业务实现:
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private MyJwtUtils myJwtUtils;
@Override
public String login(UserLoginDto dto) {
// 1. 参数校验
if (dto == null
|| StringUtils.isBlank(dto.getUsername())
|| StringUtils.isBlank(dto.getPassword())) {
throw new CustomException(ResultCodeEnum.PARAM_ERROR);
}
// 2. 查询用户
UserInfo userInfo = lambdaQuery()
.eq(UserInfo::getUsername, dto.getUsername())
.one();
if (userInfo == null) {
throw new CustomException(ResultCodeEnum.USER_NON_EXISTS);
}
// 3. 校验密码
String passwordMd5 = DigestUtils.md5DigestAsHex(dto.getPassword().getBytes(StandardCharsets.UTF_8));
if (!passwordMd5.equals(userInfo.getPassword())) {
throw new CustomException(ResultCodeEnum.LOGIN_ERROR);
}
// 4. 生成 token
String token = myJwtUtils.generateToken(userInfo.getId().toString());
// 5. 写入 Redis
redisTemplate.opsForValue().set(
"user:login:token:" + token,
JSON.toJSONString(userInfo),
10,
TimeUnit.HOURS
);
return token;
}
4.4.根据 token 获取当前用户信息
很多页面在登录成功后,都会立即请求“当前用户信息”,用于渲染头像、昵称等内容。
因此还需要再补一个接口:
GET /api/user/userInfo/auth/getCurrentUserInfo
4.4.1.UserInfoVo
@Data
@Schema(description = "用户信息视图对象")
public class UserInfoVo {
@Schema(description = "昵称")
private String nickName;
@Schema(description = "头像")
private String avatar;
}
4.4.2.Controller
@Operation(summary = "根据token获取用户信息")
@GetMapping("/auth/getCurrentUserInfo")
public Result<UserInfoVo> getCurrentUserInfo(@RequestHeader("token") String token) {
UserInfoVo userInfoVo = userInfoService.getCurrentUserInfo(token);
return Result.build(userInfoVo, ResultCodeEnum.SUCCESS);
}
4.4.3.Service
@Override
public UserInfoVo getCurrentUserInfo(String token) {
//1. 从 Redis 中获取用户信息
String userInfoJsonStr = redisTemplate.opsForValue().get("user:login:token:" + token);
if (StringUtils.isBlank(userInfoJsonStr)) {
throw new CustomException(ResultCodeEnum.LOGIN_AUTH);
}
//2. 转换成对象
UserInfo userInfo = JSON.parseObject(userInfoJsonStr, UserInfo.class);
UserInfoVo vo = new UserInfoVo();
vo.setNickName(userInfo.getNickName());
vo.setAvatar(userInfo.getAvatar());
return vo;
}

这里先采用“控制层从请求头取 token,再传给业务层”的方式,是为了让登录体系先跑通。
到了下一篇做网关鉴权时,我们会进一步优化成:
- 网关统一校验
token - 微服务直接拿“当前登录用户”
- 控制层不再手动解析
token
4.5.当前方案的意义
到这里,系统已经具备了最基础的“会话状态能力”:
注册解决了“用户从无到有”登录解决了“如何证明你是谁”token + Redis解决了“后续请求如何持续识别你”
这三步打通后,后面的购物车、地址、收藏等模块就都能以“当前用户”为前提继续开发。
5.网关
前言
5.1.网关的作用
在微服务架构中,前端不应该直接请求每一个业务微服务,而应该统一请求“网关服务”。
网关就像园区门口的门卫:
- 你是谁,要先核验
- 你要去哪里,由门卫帮你转发
- 不符合规则的请求,直接拦住

微服务中的网关主要承担三类职责:
- 请求路由
- 统一鉴权
- 跨域处理、限流、日志、灰度等横切能力
因此,网关并不是“可有可无的中转站”,它本身就是整个系统入口层的重要组成部分。
5.2.为什么鉴权要放到网关
如果仍然让每个微服务各自做登录校验,会出现这些问题:
- 每个微服务都要解析
token - 每个微服务都要查一次
Redis - 每个微服务都要知道
JWT规则 - 同样的未登录处理逻辑会到处复制
把鉴权放到网关之后,优势就很明显:
- 登录校验逻辑只写一次
- 下游微服务只关注业务本身
- 统一登录失败响应格式
- 更方便后续继续叠加限流、黑名单、来源校验等能力
5.3.最终要实现什么
最终效果如下:
- 前端请求统一先到
zx-gateway - 网关判断当前接口是否必须登录
- 如果必须登录,就从请求头取
token - 再去
Redis校验登录态 - 校验通过后,把当前用户信息放进请求头,继续转发
- 下游微服务收到请求后,再把用户信息放到
ThreadLocal - 业务代码通过工具类直接获取当前登录用户
5.4.创建 zx-gateway
在 zx-front 下新建一个模块:zx-gateway。
它本质上也是一个 Spring Boot 微服务,只不过它不负责商品、用户、订单等具体业务,而是负责“入口治理”。
5.5.引入依赖
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.zx</groupId>
<artifactId>zx-model</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.0.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.48</version>
</dependency>
</dependencies>
5.6.启动类
package com.zx.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class GateWayApplication {
public static void main(String[] args) {
SpringApplication.run(GateWayApplication.class, args);
}
}
为什么这里排除 DataSourceAutoConfiguration?
- 因为网关当前不直接访问数据库
- 如果不排除,Spring Boot 会尝试自动配置数据源
- 没有数据库配置时,启动就可能报错
5.7.配置路由
application.yml
server:
port: 10086
spring:
data:
redis:
host: localhost
port: 6379
application:
name: zx-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
globalcors:
cors-configurations:
'[/**]':
allowCredentials: true
allowedHeaders: '*'
allowedMethods: '*'
allowedOriginPatterns: '*'
maxAge: 3600
routes:
- id: product
uri: lb://product-service
predicates:
- Path=/api/product/**
filters:
- StripPrefix= 1
# 用户微服务
- id: user
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix= 1
5.8.StripPrefix=1 到底做了什么
以这个地址为例:
http://localhost:10086/api/user/userInfo/login
如果配置了:
- StripPrefix=1
那么转发给下游微服务时,网关会去掉第一个路径前缀 api,变成:
/user/userInfo/login
所以:
- 网关对外地址是
/api/user/** user-service内部控制器路径要写成/user/**
这就是“对外统一 API 前缀”和“服务内部路由前缀”之间的关系。
重新启动ProductApplication、UserApplication,GatewayApplication
访问商品首页 http://localhost:10086/api/product/index 发现可以访问了

页面测试

修改页面的baseUrl为网关地址 http://127.0.0.1:10086
经测试发现页面可以正常访问
6.小结
这一篇完成了用户模块最基础的身份体系闭环:
user-service基础环境搭建完成- 完成短信验证码发送接口
- 完成用户注册接口
- 完成用户登录接口
- 完成根据
token获取当前用户信息接口 - 完成全局异常处理
- 完成网关微服务
zx-gateway搭建
接下来继续进入更关键的一步:
如何把“登录校验”从每个微服务里抽出来,统一放到网关做,这就是下一篇的重点。