成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

為粉絲定制的SpringBoot服務端組件,零修改直接上線生產!

開發 前端
在Spring Boot中,我們可以通過引入spring-boot-starter-validation來實現參數校驗。這允許我們在模型類上使用如@NotNull、@Email等注解,進行基礎的校驗。為了實現更細粒度的參數校驗(如分組校驗),我們可以自定義校驗組。

前幾天,一位粉絲讓我為他實現一個基于Spring Boot的后端公共組件,需求如下:

  1. 支持參數校驗和分組校驗。
  2. 實現全局異常處理。
  3. 接口統一響應,并且返回體需要加密。
  4. 對接口實現版本控制。
  5. 對接口參數進行加簽,防止重放攻擊,確保接口安全。

本文將詳細介紹如何實現這些功能,幫助大家快速搭建符合這些需求的公共組件。

1. 參數校驗及分組校驗

在Spring Boot中,我們可以通過引入spring-boot-starter-validation來實現參數校驗。這允許我們在模型類上使用如@NotNull、@Email等注解,進行基礎的校驗。為了實現更細粒度的參數校驗(如分組校驗),我們可以自定義校驗組。

1.1 引入依賴

首先,在pom.xml中加入spring-boot-starter-validation依賴:

<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-validation</artifactId>  
</dependency>

1.2 實現分組校驗

我們可以創建一個自定義接口讓其繼承javax.validation.groups.Default類,用來定義不同的校驗分組:

public interface ValidGroup extends Default {  
  
    interface Update extends ValidGroup{  
  
    }  
    interface Create  extends ValidGroup{  
  
    }  
    interface Query extends ValidGroup{  
  
    }  
    interface Delete extends ValidGroup{  
  
    }
}

使用時,可以在字段上指定校驗分組:

@NotNull(groups = ValidGroup.Update.class, message = "應用ID不能為空")
 private String appId;

這樣,我們就能根據不同的場景進行靈活的參數校驗。

2. 全局異常響應

為了統一處理項目中的異常,我們可以創建一個全局異常處理類,并使用@RestControllerAdvice注解進行標注。在Spring Boot組件中,我們需要通過spring.factories文件進行配置,確保Spring Boot自動識別并加載該配置類。

2.1 創建全局異常處理類

@Slf4j  
@RestControllerAdvice  
public class GlobalExceptionHandler {  
    // 處理參數驗證異常  
    @SneakyThrows  
    @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class, ValidationException.class})  
    public Result<Void> handleValidException(HttpServletRequest request, Exception e) {  
        ...
        logError(request.getMethod(), getUrl(request),exceptionStr);  
        return ResultFactory.fail(ResultCode.CLIENT_ERROR, exceptionStr);  
    }  
  
    // 處理自定義異常  
    @ExceptionHandler(value = {AbstractException.class})  
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)  
    public Result<Void> handleAbstractException(HttpServletRequest request, AbstractException ex) {  
        ...
        return ResultFactory.fail(ex);  
    }  
  
    // 兜底處理  
    @ExceptionHandler(value = Throwable.class)  
    public Result<Void> handleThrowable(HttpServletRequest request, Throwable throwable) {  
        return ResultFactory.fail(ResultCode.SERVICE_ERROR, "系統異常,請聯系管理員!");  
    }  
  
    //記錄日志  
    private void logError(String method, String requestUrl, String exceptionStr){  
        log.error("[{}] {} [ex] {}", method, requestUrl, exceptionStr);  
    }  
}

2.2 注冊異常處理類

在組件的配置類中進行注冊:

@SpringBootConfiguration  
@ConditionalOnWebApplication  
public class WebAutoConfiguration {  
    @Bean  
    @ConditionalOnMissingBean(GlobalExceptionHandler.class)  
    public GlobalExceptionHandler globalExceptionHandler() {  
        return new GlobalExceptionHandler();  
    }  
}

在spring.factories文件中指定配置類路徑:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
  com.lxjk.core.web.configuration.WebAutoConfiguration

粉絲SpringBoot版本使用的是2.3,而在SpringBoot2.7以后路徑變成resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports/

3. 接口統一響應及返回體加密

為了統一返回接口響應體,并實現返回體加密,我們可以定義一個統一的返回類型Result,并通過ResponseBodyAdvice進行加密處理。

3.1 定義返回結果類

@Data
@Accessors(chain = true)
public class Result<T> {
    
    public static final String SUCCESS_CODE = "OK";
    public static final String SUCCESS_MESSAGE = "操作成功";
    
    private String code;
    
    private String message;
    
    private T data;
    
    private long timestamp;
    
}

3.2 創建工具類

@Slf4j
public class ResultFactory {
    
    public static <T> Result<T> success(T data) {
        return new Result<T>()
                .setCode(SUCCESS_CODE)
                .setMessage(SUCCESS_MESSAGE)
                .setData(data)
                .setTimestamp(System.currentTimeMillis());
    }
    
    public static Result<Void> fail(String code, String message) {
        return new Result<Void>()
                .setCode(code)
                .setMessage(message)
                .setTimestamp(System.currentTimeMillis());
    }
    
}

3.3 返回體加密

為了保證數據安全,我們可以通過ResponseBodyAdvice對返回結果進行加密處理:

@Slf4j
@RestControllerAdvice
public class ResponseBodyEncryptAdvice implements ResponseBodyAdvice<Object> {
    
    //加解密算法策略
    private final ResponseBodyEncoder responseBodyEncoder;

    public ResponseBodyEncryptAdvice(ResponseBodyEncoder responseBodyEncoder) {
        this.responseBodyEncoder = responseBodyEncoder;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> aClass) {
       return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if(body == null){
            return JsonUtils.obj2String(ResultFactory.success(""));
        }

        if (body instanceof String) {
            // 當響應體是String類型時,使用ObjectMapper轉換,因為Spring默認使用StringHttpMessageConverter處理字符串,不會將字符串識別為JSON
            String encryptBody = responseBodyEncoder.encode((String) body);
            return JsonUtils.obj2String(ResultFactory.success(encryptBody));
        }

        if (body instanceof Result<?>) {
            // 已經包裝過的結果無需再次包裝
            return body;
        }

        String s = responseBodyEncoder.encode(JsonUtils.obj2String(body));

        return ResultFactory.success(s);
    }

}

這段代碼做了兩件事: 1、自動將返回結果包裝成Result對象 2、對于返回內容通過ResponseBodyEncoder接口進行加密

在這里ResponseBodyEncoder是一個接口,在本項目中采用的是AES算法進行加密,由于依賴的是接口也可以很方便替換成sm2、sm3等國密算法。

圖片圖片

3.4 在配置類中注入ResponseBodyEncryptAdvice

@SpringBootConfiguration
@ConditionalOnWebApplication
public class WebAutoConfiguration {

    @Value("${lxjk.response.aes.secretKey}")
    private String secretKey;

   
    /**
     * 響應體加密算法
     */
    @Bean
    public ResponseBodyEncoder bodyEncoder() {
        return new AesResponseBodyEncoder(secretKey);
    }

    /**
     *  接口自動包裝
     */
    @Bean
    @ConditionalOnMissingBean(ResponseBodyEncryptAdvice.class)
    public ResponseBodyEncryptAdvice dailyMartGlobalResponseBodyAdvice() {
        return new ResponseBodyEncryptAdvice(bodyEncoder());
    }
    
}

3.5 控制器示例

@RequestMapping("api/user")
@RestController
@Slf4j
public class UserV1Controller {

    @GetMapping("/test")
    public Map<String,String> test() {
        Map<String,String> map = new HashMap<>();
        map.put("name","jianzh5");
        map.put("nickName","Java日知錄");
        return map;
    }
}

返回結果如下:

{
    "code": "OK",
    "message": "操作成功",
    "data": "6zscPzSDXFFHjicgwHc7vMkBDknHhoPfFsgjK8ZdchgAjtem3iR/cu96CXorIfLJ",
    "timestamp": 1735281442972
}

4. 接口版本控制

在Spring Boot項目中,接口版本控制是一個常見的需求,特別是當API接口不斷迭代時。版本控制可以幫助不同版本的API并存,同時避免影響到舊版用戶。我們可以通過路徑或請求頭的方式來實現接口版本控制:

  • 基于Path控制實現

http://example.com/v1/user    與  http://example.com/v1/user 分別對應一個接口的不同版本。

  • 基于Header控制實現

訪問相同接口時在請求頭中攜帶不同的參數如X-VERSION控制訪問不同的接口。

本文將重點介紹基于路徑的接口版本控制方法。

4.1 創建版本控制注解

首先,我們需要創建一個自定義注解@ApiVersion,用于標注API接口的版本。這個注解可以在控制器類或方法級別使用。

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiVersion {
    String value() default "v1";
}

該注解有一個value屬性,表示接口的版本,默認為v1。

4.2 創建版本條件類

接下來,我們需要定義一個RequestCondition實現類,用于處理版本條件。在該類中,我們將根據請求的URL路徑判斷接口版本,并與@ApiVersion注解中的版本進行匹配。

@Getter
@Slf4j
public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> {

    
    private static final Pattern VERSION_PREFIX_PATTERN_1 = Pattern.compile("/v\\d\\.\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_2 = Pattern.compile("/v\\d\\.\\d/");
    private static final Pattern VERSION_PREFIX_PATTERN_3 = Pattern.compile("/v\\d/");

    private static final List<Pattern> VERSION_LIST = Collections.unmodifiableList(
            Arrays.asList(VERSION_PREFIX_PATTERN_1, VERSION_PREFIX_PATTERN_2, VERSION_PREFIX_PATTERN_3)
    );

    private static final ConcurrentMap<String, String> VERSION_CACHE = new ConcurrentHashMap<>();

    private final String apiVersion;

    public ApiVersionCondition(String apiVersion) {
        this.apiVersion = apiVersion;
    }


    @Override
    public ApiVersionCondition combine(ApiVersionCondition other) {
        return new ApiVersionCondition(other.apiVersion);
    }

    @Override
    public ApiVersionCondition getMatchingCondition(HttpServletRequest request) {
        String requestUri = request.getRequestURI();
        String cachedVersion = VERSION_CACHE.get(requestUri);
        if (cachedVersion != null && Objects.equals(cachedVersion, this.apiVersion)) {
            return this;
        }

        for (Pattern pattern : VERSION_LIST) {
            Matcher m = pattern.matcher(request.getRequestURI());
            if (m.find()) {
                String version = m.group(0).replace("/", "");
                //推薦使用精確匹配版本號
                //如果選擇降低版本匹配,如有兩個版本1.1和1.2 訪問1.5 自動跳轉到1.2,不僅會影響匹配性能并且會導致版本不準確,容易產生誤解
                if (Objects.equals(version, this.apiVersion)) {
                    VERSION_CACHE.put(requestUri, version);
                    return this;
                }
            }
        }
        return null;

    }

    @Override
    public int compareTo(ApiVersionCondition other, HttpServletRequest httpServletRequest) {
        return 0;
    }
}

4.3 自定義HandlerMapping實現接口版本控制

為了讓Spring識別并根據版本條件處理請求,我們需要自定義一個HandlerMethod實現版本匹配邏輯。這一部分的關鍵是通過RequestCondition來判斷請求是否符合該版本。

public class ApiVersionRequestMappingHandler extends RequestMappingHandlerMapping {

    @Override
    protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class);
        return null == apiVersion ? super.getCustomTypeCondition(handlerType) : new ApiVersionCondition(apiVersion.value());
    }

    @Override
    protected RequestCondition<?> getCustomMethodCondition(Method method) {
        ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class);
        return null == apiVersion ? super.getCustomMethodCondition(method) : new ApiVersionCondition(apiVersion.value());
    }
}

4.4 完成配置

在Spring Boot應用的配置類中,我們需要確保API版本控制邏輯生效。我們可以通過@Configuration注解將自定義的Handlermapping加入到Spring的RequestMappingHandlerMapping中。

@SpringBootConfiguration
public class ApiMappingRegistration implements WebMvcRegistrations {

    @Override
    public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
        return new ApiVersionRequestMappingHandler();
    }

}

4.5 控制器實例

在控制器中,我們可以根據版本來定義不同的接口,默認版本號是v1,如果方法和類上都有注解,以方法上的為準。

@Api(tags = "用戶API")
@RequestMapping("api/{v}/user")
@RestController
@Slf4j
public class UserV1Controller {

    @ApiVersion("v1")
    @ApiOperation("test1")
    @GetMapping("/test")
    public String testv1() {
        return "this is v1.0.0 user";
    }

    @ApiVersion("v2")
    @ApiOperation("test2")
    @GetMapping("/test")
    public Map<String,String> testv2() {
        Map<String,String> map = new HashMap<>();
        map.put("name","jianzh5");
        map.put("nickName","Java日知錄");
        return map;
    }
}

4.6 兼容Swagger接口文檔

在實現了接口版本控制后,我們會遇到一個問題:Swagger文檔中顯示的接口路徑仍為api/{v}/user,其中的{v}占位符未被替換為實際的版本號,這不利于在線調試。

圖片圖片

為了解決這個問題,我們需要在ApiVersionRequestMappingHandler類中重寫registerHandlerMethod方法,動態替換路徑中的{v}占位符為實際的版本號。

public class ApiVersionRequestMappingHandler extends RequestMappingHandlerMapping {
    @Override
    protected void registerHandlerMethod(Object handler, Method method, RequestMappingInfo mapping) {
        //獲取方法上的ApiVersion注解
        ApiVersion apiVersion = method.getAnnotation(ApiVersion.class);
        if (apiVersion == null) {
             //獲取類上的ApiVersion注解
            apiVersion = AnnotationUtils.findAnnotation(method.getDeclaringClass(), ApiVersion.class);
        }
        if (apiVersion != null) {
            String version =  apiVersion.value();
            PatternsRequestCondition apiPattern = new PatternsRequestCondition(
                    mapping.getPatternsCondition().getPatterns().stream()
                            .map(pattern -> pattern.replace("{v}", version))
                            .toArray(String[]::new)
            );
            mapping = new RequestMappingInfo(
                    mapping.getName(),
                    apiPattern,
                    mapping.getMethodsCondition(),
                    mapping.getParamsCondition(),
                    mapping.getHeadersCondition(),
                    mapping.getConsumesCondition(),
                    mapping.getProducesCondition(),
                    mapping.getCustomCondition()
            );
        }
        super.registerHandlerMethod(handler, method, mapping);
    }
}

通過這種方式,我們能夠動態地將路徑中的{v}占位符替換為對應的版本號。例如,當接口的版本為v1時,接口路徑就會變為api/v1/user,從而解決了Swagger接口文檔中的占位符問題。

圖片圖片

5. 接口安全管理

為了確保暴露在外網的API接口的安全性,我們需要實現防篡改和防重放機制。這兩個措施能夠有效保護接口免受惡意攻擊和濫用。

5.1 防篡改

防篡改機制通常通過參數簽名來實現。具體而言,調用方將請求參數按照字典順序排序后進行加密,得到簽名(sign1)。然后,調用方將參數和簽名一同發送給后端服務。后端服務在接收到請求后,使用相同的排序規則和加密算法對參數進行簽名,得到另一個簽名(sign2)。如果sign1與sign2不一致,說明請求參數被篡改,后端服務將拒絕該請求。

這種方式能夠有效防止數據在傳輸過程中被篡改,確保接口的完整性和真實性。

5.2 防重放

防重放機制通過nonce(隨機字符串)和timestamp(時間戳)來實現。nonce是一個每次請求唯一且僅能使用一次的隨機字符串,而timestamp表示請求的時間。防重放的處理邏輯如下:

  • 時間檢查:首先檢查請求的timestamp是否超過了預設的接口處理時間限制。如果超時,則認為請求無效。
  • Redis檢查:通過nonce值在Redis中查詢是否已經存在與之對應的key (nonce:{nonce}),如果存在,表示該請求是重復請求,屬于重放攻擊。
  • 設置Redis Key過期時間:如果nonce未曾使用,則在Redis中設置該nonce值,并為其設置過期時間,過期時間通常與timestamp的有效期一致。

通過這種方式,防止了攻擊者利用截獲的請求包進行重放,確保每次請求都是唯一且有效的。

圖片圖片

5.3 代碼實現

  • 創建自定義過濾器

在自定義組件中,我們可以創建一個接口過濾器,攔截并驗證請求的安全性:

@Slf4j
public class SignatureFilter implements Filter {

    //從filter配置中獲取sign過期時間
    private Long signMaxTime;

    private final Map<String,String> nonceMap = new HashMap<>();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

        log.info("過濾URL:{}", httpRequest.getRequestURI());

        HttpServletRequestWrapper requestWrapper  = new SignRequestWrapper(httpRequest);

        RequestHeader requestHeader =buildRequestHeader(httpRequest);

        //Step1. 驗證請求頭是否存在
        if (!validateRequestHeader(requestHeader, httpResponse)) return;

        //Step2. 驗證時間戳是否過期
        if (!validateTimestamp(requestHeader, httpResponse)) return;

        //Step3. 驗證nonce是否被使用過
        if (!validateNonce(requestHeader, httpResponse)) return;

        //Step4. 驗證簽名是否正確
        if (validateSignature(httpRequest, requestWrapper, requestHeader)) {
            filterChain.doFilter(requestWrapper, servletResponse);
        } else {
            responseFail(httpResponse, ResultCode.SIGNATURE_ERROR);
        }
    }
}
  • 配置類注入過濾器

接下來,創建配置類來注入這個過濾器,并指定需要攔截的URL路徑。

@SpringBootConfiguration
public class SignatureFilterConfiguration {

    @Value("${lxjk.sign.maxTime:60}")
    private String signMaxTime;

    //filter中的初始化參數
    private final Map<String, String> initParametersMap =  new HashMap<>();

    @Bean
    public FilterRegistrationBean<SignatureFilter> contextFilterRegistrationBean() {
        initParametersMap.put("signMaxTime",signMaxTime);

        FilterRegistrationBean<SignatureFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(signatureFilter());
        registration.setInitParameters(initParametersMap);
        registration.addUrlPatterns("/api/pv/*");
        registration.setName("SignatureFilter");
        // 設置過濾器被調用的順序
        registration.setOrder(1);
        return registration;
    }



    @Bean
    public SignatureFilter signatureFilter() {
        return new SignatureFilter();
    }

}

6. 總結

本文介紹了如何通過Spring Boot實現常見的后端公共功能,包括:

  • 參數校驗:通過注解和分組校驗進行數據驗證。
  • 全局異常處理:通過@RestControllerAdvice實現統一的異常處理。
  • 接口統一響應與加密:通過ResponseBodyAdvice進行返回體加密,確保接口數據的安全性。
  • 接口版本控制:使用自定義注解和條件判斷來實現版本控制。
  • 接口簽名與防重放攻擊:通過Md5加密、簽名驗證和nonce來防止重放攻擊和篡改數據。
責任編輯:武曉燕 來源: JAVA日知錄
相關推薦

2025-03-05 08:04:31

2024-11-05 18:18:48

2024-04-10 10:09:07

2016-03-18 09:04:42

swift服務端

2023-07-26 10:21:26

服務端組件客戶端

2021-10-19 08:58:48

Java 語言 Java 基礎

2013-03-25 10:08:44

PHPWeb

2012-03-02 10:38:33

MySQL

2010-03-19 18:17:17

Java Server

2024-03-06 14:58:52

客戶端微服務架構

2010-08-03 09:59:30

NFS服務

2016-11-03 09:59:38

kotlinjavaspring

2016-08-04 14:41:21

架構java服務端開發

2021-10-14 08:39:17

Java Netty Java 基礎

2021-05-25 08:20:37

編程技能開發

2021-10-21 08:21:10

Java Reflect Java 基礎

2010-11-19 14:22:04

oracle服務端

2010-03-04 16:09:58

Ubuntu apac

2010-03-18 18:09:36

Java Socket

2021-04-26 13:20:06

Vue服務端渲染前端
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产女人第一次做爰毛片 | 国产日韩欧美二区 | www.狠狠干 | 成人永久免费视频 | 国产欧美日韩一区二区三区在线 | 久久激情五月丁香伊人 | 99国产精品久久久久 | 欧美午夜视频 | 精品国产乱码久久久 | 亚洲区一区二 | 国产成人免费视频 | 综合久久综合久久 | 久久成人av电影 | 久久lu | 在线视频一区二区 | 欧美嘿咻 | 午夜精品一区二区三区在线视频 | 精品九九九 | 亚洲精品久久久一区二区三区 | 2019天天操 | 免费精品久久久久久中文字幕 | av一级久久 | 粉嫩国产精品一区二区在线观看 | 欧美在线亚洲 | yiren22综合网成人 | 中文在线a在线 | 欧美午夜精品理论片a级按摩 | 国产精品视频久久久 | 国产精品自产av一区二区三区 | 国产精品成av人在线视午夜片 | www.日韩| 中国一级特黄毛片大片 | 99精品一区二区三区 | 日韩欧美手机在线 | 日本特黄a级高清免费大片 国产精品久久性 | 免费成人高清在线视频 | 亚洲精品久久 | 狠狠色香婷婷久久亚洲精品 | 日韩一区二区三区精品 | 夜夜爽夜夜操 | m豆传媒在线链接观看 |