微服务基础骨架搭建-02 这次要实现用户的注册、登录功能。sa-token鉴权dobbo远程调用nacos注册配置中心涉及的模块用户模块 mfc-user、认证模块 mfc-auth、common下的模块mfc-api、mfc-config、mfc-rpc、mfc-sa-token1. mfc-sa-token这个模块只是做了简单的依赖的引入和通用配置1.1 依赖sa-token.version1.45.0/sa-token.version commons-pool2.version2.11.1/commons-pool2.version !-- Sa-Token 权限认证在线文档https://sa-token.cc -- dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot3-starter/artifactId version${sa-token.version}/version /dependency !-- Sa-Token 整合 RedisTemplate -- dependency groupIdcn.dev33/groupId artifactIdsa-token-redis-template/artifactId version${sa-token.version}/version /dependency !-- 提供 Redis 连接池 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId version${commons-pool2.version}/version /dependency1.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 依赖父pomspring-cloud-alibaba.version2025.0.0.0/spring-cloud-alibaba.version !-- Source: https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-alibaba-dependencies -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version${spring-cloud-alibaba.version}/version typepom/type scopeimport/scope /dependencypomdependencies !--nacos discovery-- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId exclusions exclusion groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-netflix-ribbon/artifactId /exclusion /exclusions /dependency !--nacos config-- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency /dependencies2.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.version3.2.19/dubbo.version !-- Source: https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -- dependency groupIdorg.apache.dubbo/groupId artifactIddubbo-spring-boot-starter/artifactId version${dubbo.version}/version scopecompile/scope /dependency !-- Source: https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-nacos -- dependency groupIdorg.apache.dubbo/groupId artifactIddubbo-registry-nacos/artifactId version${dubbo.version}/version scopecompile/scope /dependency3.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 groupIdcom.mfar/groupId artifactIdmfc-base/artifactId /dependency /dependencies4.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 groupIdcom.mfar/groupId artifactIdmfc-web/artifactId /dependency !--mfc-config-- dependency groupIdcom.mfar/groupId artifactIdmfc-config/artifactId /dependency !--mfc-base-- dependency groupIdcom.mfar/groupId artifactIdmfc-datasource/artifactId /dependency !--mfc-api-- dependency groupIdcom.mfar/groupId artifactIdmfc-api/artifactId /dependency !--mfc-rpc-- dependency groupIdcom.mfar/groupId artifactIdmfc-rpc/artifactId /dependency !--starter-- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency dependency groupIdorg.mapstruct/groupId artifactIdmapstruct/artifactId /dependency /dependencies5.2 配置这里一个关键的点是 spring.config.importoptional: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 模块完成。注册入参emailcodepasswordconfirmPassword首先会进行邮箱格式和存在性的校验不符合抛异常接着进行两个密码的格式和一致性校验不符合抛异常接着从redis拿出验证码与用户的code比对不符合抛异常接着生成唯一的用户名尝试十次不成功抛异常接着加密密码设置各种属性值接着插入数据库不成功抛异常再查一把数据库获取完整的 User 对象转换成 UserInfo 对象分配角色等最后返回 UserInfo 对象。登录也差不多根据email查数据库比对密码或者验证码成功者返回 UserInfo 对象。拿到 UserInfo 后继续往下看怎么登录。6. mfc-auth接上面 5.4 登录拿到 UserInfo 对象后就使用 sa-token 的 StpUtil 工具进行登录并响应 UserInfo 给前端token 也会给前端这应该时 sa-token 框架实现的不用我们手动实现PostMapping(/register) public ResultUserInfo 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 groupIdcom.mfar/groupId artifactIdmfc-web/artifactId /dependency !--mfc-cache-- dependency groupIdcom.mfar/groupId artifactIdmfc-cache/artifactId /dependency !--mfc-sa-token-- dependency groupIdcom.mfar/groupId artifactIdmfc-sa-token/artifactId /dependency !--mfc-config-- dependency groupIdcom.mfar/groupId artifactIdmfc-config/artifactId /dependency !--mfc-api-- dependency groupIdcom.mfar/groupId artifactIdmfc-api/artifactId /dependency !--mfc-rpc-- dependency groupIdcom.mfar/groupId artifactIdmfc-rpc/artifactId /dependency !--starter-- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter/artifactId /dependency !--hutool-- dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId /dependency /dependencies6.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 RedisTemplateString, String redisTemplate; DubboReference(version 1.0.0) private UserFacadeService userFacadeService; /** * 发送注册验证码 * return */ RequestMapping(/sendRegisterCode/{email}) public ResultString 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 ResultString 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 ResultUserInfo 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 ResultUserInfo 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 ResultUserInfo 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 ResultString 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 没有拿到对应的错误信息对前端提示原因不明确用户体验不友好这个问题暂未解决