weblog/doc/7、Gateway 网关搭建与接口鉴权/7.11 密码修改接口开发 BCrypt 随机 “盐” 加密.md
2025-02-17 11:57:55 +08:00

7.9 KiB
Raw Blame History

本小节中,我们将为 Gateway 网关服务添加全局异常处理,统一一波接口出参格式。

问题复现

首先,我们来看看目前网关服务存在的问题,编辑 SaTokenConfigure 配置类,对登出接口添加权限校验,校验登录的用户是否拥有 admin 角色:

代码如下:

SaRouter.match("/auth/user/logout", r -> StpUtil.checkRole("admin"));

重启网关服务,请求一波登出接口,由于目前已注册的用户,只有一个普通用户角色,于是,网关会提示你:无此角色admin:

*发现问题了没有?*返参的格式是下面这样的:

{
	"code": 500,
	"msg": "无此角色admin",
	"data": null
}

和我们已经定好的接口出参格式,是不一致的:

{
	"success": true,
	"message": null,
	"errorCode": null,
	"data": null
}

接下来,我们就将为网关服务添加全局异常捕获器,统一一下 JSON 出参格式。

删除 SaToken 默认的异常处理

添加全局异常捕获之前,首先编辑 SaTokenConfigure 配置类,将 .setError() 方法删除掉,防止等会自定义的全局异常处理失效:

代码如下:

package com.quanxiaoha.xiaohashu.gateway.auth;

import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.reactor.filter.SaReactorFilter;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class SaTokenConfigure {
    // 注册 Sa-Token全局过滤器
    @Bean
    public SaReactorFilter getSaReactorFilter() {
        return new SaReactorFilter()
                // 拦截地址
                .addInclude("/**")    /* 拦截全部path */
                // 鉴权方法:每次访问进入
                .setAuth(obj -> {
                    // 登录校验
                    SaRouter.match("/**") // 拦截所有路由
                            .notMatch("/auth/user/login") // 排除登录接口
                            .notMatch("/auth/verification/code/send") // 排除验证码发送接口
                            .check(r -> StpUtil.checkLogin()) // 校验是否登录
                    ;

                    // 权限认证 -- 不同模块, 校验不同权限   
                    SaRouter.match("/auth/user/logout", r -> StpUtil.checkRole("admin"));

                    // 更多匹配 ...  */
                })
                ;
    }
}

添加错误响应枚举类

接着,在网关服务中新建 /enums 枚举包,并创建 ResponseCodeEnum 异常枚举类,代码如下:

package com.quanxiaoha.xiaohashu.gateway.enums;

import com.quanxiaoha.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {

    // ----------- 通用异常状态码 -----------
    SYSTEM_ERROR("500", "系统繁忙,请稍后再试"),
    UNAUTHORIZED("401", "权限不足"),


    // ----------- 业务异常状态码 -----------
    ;

    // 异常码
    private final String errorCode;
    // 错误信息
    private final String errorMessage;

}

在异常响应枚举类中,先定义两个现阶段需要用到的错误码枚举值:

  • SYSTEM_ERROR :系统繁忙错误;
  • UNAUTHORIZED : 权限不足,若 SaToken 权限校验失败,则统一返回此错误码;

网关全局异常捕获

接着,在网关服务中创建一个 /exception 异常包,用于统一放置异常相关的代码。然后,创建 GlobalExceptionHandler 全局异常处理器,代码如下:

package com.quanxiaoha.xiaohashu.gateway.exception;

import cn.dev33.satoken.exception.SaTokenException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.quanxiaoha.framework.common.response.Response;
import com.quanxiaoha.xiaohashu.gateway.enums.ResponseCodeEnum;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * @author: 犬小哈
 * @date: 2024/4/5 19:18
 * @version: v1.0.0
 * @description: 全局异常处理
 **/
@Component
@Slf4j
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 获取响应对象
        ServerHttpResponse response = exchange.getResponse();

        log.error("==> 全局异常捕获: ", ex);

        // 响参
        Response<?> result;
        // 根据捕获的异常类型,设置不同的响应状态码和响应消息
        if (ex instanceof SaTokenException) { // Sa-Token 异常
            // 权限认证失败时,设置 401 状态码
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            // 构建响应结果
            result = Response.fail(ResponseCodeEnum.UNAUTHORIZED.getErrorCode(), ResponseCodeEnum.UNAUTHORIZED.getErrorMessage());
        } else { // 其他异常,则统一提示 “系统繁忙” 错误
            result = Response.fail(ResponseCodeEnum.SYSTEM_ERROR);
        }

        // 设置响应头的内容类型为 application/json;charset=UTF-8表示响应体为 JSON 格式
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
        // 设置 body 响应体
        return response.writeWith(Mono.fromSupplier(() -> { // 使用 Mono.fromSupplier 创建响应体
            DataBufferFactory bufferFactory = response.bufferFactory();
            try {
                // 使用 ObjectMapper 将 result 对象转换为 JSON 字节数组
                return bufferFactory.wrap(objectMapper.writeValueAsBytes(result));
            } catch (Exception e) {
                // 如果转换过程中出现异常,则返回空字节数组
                return bufferFactory.wrap(new byte[0]);
            }
        }));
    }
}

解释一波异常处理器的代码:

  • 和 Spring Boot 中使用 @ControllerAdvice 注解,来定义全局异常捕获器不同。在网关中,你需要创建 ErrorWebExceptionHandler 的实现类,并将其注入到 Spring 容器中;
  • handle() 异常处理方法中,对方法的入参 ex 异常进行类型判断,从而设置不同的响应状态码和响应消息:
    • 如果异常类型为 SaTokenException,则设置 401 状态码,并构建响应结果;
    • 其他异常,则统一提示 “系统繁忙” 错误;
  • 设置响应头的内容类型为 application/json;charset=UTF-8,表示响应体为 JSON 格式;
  • 设置 body 响应体,并返回响参;

自测一波

全局异常捕获器添加完毕后,重启网关服务,再次测试一波登出接口,看看出参效果:

如上图所示,响参的 JSON 格式已经被统一了,错误提示信息为权限不足,并且状态码为 401

本小节源码下载

https://t.zsxq.com/wMlWj