微服务基础骨架搭建-02
这次要实现用户的注册、登录功能。sa-token鉴权,dobbo远程调用,nacos注册配置中心
涉及的模块:用户模块 mfc-user、认证模块 mfc-auth、common下的模块:mfc-api、mfc-config、mfc-rpc、mfc-sa-token
1. mfc-sa-token
这个模块只是做了简单的依赖的引入和通用配置
1.1 依赖
<sa-token.version>1.45.0</sa-token.version> <commons-pool2.version>2.11.1</commons-pool2.version> <!-- Sa-Token 权限认证,在线文档:https://sa-token.cc --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot3-starter</artifactId> <version>${sa-token.version}</version> </dependency> <!-- Sa-Token 整合 RedisTemplate --> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-redis-template</artifactId> <version>${sa-token.version}</version> </dependency> <!-- 提供 Redis 连接池 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>${commons-pool2.version}</version> </dependency>1.2 配置
配置了默认的 redis,引入该依赖的服务还可以通过在其 application.yml 配置文件以mfc.sa-token.redis.host方式指定redis服务
spring: data: redis: host: ${mfc.sa-token.redis.host:192.168.23.129} port: ${mfc.sa-token.redis.port:6379} password: ${mfc.sa-token.redis.password:rd123456} database: ${mfc.sa-token.redis.database:1} timeout: ${mfc.sa-token.redis.timeout:10s} lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间(使用负值表示没有限制) max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0 ############## Sa-Token 配置 (文档: https://sa-token.cc) ############## sa-token: # token 名称(同时也是 cookie 名称) token-name: satoken # token 有效期(单位:秒) 默认30天(2592000),-1 代表永久有效 timeout: 7200 # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录) is-concurrent: false # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token) is-share: false # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik) token-style: uuid # 是否输出操作日志 is-log: true2. mfc-config
该模块为nacos注册中心与配置中心,一样也是引入依赖和通用配置
2.1 依赖
父pom
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version> <!-- Source: https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-alibaba-dependencies --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>${spring-cloud-alibaba.version}</version> <type>pom</type> <scope>import</scope> </dependency>pom
<dependencies> <!--nacos discovery--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <exclusions> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </exclusion> </exclusions> </dependency> <!--nacos config--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> </dependencies>2.2 配置
spring: cloud: nacos: discovery: server-addr: ${mfc.nacos.server-addr:192.168.23.129:8848} namespace: ${mfc.nacos.namespace:d92dc5b6-1b48-465d-aa0c-ccd8e780c8af} username: ${mfc.nacos.username:nacos} password: ${mfc.nacos.password:nacos123} config: server-addr: ${mfc.nacos.server-addr:192.168.23.129:8848} namespace: ${mfc.nacos.namespace:d92dc5b6-1b48-465d-aa0c-ccd8e780c8af} file-extension: properties name: ${spring.application.name} username: ${mfc.nacos.username:nacos} password: ${mfc.nacos.password:nacos123}3. mfc-rpc
该模块配置为starter,配置了 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,里面的内容就是配置
3.1 依赖
<dubbo.version>3.2.19</dubbo.version> <!-- Source: https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>${dubbo.version}</version> <scope>compile</scope> </dependency> <!-- Source: https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-nacos --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-registry-nacos</artifactId> <version>${dubbo.version}</version> <scope>compile</scope> </dependency>3.2 配置
dubbo: consumer: timeout: 3000 check: false protocol: name: dubbo port: -1 registry: address: nacos://${mfc.nacos.server-addr:192.168.23.129:8848} username: ${mfc.nacos.username:nacos} password: ${mfc.nacos.password:nacos123} parameters: namespace: ${mfc.nacos.namespace:d92dc5b6-1b48-465d-aa0c-ccd8e780c8af} group: ${mfc.nacos.group:DEFAULT_GROUP} application: name: ${spring.application.name} qos-enable: true qos-accept-foreign-ip: false3.3 配置类
@Configuration @EnableDubbo public class RpcConfiguration { }4. mfc-api
此模块为微服务项目的api接口模块,所有的微服务对外提供服务的接口都放在这里,这里只写接口不写实现,必要的入参出参也放在这里。当前只有用户微服务的。
所有微服务模块都会依赖该模块。
4.1 依赖
只依赖 mfc-base模块
<dependencies> <!--mfc-bse--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-base</artifactId> </dependency> </dependencies>4.2 接口
只是普通的接口,不添加任何注解,出参入参根据自己的需要求进行设计即可。
这个接口是要对应的微服务去实现的,比如下面的明显是用户相关的功能,就需要用户模块去实现该接口的功能
public interface UserFacadeService { /** * 注册 * @param registryRequest * @return */ UserInfo register(RegistryRequest registryRequest); /** * 密码登录 * @param loginRequest * @return */ UserInfo login(LoginRequest loginRequest); /** * 验证码登录 * @param loginByCodeRequest * @return */ UserInfo login(LoginByCodeRequest loginByCodeRequest); }5. mfc-user
接上面 4.2 ,对外接口的实现我们放在 facade 包下,UserFacadeServiceImpl;
使用了 @DubboService(version = "1.0.0") 注解;
这里我们注入UserService,再 domain/service 包下,这个包下的实现跟传统三层写法一样,功能都在里面实现
@DubboService(version = "1.0.0") public class UserFacadeServiceImpl implements UserFacadeService { @Autowired private UserService userService; @Override public UserInfo register(RegistryRequest registryRequest) { return userService.register(registryRequest); } @Override public UserInfo login(LoginRequest loginRequest) { return userService.login(loginRequest); } @Override public UserInfo login(LoginByCodeRequest loginByCodeRequest) { return userService.login(loginByCodeRequest); } }5.1 依赖
都是引入我们之前封装好的
<dependencies> <!--mfc-web--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-web</artifactId> </dependency> <!--mfc-config--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-config</artifactId> </dependency> <!--mfc-base--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-datasource</artifactId> </dependency> <!--mfc-api--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-api</artifactId> </dependency> <!--mfc-rpc--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-rpc</artifactId> </dependency> <!--starter--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> </dependency> </dependencies>5.2 配置
这里一个关键的点是 spring.config.import
optional:classpath:config.yml,optional:classpath:rpc.yml 等有关 nacoa配置的,一定要在 optional:nacos:${spring.application.name}.properties 这项之前
如果顺序不对,服务启动会失败
我搜了一下也结合AI看了一下说是加载时没有读取到nacos相关的配置(地址用户名密码等)
没有 optional:nacos:${spring.application.name}.properties 也起不起服务
server: port: 12020 spring: application: name: user-service config: import: optional:classpath:config.yml,optional:classpath:rpc.yml,optional:nacos:${spring.application.name}.properties,optional:classpath:cache.yml,optional:classpath:datasource.yml,optional:classpath:sa-token.yml mfc: mysql: url: jdbc:mysql://192.168.23.129:3306/mfc_user username: root password: ms1234565.3 异常的定义与使用
5.3.1 用户异常类
继承base模块 BizException,写五个构造函数😂
public class UserException extends BizException { public UserException(String message, ErrorCode errorCode) { super(message, errorCode); } public UserException(ErrorCode errorCode) { super(errorCode); } public UserException(String message, Throwable cause, ErrorCode errorCode) { super(message, cause, errorCode); } public UserException(Throwable cause, ErrorCode errorCode) { super(cause, errorCode); } public UserException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, ErrorCode errorCode) { super(message, cause, enableSuppression, writableStackTrace, errorCode); } }5.3.2 用户异常码
继承base模块的 ErrorCode 接口
public enum UserErrorCode implements ErrorCode { /** * 邮箱格式错误 */ EMAIL_FORMAT_ERROR("EMAIL_FORMAT_ERROR", "邮箱格式错误"), /** * 邮箱已注册 */ EMAIL_ALREADY_REGISTERED("EMAIL_ALREADY_REGISTERED", "邮箱已注册"), /** * 验证码错误 */ VERIFICATION_CODE_ERROR("VERIFICATION_CODE_ERROR", "验证码错误"), /** * 密码不一致或者格式错误 */ PASSWORD_FORMAT_ERROR("PASSWORD_FORMAT_ERROR", "密码不一致或者格式错误"), /** * 用户名生成失败 */ USERNAME_GENERATE_ERROR("USERNAME_GENERATE_ERROR", "用户名生成失败"), /** * 注册失败 */ REGISTER_ERROR("REGISTER_ERROR", "注册失败,请重试"), /** * 角色分配出错 */ ROLE_ASSIGN_ERROR("ROLE_ASSIGN_ERROR", "角色分配出错"), ; private final String code; private final String message; UserErrorCode(String code, String message) { this.code = code; this.message = message; } @Override public String getCode() { return this.code; } @Override public String getMessage() { return this.message; } }5.3.3 使用
在需要的地方抛一个异常即可
// 邮箱:校验格式及是否存在 if (!Validator.isEmail(registryRequest.getEmail())) { throw new UserException(UserErrorCode.EMAIL_FORMAT_ERROR); }抛出的异常,最终会被 全局异常处理器 GlobalWebExceptionHandler (mfc-web模块)处理,最终封装成一个Result 对象响应给前端,message就是 UserErrorCode.EMAIL_FORMAT_ERROR 对应的message 提示信息
5.4 注册登录
在这里简单说一下注册登录逻辑吧
说之前先明确一下:
我们这里设计的注册登录响应的都是 UserInfo 对象,对象的属性根据自己的业务进行设计。真正的登录操作并不在 mfc-user 模块,而是在 mfc-auth 模块完成。
注册:
入参:email,code,password,confirmPassword
首先会进行邮箱格式和存在性的校验,不符合抛异常;
接着进行两个密码的格式和一致性校验,不符合抛异常;
接着从redis拿出验证码,与用户的code比对,不符合抛异常;
接着生成唯一的用户名,尝试十次,不成功抛异常;
接着加密密码,设置各种属性值,接着插入数据库,不成功抛异常;
再查一把数据库,获取完整的 User 对象,转换成 UserInfo 对象,分配角色等;
最后返回 UserInfo 对象。
登录也差不多,根据email查数据库,比对密码或者验证码,成功者返回 UserInfo 对象。
拿到 UserInfo 后,继续往下看怎么登录。
6. mfc-auth
接上面 5.4 登录,拿到 UserInfo 对象后,就使用 sa-token 的 StpUtil 工具进行登录,并响应 UserInfo 给前端,token 也会给前端,这应该时 sa-token 框架实现的,不用我们手动实现
@PostMapping("/register") public Result<UserInfo> register(RegistryRequest registryRequest) { UserInfo userInfo = userFacadeService.register(registryRequest); StpUtil.login(userInfo.getId()); StpUtil.getSession().set(userInfo.getId().toString(), userInfo); // 删除验证码 redisTemplate.delete("auth:code:registry:" + registryRequest.getEmail()); log.info("用户 {} 注册并登录成功", userInfo.getEmail()); return Result.ok(userInfo); }6.1 依赖
<dependencies> <!--mfc-web--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-web</artifactId> </dependency> <!--mfc-cache--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-cache</artifactId> </dependency> <!--mfc-sa-token--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-sa-token</artifactId> </dependency> <!--mfc-config--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-config</artifactId> </dependency> <!--mfc-api--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-api</artifactId> </dependency> <!--mfc-rpc--> <dependency> <groupId>com.mfar</groupId> <artifactId>mfc-rpc</artifactId> </dependency> <!--starter--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--hutool--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> </dependencies>6.2 配置
server: port: 12010 spring: application: name: auth-service config: import: optional:classpath:config.yml,optional:classpath:rpc.yml,optional:nacos:${spring.application.name}.properties,optional:classpath:cache.yml,optional:classpath:sa-token.yml,6.3 controller
使用
@DubboReference(version = "1.0.0")
private UserFacadeService userFacadeService;
调用远程接口
@Slf4j @RestController @RequestMapping("/auth") public class AuthController { @Autowired private RedisTemplate<String, String> redisTemplate; @DubboReference(version = "1.0.0") private UserFacadeService userFacadeService; /** * 发送注册验证码 * @return */ @RequestMapping("/sendRegisterCode/{email}") public Result<String> sendRegisterCode(@PathVariable String email) { boolean isEmail = Validator.isEmail(email); if (!isEmail) { throw new AuthException(AuthErrorCode.EMAIL_FORMAT_ERROR); } Random random = new Random(); int code = random.nextInt(100000,1000000); redisTemplate.opsForValue().set("auth:code:registry:" + email, String.valueOf(code), 5 * 60, TimeUnit.SECONDS); // TODO: 发送验证码邮件 log.info("邮箱:{},注册验证码: {}", email, code); return Result.ok("验证码已发送,请检查邮箱"); } /** * 发送登录验证码 * @return */ @RequestMapping("/sendLoginCode/{email}") public Result<String> sendLoginCode(@PathVariable String email) { boolean isEmail = Validator.isEmail(email); if (!isEmail) { throw new AuthException(AuthErrorCode.EMAIL_FORMAT_ERROR); } Random random = new Random(); int code = random.nextInt(100000,1000000); redisTemplate.opsForValue().set("auth:code:login:" + email, String.valueOf(code), 5 * 60, TimeUnit.SECONDS); // TODO: 发送验证码邮件 log.info("邮箱:{},登录验证码: {}", email, code); return Result.ok("验证码已发送,请检查邮箱"); } @PostMapping("/register") public Result<UserInfo> register(RegistryRequest registryRequest) { UserInfo userInfo = userFacadeService.register(registryRequest); StpUtil.login(userInfo.getId()); StpUtil.getSession().set(userInfo.getId().toString(), userInfo); // 删除验证码 redisTemplate.delete("auth:code:registry:" + registryRequest.getEmail()); log.info("用户 {} 注册并登录成功", userInfo.getEmail()); return Result.ok(userInfo); } @PostMapping("/login") public Result<UserInfo> login(LoginRequest loginRequest) { log.info("loginRequest: {} ", loginRequest); UserInfo userInfo = userFacadeService.login(loginRequest); StpUtil.login(userInfo.getId()); StpUtil.getSession().set(userInfo.getId().toString(), userInfo); log.info("用户 {} 登录成功", userInfo.getEmail()); return Result.ok(userInfo); } @PostMapping("/loginCode") public Result<UserInfo> login(LoginByCodeRequest loginRequest) { UserInfo userInfo = userFacadeService.login(loginRequest); StpUtil.login(userInfo.getId()); StpUtil.getSession().set(userInfo.getId().toString(), userInfo); // 删除验证码 redisTemplate.delete("auth:code:login:" + loginRequest.getEmail()); log.info("用户 {} 登录成功", userInfo.getEmail()); return Result.ok(userInfo); } /** * 退出登录 * @return */ @PostMapping("/logout") public Result<String> logout() { StpUtil.logout(); return Result.ok("退出登录成功"); } @RequestMapping("/test") public String test() { log.info("6666测试3333"); return "test"; } }7. Dubbo的使用总结
上面所述,user作为服务提供者,auth作为消费者。
主要有三个注解:@EnableDubbo、@DubboService(version = "1.0.0") 和 @DubboReference(version = "1.0.0")
引入 mfc-rpc 后,
服务提供者 user 需要实现 api 模块对外接口(XxxFacadeService)的功能,并在实现类上添加注解DubboService(version = "1.0.0")
消费者 auth 则使用注解 @DubboReference(version = "1.0.0") 注入 XxxFacadeService 即可使用其实现的方法。
nacos服务列表如下:
8.问题
在mfc-user中,抛出的异常信息,由于跨服务了,在mfc-auth没有得到正确的处理,从而触发兜底的错误处理(GlobalWebExceptionHandler ),没有拿到对应的错误信息,对前端提示原因不明确,用户体验不友好!
这个问题暂未解决!
