精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

拋棄AOP!SpringBoot + YAML 零侵入數(shù)據(jù)脫敏神操作!

數(shù)據(jù)庫 其他數(shù)據(jù)庫
不用寫復(fù)雜的 AOP 切面,不用改業(yè)務(wù)代碼,只改改 YAML 配置,就能實(shí)現(xiàn)接口響應(yīng)脫敏,還支持動(dòng)態(tài)配置、日志脫敏、性能優(yōu)化,新手也能快速上手。

兄弟們,今天咱們來聊個(gè)老生常談但又總讓人頭疼的話題 —— 數(shù)據(jù)脫敏。先跟大家嘮嘮我之前踩過的坑啊:去年做一個(gè)用戶中心項(xiàng)目,產(chǎn)品經(jīng)理拍著桌子說 “用戶手機(jī)號(hào)、身份證號(hào)必須脫敏!日志里不能有明文,接口返回也不能漏!”。我當(dāng)時(shí)一拍胸脯 “小意思,AOP 搞定!”,結(jié)果呢?

寫了個(gè)@SensitiveField注解,又搞了個(gè)切面攔截 Controller 返回值,用反射遍歷字段處理。一開始挺順利,直到遇到嵌套對(duì)象 —— 比如User里套了個(gè)Address,Address里有個(gè)contactPhone要脫敏,我那切面直接懵了,遞歸反射寫了三層才搞定;后來又遇到集合,List<User>得循環(huán)處理每個(gè)元素,代碼越改越亂,最后切面里全是 if-else,跟個(gè)迷宮似的。

更坑的是上線后,運(yùn)維說 “你這接口響應(yīng)慢了 100ms”,查了半天發(fā)現(xiàn)是反射次數(shù)太多,尤其是高并發(fā)的時(shí)候,CPU 占用直接上去了。當(dāng)時(shí)我就想:就不能有個(gè)不用寫切面、不用改業(yè)務(wù)代碼,甚至連實(shí)體類都不用動(dòng)的脫敏方案嗎?

還真讓我找到了!今天就給大家分享這個(gè) “偷懶神器”——SpringBoot + YAML 零侵入數(shù)據(jù)脫敏方案。不用 AOP,不用加注解,改改配置文件就能搞定,新手看一遍也能上手,看完你絕對(duì)想收藏!

一、先搞懂:為啥要做數(shù)據(jù)脫敏?別等踩坑才后悔

在講方案之前,先跟沒接觸過脫敏的兄弟補(bǔ)補(bǔ)課 —— 別覺得脫敏是 “多此一舉”,等出了問題你就知道有多重要了。

舉個(gè)真實(shí)案例:前兩年某電商平臺(tái),開發(fā)在日志里打印了用戶的銀行卡號(hào)(明文),結(jié)果被黑客通過日志漏洞爬走了,最后不僅賠了用戶錢,還被監(jiān)管罰了幾百萬。你說這虧不虧?

咱們?nèi)粘i_發(fā)里,需要脫敏的場(chǎng)景主要有 3 個(gè):

  1. 接口返回:給前端返回用戶信息時(shí),手機(jī)號(hào)不能是13800138000,得是1388000;身份證號(hào)不能是110101199001011234,得是110101****1234
  2. 日志打印:不管是業(yè)務(wù)日志還是異常日志,只要有敏感信息,必須脫敏,不然日志文件就是 “定時(shí)炸彈”
  3. 數(shù)據(jù)庫存儲(chǔ):這個(gè)得區(qū)分情況 —— 像手機(jī)號(hào)、郵箱可以存明文(但響應(yīng)和日志要脫敏),但銀行卡號(hào)、身份證號(hào)這種高敏感信息,數(shù)據(jù)庫里最好存加密后的結(jié)果,脫敏只負(fù)責(zé) “前端展示”

簡單說:脫敏的核心是 “該看的人能看,不該看的人看不到”,既保證用戶信息安全,又不影響業(yè)務(wù)正常運(yùn)行。

之前用 AOP 做脫敏,雖然能實(shí)現(xiàn)功能,但有 3 個(gè)致命問題:

  • 侵入性強(qiáng):得給實(shí)體類加注解,改業(yè)務(wù)代碼,萬一后續(xù)要改脫敏規(guī)則,牽一發(fā)動(dòng)全身
  • 代碼復(fù)雜:處理嵌套對(duì)象、集合、基本類型,反射邏輯寫得頭暈,還容易出 bug
  • 性能拉胯:反射次數(shù)多,高并發(fā)場(chǎng)景下接口響應(yīng)變慢,CPU 占用飆升

而今天要講的方案,完美解決這 3 個(gè)問題 ——零侵入、配置化、輕量級(jí),咱們一步步來拆解。

二、核心原理:SpringBoot 自帶的 “響應(yīng)攔截神器”,比 AOP 更輕

很多兄弟不知道,SpringMVC 里有個(gè)叫ResponseBodyAdvice的接口,它能在 “響應(yīng)體返回給前端之前” 攔截處理,相當(dāng)于給響應(yīng)加了個(gè) “過濾器”。

咱們之前用 AOP,還得自己寫切面、定義切點(diǎn)(比如攔截所有@RestController的方法),而ResponseBodyAdvice是 Spring 官方提供的擴(kuò)展點(diǎn),不用處理復(fù)雜的切面表達(dá)式,也不用考慮攔截順序,比 AOP 更簡單、更輕量。

舉個(gè)通俗的例子:如果把接口響應(yīng)比作 “快遞”,ResponseBodyAdvice就是 “快遞分揀員”,在快遞送到用戶(前端)手里之前,先檢查一下里面有沒有 “敏感物品”(敏感字段),有就按規(guī)則 “包裝一下”(脫敏),再送出去。

整個(gè)方案的核心邏輯就是:

  1. 用ResponseBodyAdvice攔截所有接口響應(yīng)
  2. 從 YAML 配置里讀取 “哪些接口、哪些字段需要脫敏”
  3. 對(duì)響應(yīng)體里的敏感字段按規(guī)則處理
  4. 把處理后的響應(yīng)體返回給前端

全程不用改業(yè)務(wù)代碼,不用加注解,所有規(guī)則都在 YAML 里配置,這就是 “零侵入” 的關(guān)鍵!

三、實(shí)戰(zhàn)步驟:從 0 到 1 實(shí)現(xiàn),復(fù)制代碼就能用

咱們先定個(gè)目標(biāo):實(shí)現(xiàn)兩個(gè)接口的脫敏需求

  • 接口 1:GET /api/user/get,返回用戶信息,需要脫敏phone(手機(jī)號(hào))和idCard(身份證號(hào))
  • 接口 2:POST /api/order/list,返回訂單列表,需要脫敏bankCard(銀行卡號(hào))和user.phone(嵌套字段)

環(huán)境準(zhǔn)備:JDK 1.8+,SpringBoot 2.7.x(其他版本也能用,差別不大)

第一步:引入依賴,就兩個(gè),不多加

首先創(chuàng)建一個(gè) SpringBoot 項(xiàng)目,然后在pom.xml里加兩個(gè)依賴:

  • spring-boot-starter-web:必備,不用多說
  • hutool-all:國產(chǎn)工具包,里面有很多現(xiàn)成的脫敏方法,省得咱們自己寫正則
<dependencies>
    <!-- SpringBoot Web核心依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Hutool工具包:簡化脫敏、字符串處理 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.20</version> <!-- 用最新版就行 -->
    </dependency>
    <!-- 可選:如果用Lombok,加這個(gè),能少寫getter/setter -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

Hutool 不是必須的,如果你不想引入第三方包,自己寫正則也能實(shí)現(xiàn)脫敏,后面會(huì)講怎么自定義。

第二步:寫 YAML 配置,脫敏規(guī)則全在這里定

最關(guān)鍵的一步來了!咱們?cè)赼pplication.yml里配置脫敏規(guī)則,不用改任何 Java 代碼。

先看配置結(jié)構(gòu),我都加了注釋,一看就懂:

# 應(yīng)用基礎(chǔ)配置
spring:
  application:
    name: sensitive-demo
  # 多環(huán)境配置:開發(fā)環(huán)境可以關(guān)閉脫敏,方便調(diào)試
  profiles:
    active: dev
# 脫敏核心配置:dev環(huán)境(開發(fā))
---
spring:
  config:
    activate:
      on-profile: dev
# 開發(fā)環(huán)境關(guān)閉脫敏,方便調(diào)試接口,看明文數(shù)據(jù)
sensitive:
  enabled: false
# 脫敏核心配置:prod環(huán)境(生產(chǎn))
---
spring:
  config:
    activate:
      on-profile: prod
sensitive:
  enabled: true # 生產(chǎn)環(huán)境開啟脫敏
  # 接口脫敏映射:按接口配置需要脫敏的字段
  mappings:
    # 第一個(gè)接口:獲取用戶信息
    - path: /api/user/get
      method: GET # 請(qǐng)求方法:GET/POST/PUT/DELETE,不區(qū)分大小寫
      fields: # 需要脫敏的字段
        - name: phone # 字段名:對(duì)應(yīng)響應(yīng)體里的phone字段
          rule: mobile # 脫敏規(guī)則:mobile(手機(jī)號(hào))
        - name: idCard # 字段名:身份證號(hào)
          rule: idCard # 脫敏規(guī)則:idCard(身份證號(hào))
    # 第二個(gè)接口:獲取訂單列表
    - path: /api/order/list
      method: POST
      fields:
        - name: bankCard # 字段名:銀行卡號(hào)
          rule: bankCard # 脫敏規(guī)則:bankCard(銀行卡號(hào))
        - name: user.phone # 嵌套字段:order里的user對(duì)象的phone字段
          rule: mobile
    # 可以繼續(xù)加更多接口...
  # 自定義脫敏規(guī)則:如果Hutool的規(guī)則不夠用,自己加
  custom-rules:
    # 比如自定義郵箱脫敏規(guī)則:zhangsan@163.com → zh****@163.com
    - name: email
      regex: "([a-zA-Z0-9_]{2})[a-zA-Z0-9_]*@([a-zA-Z0-9.]+)" # 正則表達(dá)式
      replacement: "$1****@$2" # 替換規(guī)則:$1是第一個(gè)分組,$2是第二個(gè)分組

這里有幾個(gè)關(guān)鍵點(diǎn)要說明:

  1. 多環(huán)境區(qū)分:開發(fā)環(huán)境(dev)關(guān)閉脫敏,方便調(diào)試;生產(chǎn)環(huán)境(prod)開啟,保證安全。不用每次改代碼,切換環(huán)境就行。
  2. 接口映射:每個(gè)mapping對(duì)應(yīng)一個(gè)接口,path是接口路徑,method是請(qǐng)求方法,fields是需要脫敏的字段。
  3. 嵌套字段:支持user.phone這種嵌套字段,不管嵌套多少層,用 “.” 分隔就行。
  4. 脫敏規(guī)則:內(nèi)置了mobile、idCard、bankCard三種規(guī)則(后面會(huì)講怎么實(shí)現(xiàn)),還支持自定義規(guī)則(比如上面的email)。

第三步:把 YAML 配置映射成 Java 對(duì)象

SpringBoot 不能直接讀取 YAML 里的復(fù)雜結(jié)構(gòu)(比如mappings列表),所以咱們要寫個(gè)配置類,把 YAML 配置映射成 Java 對(duì)象,方便后續(xù)使用。

用@ConfigurationProperties注解就能實(shí)現(xiàn),代碼很簡單:

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
 * 脫敏配置類:把YAML里的sensitive配置映射成Java對(duì)象
 */
@Component
@ConfigurationProperties(prefix = "sensitive") // 對(duì)應(yīng)YAML里的sensitive節(jié)點(diǎn)
@Data // Lombok注解,省得寫getter/setter
public class SensitiveProperties {
    /**
     * 是否開啟脫敏功能:true=開啟,false=關(guān)閉
     */
    private boolean enabled = false;
    /**
     * 接口脫敏映射列表
     */
    private List<SensitiveMapping> mappings;
    /**
     * 自定義脫敏規(guī)則列表
     */
    private List<CustomRule> customRules;
    /**
     * 單個(gè)接口的脫敏配置
     */
    @Data
    public static class SensitiveMapping {
        /**
         * 接口路徑:比如/api/user/get
         */
        private String path;
        /**
         * 請(qǐng)求方法:GET/POST/PUT/DELETE,不區(qū)分大小寫
         */
        private String method;
        /**
         * 該接口需要脫敏的字段列表
         */
        private List<SensitiveField> fields;
    }
    /**
     * 單個(gè)字段的脫敏配置
     */
    @Data
    public static class SensitiveField {
        /**
         * 字段名:支持嵌套字段,比如user.phone
         */
        private String name;
        /**
         * 脫敏規(guī)則:比如mobile、idCard、bankCard,或自定義規(guī)則名
         */
        private String rule;
    }
    /**
     * 自定義脫敏規(guī)則
     */
    @Data
    public static class CustomRule {
        /**
         * 規(guī)則名:比如email,在fields.rule里引用
         */
        private String name;
        /**
         * 正則表達(dá)式:用來匹配敏感字段
         */
        private String regex;
        /**
         * 替換規(guī)則:比如$1****@$2
         */
        private String replacement;
    }
}

這里用了 Lombok 的@Data注解,如果你沒加 Lombok 依賴,自己寫 getter 和 setter 就行,功能一樣。

第四步:實(shí)現(xiàn)脫敏工具類,規(guī)則全在這里

接下來寫個(gè)脫敏工具類,負(fù)責(zé)實(shí)現(xiàn)具體的脫敏邏輯 —— 包括內(nèi)置規(guī)則(手機(jī)號(hào)、身份證號(hào)、銀行卡號(hào))和自定義規(guī)則(從 YAML 里讀)。

咱們用 Hutool 的DesensitizedUtil來實(shí)現(xiàn)內(nèi)置規(guī)則,省得自己寫正則,效率更高:

import cn.hutool.core.util.DesensitizedUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;
/**
 * 脫敏工具類:實(shí)現(xiàn)各種脫敏規(guī)則
 */
@Component
public class SensitiveUtil {
    /**
     * 自定義脫敏規(guī)則緩存:key=規(guī)則名,value=正則Pattern
     */
    private final Map<String, Pattern> customRulePatterns = new HashMap<>();
    /**
     * 自定義脫敏替換規(guī)則緩存:key=規(guī)則名,value=替換字符串
     */
    private final Map<String, String> customRuleReplacements = new HashMap<>();
    @Resource
    private SensitiveProperties sensitiveProperties;
    /**
     * 初始化:把YAML里的自定義規(guī)則加載到緩存
     */
    public void init() {
        if (sensitiveProperties.getCustomRules() == null) {
            return;
        }
        // 遍歷自定義規(guī)則,編譯正則表達(dá)式并緩存
        for (SensitiveProperties.CustomRule customRule : sensitiveProperties.getCustomRules()) {
            customRulePatterns.put(
                    customRule.getName(),
                    Pattern.compile(customRule.getRegex())
            );
            customRuleReplacements.put(
                    customRule.getName(),
                    customRule.getReplacement()
            );
        }
    }
    /**
     * 核心方法:根據(jù)規(guī)則脫敏字符串
     * @param value 原始字符串(比如手機(jī)號(hào)13800138000)
     * @param rule  脫敏規(guī)則(比如mobile)
     * @return 脫敏后的字符串(比如138****8000)
     */
    public String desensitize(String value, String rule) {
        // 1. 空值直接返回,避免空指針
        if (StrUtil.isBlank(value)) {
            return value;
        }
        // 2. 處理內(nèi)置規(guī)則
        switch (rule.toLowerCase()) {
            case "mobile": // 手機(jī)號(hào)脫敏:138****8000
                return DesensitizedUtil.mobilePhone(value);
            case "idcard": // 身份證號(hào)脫敏:110101********1234
                return DesensitizedUtil.idCardNum(value, 6, 4);
            case "bankcard": // 銀行卡號(hào)脫敏:6222*******1234
                return DesensitizedUtil.bankCard(value);
            default: // 3. 處理自定義規(guī)則
                return handleCustomRule(value, rule);
        }
    }
    /**
     * 處理自定義脫敏規(guī)則
     */
    private String handleCustomRule(String value, String rule) {
        // 檢查自定義規(guī)則是否存在
        if (!customRulePatterns.containsKey(rule) || !customRuleReplacements.containsKey(rule)) {
            return value; // 規(guī)則不存在,返回原始值,避免報(bào)錯(cuò)
        }
        // 用自定義的正則和替換規(guī)則處理
        Pattern pattern = customRulePatterns.get(rule);
        String replacement = customRuleReplacements.get(rule);
        return pattern.matcher(value).replaceAll(replacement);
    }
}

這里有兩個(gè)關(guān)鍵點(diǎn):

  1. 初始化方法:把 YAML 里的自定義規(guī)則編譯成Pattern緩存起來,避免每次脫敏都重新編譯正則(正則編譯很耗時(shí),緩存能提高性能)。
  2. 內(nèi)置規(guī)則:直接用 Hutool 的DesensitizedUtil,里面還有很多其他規(guī)則(比如郵箱、密碼),如果需要可以自己加。
  3. 自定義規(guī)則:通過正則匹配和替換實(shí)現(xiàn),靈活度很高,不管是郵箱、地址還是其他敏感字段,都能搞定。

第五步:實(shí)現(xiàn)核心攔截器,用 ResponseBodyAdvice

這是整個(gè)方案的 “靈魂”—— 用ResponseBodyAdvice攔截響應(yīng)體,然后調(diào)用上面的工具類進(jìn)行脫敏。

不用寫 AOP 切面,不用定義切點(diǎn),只要實(shí)現(xiàn)ResponseBodyAdvice接口就行,代碼比 AOP 簡單多了:

import cn.hutool.core.util.StrUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.annotation.Resource;
import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * 響應(yīng)體脫敏攔截器:用ResponseBodyAdvice實(shí)現(xiàn)零侵入脫敏
 */
@ControllerAdvice // 全局?jǐn)r截所有@Controller的響應(yīng)
public class SensitiveResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Resource
    private SensitiveProperties sensitiveProperties;
    @Resource
    private SensitiveUtil sensitiveUtil;
    /**
     * 接口脫敏配置緩存:key=path#method(比如/api/user/get#GET),value=該接口的敏感字段列表
     */
    private final Map<String, List<SensitiveProperties.SensitiveField>> sensitiveFieldCache = new ConcurrentHashMap<>();
    /**
     * 初始化:1. 初始化脫敏工具類 2. 緩存接口脫敏配置
     */
    @PostConstruct
    public void init() {
        // 1. 初始化脫敏工具類(加載自定義規(guī)則)
        sensitiveUtil.init();
        // 2. 緩存接口脫敏配置,避免每次請(qǐng)求都遍歷mappings
        if (sensitiveProperties.getMappings() == null) {
            return;
        }
        for (SensitiveProperties.SensitiveMapping mapping : sensitiveProperties.getMappings()) {
            // 生成key:path#method(統(tǒng)一轉(zhuǎn)小寫,避免大小寫問題)
            String key = mapping.getPath().toLowerCase() + "#" + mapping.getMethod().toLowerCase();
            sensitiveFieldCache.put(key, mapping.getFields());
        }
    }
    /**
     * 第一步:判斷當(dāng)前請(qǐng)求是否需要脫敏
     * @param returnType 方法返回類型
     * @param converterType 消息轉(zhuǎn)換器類型
     * @return true=需要脫敏,false=不需要
     */
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 1. 如果脫敏功能關(guān)閉,直接返回false
        if (!sensitiveProperties.isEnabled()) {
            return false;
        }
        // 2. 只處理JSON響應(yīng)(大部分項(xiàng)目都是JSON,XML可以自己加)
        return MediaType.APPLICATION_JSON.equalsTypeAndSubtype(MediaType.valueOf(converterType.getSimpleName()));
    }
    /**
     * 第二步:對(duì)響應(yīng)體進(jìn)行脫敏處理
     * @param body 原始響應(yīng)體
     * @param returnType 方法返回類型
     * @param selectedContentType 響應(yīng)類型
     * @param selectedConverterType 消息轉(zhuǎn)換器類型
     * @param request 請(qǐng)求對(duì)象
     * @param response 響應(yīng)對(duì)象
     * @return 脫敏后的響應(yīng)體
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        // 1. 獲取當(dāng)前請(qǐng)求的path和method
        String path = request.getURI().getPath().toLowerCase();
        String method = request.getMethod().name().toLowerCase();
        String cacheKey = path + "#" + method;
        // 2. 從緩存獲取當(dāng)前接口的敏感字段列表
        List<SensitiveProperties.SensitiveField> sensitiveFields = sensitiveFieldCache.get(cacheKey);
        if (sensitiveFields == null || sensitiveFields.isEmpty()) {
            return body; // 沒有需要脫敏的字段,直接返回原始響應(yīng)體
        }
        // 3. 對(duì)響應(yīng)體進(jìn)行脫敏處理
        processSensitiveField(body, sensitiveFields);
        // 4. 返回脫敏后的響應(yīng)體
        return body;
    }
    /**
     * 核心方法:處理響應(yīng)體里的敏感字段
     * @param body 響應(yīng)體對(duì)象(可能是單個(gè)對(duì)象、集合、Map等)
     * @param sensitiveFields 敏感字段列表
     */
    private void processSensitiveField(Object body, List<SensitiveProperties.SensitiveField> sensitiveFields) {
        if (body == null) {
            return;
        }
        // 1. 如果是集合(List、Set等),遍歷每個(gè)元素處理
        if (body instanceof List<?>) {
            List<?> list = (List<?>) body;
            for (Object item : list) {
                processSensitiveField(item, sensitiveFields); // 遞歸處理每個(gè)元素
            }
            return;
        }
        // 2. 如果是Map(比如接口返回Map<String, Object>),遍歷每個(gè)entry處理
        if (body instanceof Map<?, ?>) {
            Map<?, ?> map = (Map<?, ?>) body;
            for (Map.Entry<?, ?> entry : map.entrySet()) {
                Object value = entry.getValue();
                if (value == null) {
                    continue;
                }
                // 檢查當(dāng)前key是否是敏感字段
                String key = entry.getKey().toString();
                for (SensitiveProperties.SensitiveField sensitiveField : sensitiveFields) {
                    // 如果是簡單字段(不是嵌套字段),直接脫敏
                    if (StrUtil.equals(key, sensitiveField.getName())) {
                        String desensitizedValue = sensitiveUtil.desensitize(value.toString(), sensitiveField.getRule());
                        entry.setValue(desensitizedValue);
                        break;
                    }
                }
                // 遞歸處理Map里的嵌套對(duì)象
                processSensitiveField(value, sensitiveFields);
            }
            return;
        }
        // 3. 如果是普通Java對(duì)象(比如User、Order),用反射處理字段
        Class<?> clazz = body.getClass();
        // 遍歷所有敏感字段,處理每個(gè)字段
        for (SensitiveProperties.SensitiveField sensitiveField : sensitiveFields) {
            try {
                // 處理字段:支持嵌套字段(比如user.phone)
                Object fieldValue = getNestedFieldValue(body, sensitiveField.getName());
                if (fieldValue == null) {
                    continue;
                }
                // 脫敏處理:把字段值轉(zhuǎn)成字符串,脫敏后再設(shè)回去
                String desensitizedValue = sensitiveUtil.desensitize(fieldValue.toString(), sensitiveField.getRule());
                setNestedFieldValue(body, sensitiveField.getName(), desensitizedValue, fieldValue.getClass());
            } catch (Exception e) {
                // 遇到異常不拋出,避免影響接口正常返回(脫敏是輔助功能,不能讓它搞崩主流程)
                e.printStackTrace();
            }
        }
    }
    /**
     * 獲取嵌套字段的值:比如從Order對(duì)象里獲取user.phone的值
     * @param obj 目標(biāo)對(duì)象(比如Order)
     * @param fieldName 嵌套字段名(比如user.phone)
     * @return 字段值(比如13800138000)
     */
    private Object getNestedFieldValue(Object obj, String fieldName) throws Exception {
        if (StrUtil.isBlank(fieldName)) {
            return null;
        }
        // 分割字段名:比如"user.phone" → ["user", "phone"]
        String[] fieldNames = fieldName.split("\\.");
        Object currentObj = obj;
        for (String name : fieldNames) {
            if (currentObj == null) {
                return null;
            }
            // 獲取當(dāng)前對(duì)象的字段(包括私有字段)
            Field field = getDeclaredField(currentObj.getClass(), name);
            if (field == null) {
                return null;
            }
            field.setAccessible(true); // 突破私有字段訪問限制
            currentObj = field.get(currentObj); // 獲取字段值,作為下一層的對(duì)象
        }
        return currentObj;
    }
    /**
     * 設(shè)置嵌套字段的值:比如給Order對(duì)象的user.phone設(shè)置脫敏后的值
     * @param obj 目標(biāo)對(duì)象(比如Order)
     * @param fieldName 嵌套字段名(比如user.phone)
     * @param value 脫敏后的值(比如138****8000)
     * @param fieldType 字段類型(比如String)
     */
    private void setNestedFieldValue(Object obj, String fieldName, String value, Class<?> fieldType) throws Exception {
        if (StrUtil.isBlank(fieldName)) {
            return;
        }
        // 分割字段名:比如"user.phone" → ["user", "phone"]
        String[] fieldNames = fieldName.split("\\.");
        Object currentObj = obj;
        // 遍歷到倒數(shù)第二個(gè)字段(比如"user")
        for (int i = 0; i < fieldNames.length - 1; i++) {
            String name = fieldNames[i];
            Field field = getDeclaredField(currentObj.getClass(), name);
            if (field == null) {
                return;
            }
            field.setAccessible(true);
            currentObj = field.get(currentObj); // 獲取下一層對(duì)象(比如user對(duì)象)
            if (currentObj == null) {
                return;
            }
        }
        // 處理最后一個(gè)字段(比如"phone")
        String lastFieldName = fieldNames[fieldNames.length - 1];
        Field lastField = getDeclaredField(currentObj.getClass(), lastFieldName);
        if (lastField == null) {
            return;
        }
        lastField.setAccessible(true);
        // 把脫敏后的字符串轉(zhuǎn)成字段對(duì)應(yīng)的類型(比如Long類型的phone)
        Object fieldValue = convertValue(value, fieldType);
        lastField.set(currentObj, fieldValue); // 設(shè)置脫敏后的值
    }
    /**
     * 獲取類的字段(包括父類的字段)
     * @param clazz 目標(biāo)類
     * @param fieldName 字段名
     * @return 字段對(duì)象
     */
    private Field getDeclaredField(Class<?> clazz, String fieldName) {
        // 遍歷當(dāng)前類和父類,直到找到字段或到Object類
        while (clazz != null && clazz != Object.class) {
            try {
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass(); // 沒找到,找父類
            }
        }
        return null; // 沒找到字段
    }
    /**
     * 把字符串轉(zhuǎn)成目標(biāo)類型(比如String → Long)
     * @param value 字符串值
     * @param targetType 目標(biāo)類型
     * @return 轉(zhuǎn)換后的值
     */
    private Object convertValue(String value, Class<?> targetType) {
        if (targetType == String.class) {
            return value;
        }
        if (targetType == Integer.class || targetType == int.class) {
            return Integer.parseInt(value);
        }
        if (targetType == Long.class || targetType == long.class) {
            return Long.parseLong(value);
        }
        // 其他類型可以自己加,比如Double、Boolean等
        return value;
    }
}

這段代碼雖然長,但邏輯很清晰,我分幾個(gè)部分給大家解釋:

1. 初始化和緩存

  • 用@PostConstruct注解在項(xiàng)目啟動(dòng)時(shí)初始化:加載自定義脫敏規(guī)則,把接口脫敏配置緩存到sensitiveFieldCache里(key 是path#method)。
  • 緩存的目的是避免每次請(qǐng)求都遍歷mappings列表,提高性能,尤其是接口多的時(shí)候。

2. supports 方法:判斷是否需要脫敏

  • 首先檢查脫敏功能是否開啟(sensitive.enabled),關(guān)閉的話直接返回false。
  • 只處理 JSON 響應(yīng)(大部分項(xiàng)目都是 JSON),如果需要處理 XML,可以自己加判斷。

3. beforeBodyWrite 方法:攔截響應(yīng)體

  • 獲取當(dāng)前請(qǐng)求的path和method,生成緩存 key,從緩存里拿敏感字段列表。
  • 如果沒有敏感字段,直接返回原始響應(yīng)體;有就調(diào)用processSensitiveField方法處理。

4. processSensitiveField 方法:核心處理邏輯

  • 處理集合:如果響應(yīng)體是List,遍歷每個(gè)元素遞歸處理。
  • 處理 Map:如果響應(yīng)體是Map,遍歷每個(gè) entry,檢查 key 是否是敏感字段,是就脫敏,然后遞歸處理 value 里的嵌套對(duì)象。
  • 處理普通對(duì)象:用反射處理,支持嵌套字段(比如user.phone),這里是整個(gè)方法的重點(diǎn)。

5. 嵌套字段處理

  • getNestedFieldValue:通過 “.” 分割字段名,逐層獲取對(duì)象的字段值,比如從Order里獲取user,再從user里獲取phone。
  • setNestedFieldValue:和上面相反,逐層找到最后一個(gè)字段,把脫敏后的值設(shè)回去。
  • getDeclaredField:獲取類的字段,包括父類的字段,解決私有字段訪問問題。

第六步:寫測(cè)試代碼,驗(yàn)證效果

咱們寫幾個(gè)測(cè)試類,看看脫敏效果到底怎么樣。

1. 定義實(shí)體類

先寫User、Order、Result三個(gè)實(shí)體類(Result是接口統(tǒng)一返回格式):

// User.java
import lombok.Data;
@Data
public class User {
    private Long id;
    private String name;
    private String phone; // 需要脫敏的字段
    private String idCard; // 需要脫敏的字段
}
// Order.java
import lombok.Data;
@Data
public class Order {
    private Long id;
    private String orderNo;
    private String bankCard; // 需要脫敏的字段
    private User user; // 嵌套對(duì)象,里面的phone需要脫敏
}
// Result.java:接口統(tǒng)一返回格式
import lombok.Data;
@Data
public class Result<T> {
    private Integer code; // 狀態(tài)碼:200=成功,500=失敗
    private String msg; // 提示信息
    private T data; // 響應(yīng)數(shù)據(jù)
    // 成功返回
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("success");
        result.setData(data);
        return result;
    }
    // 失敗返回
    public static <T> Result<T> fail(String msg) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMsg(msg);
        return result;
    }
}

2. 寫測(cè)試 Controller

然后寫UserController和OrderController,提供兩個(gè)測(cè)試接口:

// UserController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/user")
public class UserController {
    /**
     * 測(cè)試接口1:獲取用戶信息
     */
    @GetMapping("/get")
    public Result<User> getUser(@RequestParam Long id) {
        // 模擬從數(shù)據(jù)庫查詢用戶信息
        User user = new User();
        user.setId(id);
        user.setName("張三");
        user.setPhone("13800138000"); // 明文手機(jī)號(hào)
        user.setIdCard("110101199001011234"); // 明文身份證號(hào)
        return Result.success(user);
    }
}
// OrderController.java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/api/order")
public class OrderController {
    /**
     * 測(cè)試接口2:獲取訂單列表
     */
    @PostMapping("/list")
    public Result<List<Order>> getOrderList(@RequestBody OrderQuery query) {
        // 模擬從數(shù)據(jù)庫查詢訂單列表
        List<Order> orderList = new ArrayList<>();
        Order order1 = new Order();
        order1.setId(1L);
        order1.setOrderNo("20240520001");
        order1.setBankCard("6222021234567890123"); // 明文銀行卡號(hào)
        User user1 = new User();
        user1.setId(1L);
        user1.setName("李四");
        user1.setPhone("13900139000"); // 嵌套對(duì)象的明文手機(jī)號(hào)
        order1.setUser(user1);
        orderList.add(order1);
        return Result.success(orderList);
    }
    // 訂單查詢參數(shù)
    public static class OrderQuery {
        private Long userId;
        // getter/setter
        public Long getUserId() {
            return userId;
        }
        public void setUserId(Long userId) {
            this.userId = userId;
        }
    }
}

3. 啟動(dòng)項(xiàng)目,測(cè)試效果

把a(bǔ)pplication.yml里的spring.profiles.active改成prod(開啟脫敏),然后啟動(dòng)項(xiàng)目。

用 Postman 或?yàn)g覽器測(cè)試接口:

測(cè)試接口 1:GET /api/user/get?id=1

預(yù)期效果:phone脫敏成1388000,idCard脫敏成110101****1234

實(shí)際返回結(jié)果:

{
    "code": 200,
    "msg": "success",
    "data": {
        "id": 1,
        "name": "張三",
        "phone": "138****8000", // 成功脫敏
        "idCard": "110101********1234" // 成功脫敏
    }
}
測(cè)試接口 2:POST /api/order/list

請(qǐng)求參數(shù):

{
    "userId": 1
}

預(yù)期效果:bankCard脫敏成622202**123,user.phone脫敏成139**9000實(shí)際返回結(jié)果:

{
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "orderNo": "20240520001",
            "bankCard": "622202********123", // 成功脫敏
            "user": {
                "id": 1,
                "name": "李四",
                "phone": "139****9000", // 嵌套字段成功脫敏
                "idCard": null
            }
        }
    ]
}

完美!和預(yù)期效果一致,而且咱們沒改任何業(yè)務(wù)代碼,只改了 YAML 配置,真正實(shí)現(xiàn)了 “零侵入”。

四、進(jìn)階優(yōu)化:讓方案更實(shí)用,應(yīng)對(duì)復(fù)雜場(chǎng)景

上面的方案已經(jīng)能滿足大部分場(chǎng)景了,但在實(shí)際項(xiàng)目中,還有一些細(xì)節(jié)需要優(yōu)化,咱們?cè)傺a(bǔ)充幾個(gè)進(jìn)階功能。

1. 支持動(dòng)態(tài)配置:不用重啟服務(wù),實(shí)時(shí)更新脫敏規(guī)則

上面的方案有個(gè)問題:如果要新增一個(gè)脫敏接口,得改 YAML 配置,然后重啟服務(wù),很麻煩。

解決辦法:用配置中心(比如 Nacos、Apollo)存儲(chǔ)脫敏規(guī)則,實(shí)現(xiàn)動(dòng)態(tài)更新。

以 Nacos 為例,步驟如下:

  • 在 Nacos 里創(chuàng)建一個(gè)配置文件(比如sensitive-demo-prod.yaml),把 YAML 里的sensitive節(jié)點(diǎn)配置放進(jìn)去。
  • 在項(xiàng)目里引入 Nacos 配置依賴:
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    <version>2.2.9.RELEASE</version>
</dependency>
  • 在bootstrap.yml里配置 Nacos 地址:
spring:
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848 # Nacos地址
        file-extension: yaml # 配置文件格式
        group: DEFAULT_GROUP # 配置分組
  application:
    name: sensitive-demo # 服務(wù)名,對(duì)應(yīng)Nacos里的配置文件名前綴
  profiles:
    active: prod # 環(huán)境
  • 在SensitiveProperties里加@RefreshScope注解,支持配置動(dòng)態(tài)刷新:
@Component
@ConfigurationProperties(prefix = "sensitive")
@Data
@RefreshScope // 關(guān)鍵:開啟配置動(dòng)態(tài)刷新
public class SensitiveProperties {
    // ... 原有代碼不變
}
  • 在SensitiveResponseBodyAdvice里監(jiān)聽配置刷新事件,重新初始化緩存:
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
publicclass SensitiveConfigRefreshListener implements ApplicationListener<EnvironmentChangeEvent> {

    @Resource
    private SensitiveResponseBodyAdvice sensitiveResponseBodyAdvice;

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {
        // 如果脫敏配置發(fā)生變化,重新初始化緩存
        if (event.getKeys().stream().anyMatch(key -> key.startsWith("sensitive."))) {
            sensitiveResponseBodyAdvice.init();
        }
    }
}

這樣一來,以后要新增或修改脫敏規(guī)則,直接在 Nacos 里改配置,不用重啟服務(wù),配置會(huì)自動(dòng)刷新,非常方便!

2. 日志脫敏:避免日志里出現(xiàn)明文敏感信息

前面咱們解決了接口響應(yīng)脫敏,但日志里如果打印了敏感信息,還是會(huì)有風(fēng)險(xiǎn)。比如:

log.info("用戶登錄成功,手機(jī)號(hào):{}", user.getPhone()); // 日志里會(huì)出現(xiàn)明文手機(jī)號(hào)

解決辦法:用 Logback 的自定義轉(zhuǎn)換器,對(duì)日志里的敏感字段進(jìn)行脫敏。步驟如下:

  • 寫一個(gè)日志脫敏轉(zhuǎn)換器:
import ch.qos.logback.classic.pattern.ClassicConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * Logback日志脫敏轉(zhuǎn)換器
 */
@Component
publicclass LogSensitiveConverter extends ClassicConverter {

    @Resource
    private SensitiveUtil sensitiveUtil;

    @Override
    publicString convert(ILoggingEvent event) {
        String message = event.getMessage();
        if (message == null) {
            return"";
        }

        // 對(duì)日志里的手機(jī)號(hào)、身份證號(hào)、銀行卡號(hào)進(jìn)行脫敏
        message = sensitiveUtil.desensitize(message, "mobile");
        message = sensitiveUtil.desensitize(message, "idCard");
        message = sensitiveUtil.desensitize(message, "bankCard");

        return message;
    }
}
  • 在logback-spring.xml里配置轉(zhuǎn)換器:
<configuration>
    <!-- 配置脫敏轉(zhuǎn)換器 -->
    <conversionRule conversionWord="sensitive" converterClass="com.example.sensitivedemo.config.LogSensitiveConverter" />

    <!-- 控制臺(tái)輸出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 使用sensitive轉(zhuǎn)換器脫敏日志 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %sensitive%n</pattern>
        </encoder>
    </appender>

    <!-- 全局日志級(jí)別 -->
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

這樣一來,日志里的敏感信息會(huì)自動(dòng)脫敏,比如:

2024-05-20 15:30:00 [http-nio-8080-exec-1] INFO  com.example.sensitivedemo.controller.UserController - 用戶登錄成功,手機(jī)號(hào):138****8000

3. 性能優(yōu)化:減少反射次數(shù),提高接口響應(yīng)速度

前面咱們提到,反射會(huì)影響性能,尤其是高并發(fā)場(chǎng)景。咱們可以通過緩存反射獲取的字段信息,減少反射次數(shù)。

修改SensitiveResponseBodyAdvice里的processSensitiveField方法,增加字段緩存:

/**
 * 字段緩存:key=類名#字段名,value=字段對(duì)象
 */
private final Map<String, Field> fieldCache = new ConcurrentHashMap<>();

/**
 * 獲取類的字段(包括父類的字段),并緩存
 */
private Field getDeclaredField(Class<?> clazz, String fieldName) {
    String cacheKey = clazz.getName() + "#" + fieldName;
    // 先從緩存里拿
    if (fieldCache.containsKey(cacheKey)) {
        return fieldCache.get(cacheKey);
    }

    // 緩存里沒有,遍歷類和父類找字段
    Class<?> currentClazz = clazz;
    while (currentClazz != null && currentClazz != Object.class) {
        try {
            Field field = currentClazz.getDeclaredField(fieldName);
            field.setAccessible(true); // 突破私有字段訪問限制
            fieldCache.put(cacheKey, field); // 緩存字段
            return field;
        } catch (NoSuchFieldException e) {
            currentClazz = currentClazz.getSuperclass(); // 找父類
        }
    }

    returnnull;
}

這樣一來,同一個(gè)類的同一個(gè)字段,只會(huì)反射一次,后續(xù)都從緩存里拿,性能會(huì)提升很多。

五、對(duì)比 AOP:這個(gè)方案到底好在哪里?

最后咱們來總結(jié)一下,這個(gè) SpringBoot + YAML 方案,和傳統(tǒng)的 AOP 方案相比,優(yōu)勢(shì)到底在哪里:

對(duì)比維度

傳統(tǒng) AOP 方案

SpringBoot + YAML 方案

侵入性

強(qiáng):需要加注解、改業(yè)務(wù)代碼、寫切面

零侵入:不用改業(yè)務(wù)代碼,只改配置文件

代碼復(fù)雜度

高:處理嵌套對(duì)象、集合要寫復(fù)雜邏輯

低:基于 ResponseBodyAdvice,邏輯清晰

配置靈活性

差:改規(guī)則要改代碼,重啟服務(wù)

好:配置化,支持動(dòng)態(tài)更新(配 Nacos)

性能

一般:反射次數(shù)多,切面攔截有開銷

好:字段緩存,減少反射,輕量級(jí)攔截

維護(hù)成本

高:代碼耦合度高,后續(xù)修改牽一發(fā)動(dòng)全身

低:配置集中管理,新增接口只加配置

簡單說:用 AOP 做脫敏,就像 “給每個(gè)房間裝一扇門”,每個(gè)門都要單獨(dú)設(shè)計(jì)、安裝;而用這個(gè)方案,就像 “裝一個(gè)智能門禁系統(tǒng)”,統(tǒng)一配置,所有房間都能用,還能隨時(shí)改規(guī)則。

六、總結(jié)

兄弟們,看到這里,相信你已經(jīng)明白這個(gè)零侵入脫敏方案的好處了。不用寫復(fù)雜的 AOP 切面,不用改業(yè)務(wù)代碼,只改改 YAML 配置,就能實(shí)現(xiàn)接口響應(yīng)脫敏,還支持動(dòng)態(tài)配置、日志脫敏、性能優(yōu)化,新手也能快速上手。

如果你現(xiàn)在正在做數(shù)據(jù)脫敏相關(guān)的需求,或者之前用 AOP 踩過坑,不妨試試這個(gè)方案,絕對(duì)能讓你少寫很多代碼,少踩很多坑。

責(zé)任編輯:武曉燕 來源: 石杉的架構(gòu)筆記
相關(guān)推薦

2021-02-03 09:34:28

潮數(shù)

2021-08-02 18:23:01

Spring隱私數(shù)據(jù)

2024-02-05 13:39:00

隱私數(shù)據(jù)脫敏

2023-10-09 07:37:01

2024-09-02 00:27:51

SpringAOP自定義

2025-06-18 02:12:00

2009-12-09 16:47:08

Linux操作系統(tǒng)

2019-03-07 15:45:30

SQL字符串腳本語言

2018-01-26 07:53:46

數(shù)據(jù)脫敏數(shù)據(jù)安全信息安全

2024-02-21 15:30:56

2025-10-20 01:10:00

Spring自動(dòng)化數(shù)據(jù)

2017-02-05 17:27:43

2020-04-10 10:36:20

網(wǎng)絡(luò)通信框架

2020-03-13 14:05:14

SpringBoot+數(shù)據(jù)源Java

2021-10-22 06:53:45

脫敏處理數(shù)據(jù)

2022-05-16 08:50:23

數(shù)據(jù)脫加密器

2025-03-10 00:13:00

數(shù)據(jù)庫脫敏日志脫敏出脫敏

2022-12-14 09:51:04

Twitter開源

2023-07-08 00:12:26

框架結(jié)構(gòu)組件

2025-03-11 08:34:22

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

国产精品久久久久久久久久久久久久久久久 | 亚洲黄色一级大片| 国产精品多人| 亚洲女人初尝黑人巨大| 亚洲欧美aaa| 麻豆网站免费在线观看| 国产欧美视频在线观看| 97在线资源站| 国产精品自拍第一页| 你懂的国产精品| 亚洲片国产一区一级在线观看| 亚洲黄色av片| 久久野战av| 亚洲国产成人av好男人在线观看| 日韩福利在线| 免费观看的毛片| 久久精品99久久久| 欧美夜福利tv在线| 黄色一级视频在线观看| av中文一区| 亚洲精品国偷自产在线99热| 在线观看av免费观看| 亚洲第一影院| 亚洲国产日韩精品| 桥本有菜av在线| 九九热视频在线观看| 成人免费观看视频| 96pao国产成视频永久免费| 亚洲av中文无码乱人伦在线视色| 久草视频免费在线| 熟妇人妻系列aⅴ无码专区友真希 熟妇人妻av无码一区二区三区 | 精品自拍视频| 欧美色播在线播放| 美女扒开大腿让男人桶| 二区在线播放| 国产精品美女久久久久久2018 | 国产无套精品一区二区三区| 欧美一级网址| 欧美色男人天堂| 99久久国产宗和精品1上映 | 国产日韩欧美视频| 天天干天天操天天操| 亚洲一区二区三区高清不卡| 欧美精品video| 欧美日韩激情在线观看| 中文字幕日韩一区二区不卡| 久久精品91久久久久久再现| 992在线观看| 日韩成人激情| 色婷婷综合久久久久| 亚洲一级片在线播放| 国产一区二区三区四区大秀| 亚洲免费视频观看| 男人操女人动态图| 精品freesex老太交| 亚洲欧美综合另类中字| 伊人网伊人影院| 国产毛片一区二区三区| 在线精品播放av| 国产探花视频在线播放| 成人综合专区| 草民午夜欧美限制a级福利片| 三上悠亚在线观看视频| 亚洲激情中文| 欧美激情18p| 国产精品老女人| 欧美亚洲专区| 国产精品国模在线| 91 中文字幕| 国产九色精品成人porny| 91九色蝌蚪嫩草| 人妻精品无码一区二区| 91老师片黄在线观看| 日韩一区免费观看| 国产不卡在线| 午夜激情一区二区三区| 日本xxxxxxx免费视频| 97成人超碰| 日韩免费观看高清完整版在线观看| a级片在线观看视频| 最近国产精品视频| 三级精品视频久久久久| 欧美xxxx精品| 激情视频一区二区三区| 日韩女优在线播放| 精品人妻伦一二三区久久 | bl视频在线免费观看| 欧美性猛交xxxx乱大交3| 天天爽夜夜爽一区二区三区| 中文在线综合| 国产一区二区三区在线| 少妇影院在线观看| 久久精品道一区二区三区| 国产精品三级网站| 人妻一区二区三区免费| 国产丝袜欧美中文另类| 91黄色在线看| 欧美一级二级视频| 亚洲成人精品在线| 自拍偷拍第9页| 一区在线免费| 国产欧美日韩91| 五月色婷婷综合| 国产精品国产精品国产专区不片 | 日韩精品一区二区亚洲av性色| 精品69视频一区二区三区Q| 国产精品视频不卡| 色视频在线观看免费| 亚洲免费av观看| 五月婷婷狠狠操| 欧美大胆a级| 免费97视频在线精品国自产拍| 久久精品无码av| 国产成都精品91一区二区三| 亚洲一区二区三区欧美| 成人av观看| 日韩欧美一区电影| 成人性视频免费看| 久久精品官网| 激情小说综合区| 亚洲淫性视频| 欧美精品自拍偷拍| 久久婷婷五月综合| 国产日韩欧美| 国产高清自拍一区| 国产婷婷视频在线| 欧美日韩亚洲综合| a级片在线观看| 亚洲视频播放| 狠狠色综合一区二区| 黄页网站在线观看免费| 欧美一区二区三区男人的天堂| 国产伦理片在线观看| 欧美中文日韩| 免费在线一区二区| 性欧美18~19sex高清播放| 精品久久国产字幕高潮| 欧美被狂躁喷白浆精品| 国产一区二三区| 欧美少妇一级片| 日韩综合久久| 久久影院资源网| 国产美女明星三级做爰| 亚洲欧美电影一区二区| 国产资源中文字幕| 自拍日韩欧美| 成人在线看片| 波多野结衣精品| 精品1区2区在线观看| 国产小视频在线看| 丁香天五香天堂综合| 男女激情免费视频| 卡通动漫国产精品| 国产91精品久| 福利视频在线导航| 欧美精品精品一区| 欧美在线视频第一页| 国产精品影视网| 欧美一级欧美一级| 国内精品国产成人国产三级粉色 | 成年人午夜视频在线观看| 美国成人xxx| 国产成人中文字幕| 香港伦理在线| 欧美一区二区黄色| 国产精品二区一区二区aⅴ| 99re热这里只有精品视频| 九九九九免费视频| 精品国产91乱码一区二区三区四区 | 亚洲免费不卡| 日本综合精品一区| 8x海外华人永久免费日韩内陆视频 | 久久久天堂国产精品| 91综合久久爱com| 51视频国产精品一区二区| 成人在线免费观看| 日韩三级中文字幕| 欧产日产国产69| 亚洲品质自拍视频网站| 娇妻高潮浓精白浆xxⅹ| 日韩黄色免费电影| 亚洲欧洲日韩精品| 岛国精品一区| 国产精品视频xxx| 9999热视频在线观看| 在线视频欧美日韩| 丰满人妻妇伦又伦精品国产| 欧美视频第一页| 51精品免费网站| 91视频国产资源| 中文字幕avav| 日韩福利视频导航| 国产美女永久无遮挡| 国产精品羞羞答答在线观看| 91免费观看网站| 中文字幕在线免费观看视频| 久久亚洲精品一区二区| 欧洲成人av| 日韩精品一区二区三区中文精品| 不卡av电影在线| 亚洲午夜在线电影| 日韩欧美在线视频播放| 91麻豆精品秘密| 性折磨bdsm欧美激情另类| 欧美aaaaaa午夜精品| 超碰成人免费在线| 91精品1区| 亚洲啪啪av| 日韩福利视频一区| wwwxx欧美| 成人在线啊v| 国产精品自拍偷拍| 午夜精品成人av| 97视频国产在线| 欧美xxxx做受欧美88bbw| 最新91在线视频| 国产原创av在线| 日韩风俗一区 二区| www.97av| 91麻豆精品国产91久久久使用方法 | 日本美女一区二区三区视频| 欧美色图色综合| 影音先锋久久资源网| 国产精品12p| 久久久久久免费视频| 日韩一区不卡| 欧美色就是色| 欧美一二三四五区| 九九综合九九| 欧美一区二区在线| 亚洲另类av| 裸体丰满少妇做受久久99精品| 激情小说亚洲色图| 国产aⅴ精品一区二区三区黄| 精品一区二区三区中文字幕在线 | 国产精品 欧美在线| 中老年在线免费视频| 97免费在线视频| 91福利在线尤物| 97婷婷涩涩精品一区| 精精国产xxx在线视频app| 久久久中文字幕| 国产精品一二三产区| 18性欧美xxxⅹ性满足| 韩国成人二区| 午夜精品美女自拍福到在线| 91禁在线看| 欧美最近摘花xxxx摘花| 六月婷婷综合| 国产精品久久一区| 日韩av黄色| 亚洲在线一区二区| 亚洲日本视频在线| 好吊色欧美一区二区三区 | 日韩小视频在线观看| 久操视频在线播放| 欧美另类极品videosbest最新版本 | 1024精品视频| 视频一区二区欧美| 日本不卡一区在线| 国产一区视频在线看| 蜜桃视频无码区在线观看| 国产**成人网毛片九色 | 一区二区在线免费看| 国产一区二区0| 亚洲成年人在线观看| 久久免费精品国产久精品久久久久| 国产毛片久久久久久久| 欧美韩日一区二区三区四区| 糖心vlog免费在线观看| 亚洲成人激情av| 精品久久久久久久久久久国产字幕| 欧美四级电影网| www男人的天堂| 日韩电影第一页| 最新电影电视剧在线观看免费观看| 日韩一区在线视频| 123区在线| 国产精品美女久久久久久免费| 91嫩草国产线观看亚洲一区二区| 99理论电影网| 欧美激情在线精品一区二区三区| 亚洲欧美综合一区| 在线欧美不卡| 天天干天天玩天天操| 成人午夜免费电影| 中文字幕伦理片| 亚洲人妖av一区二区| 可以在线观看av的网站| 欧美欧美午夜aⅴ在线观看| 亚洲欧美黄色片| 日韩在线观看免费| 国产传媒在线| 成人在线观看视频网站| 自拍亚洲一区| 大西瓜av在线| 九一九一国产精品| 国产精品久久无码| 亚洲欧美偷拍三级| 日韩精选在线观看| 日韩精品一区二区三区视频| wwwxxx在线观看| 91av在线免费观看视频| 豆花视频一区| 亚洲国产日韩综合一区| 国产精品入口| 色哟哟在线观看视频| 国产精品女主播在线观看| 日韩精品一卡二卡| 日韩美女视频在线| 男人天堂手机在线| 国产精品久久久久福利| 噜噜噜天天躁狠狠躁夜夜精品 | 肉肉视频在线观看| 成人黄色网免费| 国产欧美日韩影院| 欧美女人性生活视频| 粉嫩嫩av羞羞动漫久久久 | 性色av蜜臀av浪潮av老女人| 中文字幕一区在线观看视频| 欧美一级淫片免费视频黄| 亚洲国产黄色片| 黄色的视频在线观看| 99精品国产高清一区二区| 图片区亚洲欧美小说区| 久久婷婷国产91天堂综合精品| 91在线精品秘密一区二区| 五月婷婷激情网| 亚洲精品xxxx| 色多多在线观看| 精品视频第一区| 狠狠综合久久| 亚洲av午夜精品一区二区三区| 亚洲久本草在线中文字幕| 国产精品久久影视| 日韩中文字幕在线| 国产亚洲精品精品国产亚洲综合| 欧美一级爱爱| 日韩成人免费在线| 国产7777777| 欧美精品高清视频| 直接在线观看的三级网址| 亚洲专区中文字幕| 欧美黄污视频| 91传媒理伦片在线观看| 五月激情综合网| 欧美人体大胆444www| 国产第一区电影| 青青草成人影院| 超碰成人在线播放| 亚洲欧美在线另类| 精品黑人一区二区三区在线观看| 欧美成人高清视频| 97一区二区国产好的精华液| 国产v片免费观看| 久久伊人蜜桃av一区二区| 蜜臀99久久精品久久久久小说 | 免费一级黄色录像| 欧美日韩国产美| 中文av资源在线| 国产一区二区自拍| 久久久精品五月天| 国产在线免费av| 欧美一二三区精品| h片在线观看视频免费| 欧美日韩在线精品一区二区三区| 久久高清国产| 国产麻豆a毛片| 精品久久一区二区| 北岛玲heyzo一区二区| 这里只有精品66| 不卡区在线中文字幕| 日本一本在线观看| 九九精品视频在线观看| 欧美性生活一级片| 亚洲综合婷婷久久| 亚洲成人一区在线| 国产永久免费高清在线观看视频| 成人有码在线视频| 新狼窝色av性久久久久久| 青青青手机在线视频| 亚洲精品久久久久久下一站 | 日韩欧美电影一二三| 欧美日韩国产观看视频| 天天人人精品| 大美女一区二区三区| 进去里视频在线观看| 欧美老少做受xxxx高潮| 国产传媒欧美日韩成人精品大片| 天天爽夜夜爽视频| 色素色在线综合| 午夜在线激情影院| 日韩福利在线| www.成人在线| 国产普通话bbwbbwbbw| 57pao精品| 国产精品v欧美精品v日本精品动漫| 中字幕一区二区三区乱码| 精品成人a区在线观看| 久久er热在这里只有精品66|