日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

深入字节码操作:使用ASM和Javassist创建审核日志

發布時間:2025/3/21 java 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入字节码操作:使用ASM和Javassist创建审核日志 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

深入字節碼操作:使用ASM和Javassist創建審核日志

原文鏈接:https://blog.newrelic.com/2014/09/29/diving-bytecode-manipulation-creating-audit-log-asm-javassist/

在堆棧中使用spring和hibernate,您的應用程序的字節碼可能會在運行時被增強或處理。 字節碼是Java虛擬機(JVM)的指令集,所有在JVM上運行的語言都必須最終編譯為字節碼。 操作字節碼原因如下:

  • 程序分析:?
    • 查找應用bug
    • 檢查代碼復雜性
    • 查找特定注解的類
  • 類生成:?
    • 使用代理從數據庫中懶惰加載數據
  • 安全性?
    • 特定API限制訪問權限
    • 代碼混淆
  • 無Java源碼類轉換?
    • 代碼分析
    • 代碼優化
  • 最后,添加日志

有幾種可用于操作字節碼的工具,從非常低級的工具(如需要字節碼級別工作的ASM)到諸如AspectJ等高級框架(允許編寫純Java)。

本博文,我將演示分別使用Javassist和ASM實現一種審計日志的方法。

審計日志例子

假定我沒有如下代碼:

public class BankTransactions {public static void main(String[] args) {BankTransactions bank = new BankTransactions();for (int i = 0; i < 100; i++) {String accountId = "account" + i;bank.login("password", accountId, "Ashley");bank.unimportantProcessing(accountId);bank.withdraw(accountId, Double.valueOf(i));}System.out.println("Transactions completed");} }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

我們要記錄重要的操作以及關鍵信息以確定操作。 以上,我將確定登錄退出的重要動作。 對于登錄,重要信息將是帳戶ID和用戶。 對于退出,重要信息將是帳戶ID和撤回的金額。 記錄重要操作的一種方法是將日志記錄語句添加到每個重要的方法,但這將是乏味的。 相反,我們可以為重要的方法添加注釋,然后使用工具來注入日志記錄。 在這種情況下,該工具將是一個字節碼操作框架。

@ImportantLog(fields = { "1", "2" }) public void login(String password, String accountId, String userName) {// login logic } @ImportantLog(fields = { "0", "1" }) public void withdraw(String accountId, Double moneyToRemove) {// transaction logic }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

@ImportantLog注釋表示我們要在每次調用該方法時記錄一條消息,而@ImportantLog注釋中的fields參數表示應記錄的每個參數的索引位置。 例如,對于登錄,我們要記錄第1位和第2位的輸入參數。它們是accountIduserName。 我們不會記錄第0位的密碼參數。

使用字節碼和注釋來執行日志記錄有兩個主要優點:

  • 日志記錄與業務邏輯分離,這有助于保持代碼清潔和簡單。
  • 在不修改源代碼的情況下,輕松刪除審核日志記錄。
  • 在哪里實際修改字節碼?

    我們可以使用1.5中引入的核心Java功能來操縱字節碼。 此功能稱為Java代理。?
    要了解Java代理,讓我們來看一下典型的Java處理流程。

    使用包含我們的main方法的類作為輸入參數執行命令java。 這將啟動Java運行時環境,使用classloader來加載輸入類,并調用該類的main方法。 在我們具體的例子中,調用了BankTransactions的main方法,這將導致一些處理發生,并打印“完成交易”。

    現在來看一下使用Java代理的Java進程。

    命令java運行兩個輸入參數。第一個是JVM參數-javaagent,指向代理jar。第二個是包含我們主要方法的類。javaagent標志告訴JVM首先加載代理。 代理的主類必須在代理jar的清單中指定。 一旦類被加載,類的premain方法被調用。 這個premain方法充當代理的安裝鉤子。 它允許代理注冊一個類變換器。 當類變換器在JVM中注冊時,該變換器將在類加載到JVM前接收每個類的字節。 這為類變換器提供了根據需要修改類的字節的機會。 一旦類變換器修改了字節,它將修改的字節返回給JVM。 這些字節接著由JVM驗證和加載。

    在我們具體的例子中,當BankTransaction加載時,字節將首先進入類變換器進行潛在的修改。修改后的字節將被返回并加載到JVM中。 加載完之后,調用類中的main方法,進行一些處理,并打印“事務完成”。

    讓我們來看看代碼。 下面我有代理的premain方法:

    public class JavassistAgent {public static void premain(String agentArgs, Instrumentation inst) {System.out.println("Starting the agent");inst.addTransformer(new ImportantLogClassTransformer());} }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    premain方法打印出一個消息,然后注冊一個類變換器。 類變換器必須實現方法轉換,加載到JVM中的每個類都會調用它。它以該類的字節數組作為方法的輸入,然后返回修改后的字節數組。如果類變換器決定不修改特定類的字節,則可以返回null。

    public class ImportantLogClassTransformer implements ClassFileTransformer {public byte[] transform(ClassLoader loader, String className,Class classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {// manipulate the bytes herereturn modified bytes;} }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    現在我們知道在哪里修改一個類的字節,接著需要知道如何修改字節。

    如何使用Javassist修改字節碼?

    Javassist是一個具有高級和低級API的字節碼操作框架。我將重點關注高級的面向對象的API,首先從Javassist中的對象的解釋開始。接下來,我將實現審核日志應用程序的實際代碼。

    Javassist使用CtClass對象來表示一個類。 這些CtClass對象可以從ClassPool獲得,用于修改Classes。ClassPool是一個基于HashMap實現的CtClass對象容器,其中鍵是類名稱,值是表示該類的CtClass對象。默認的ClassPool使用與底層JVM相同的類路徑。因此,在某些情況下,可能需要向ClassPool添加類路徑或類字節。

    類似于包含字段,方法和構造函數的Java類,CtClass對象包含CtFieldsCtConstructorsCtMethods。所有這些對象都可以修改。我將重點關注方法操作,因為審核日志應用程序需要這種行為。

    以下是修改方法的幾種方法:

    上圖顯示了Javassist的主要優點之一。實際上不必寫字節碼。而是編寫Java代碼。一個復雜的情況是Java代碼必須在引號內。

    現在我們了解了Javassist的基本構建塊,現在來看看應用程序的實際代碼。 類變換器的變換方法需要執行以下步驟:

  • 將字節數組轉換為CtClass對象
  • 檢查CtClass中每個帶注解@ImportantLog的方法
  • 如果方法中存在@ImportantLog注釋,那么:?
    • 獲取方法重要參數索引
    • 函數開始增加日志語句
  • 使用Javassist編寫Java代碼時,請注意以下問題:

    • JVM在包之間使用斜杠,而Javassist使用點。
    • 當插入多行Java代碼時,代碼需要在括號內。
    • 當使用12等引用方法參數值時,知道0this1。
    • 注釋擁有可見和不可見的簽。 不可見的注釋在運行時無法獲取。

    實際的Java代碼如下:

    public class ImportantLogClassTransformer implements ClassFileTransformer {private static final String METHOD_ANNOTATION = "com.example.spring2gx.mains.ImportantLog";private static final String ANNOTATION_ARRAY = "fields";private ClassPool pool;public ImportantLogClassTransformer() {pool = ClassPool.getDefault();}public byte[] transform(ClassLoader loader, String className,Class classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {try {pool.insertClassPath(new ByteArrayClassPath(className,classfileBuffer));CtClass cclass = pool.get(className.replaceAll("/", "."));if (!cclass.isFrozen()) {for (CtMethod currentMethod : cclass.getDeclaredMethods()) {Annotation annotation = getAnnotation(currentMethod);if (annotation != null) {List parameterIndexes = getParamIndexes(annotation);currentMethod.insertBefore(createJavaString(currentMethod, className, parameterIndexes));}}return cclass.toBytecode();}} catch (Exception e) {e.printStackTrace();}return null;}private Annotation getAnnotation(CtMethod method) {MethodInfo mInfo = method.getMethodInfo();// the attribute we are looking for is a runtime invisible attribute// use Retention(RetentionPolicy.RUNTIME) on the annotation to make it// visible at runtimeAnnotationsAttribute attInfo = (AnnotationsAttribute) mInfo.getAttribute(AnnotationsAttribute.invisibleTag);if (attInfo != null) {// this is the type name meaning use dots instead of slashesreturn attInfo.getAnnotation(METHOD_ANNOTATION);}return null;}private List getParamIndexes(Annotation annotation) {ArrayMemberValue fields = (ArrayMemberValue) annotation.getMemberValue(ANNOTATION_ARRAY);if (fields != null) {MemberValue[] values = (MemberValue[]) fields.getValue();List parameterIndexes = new ArrayList();for (MemberValue val : values) {parameterIndexes.add(((StringMemberValue) val).getValue());}return parameterIndexes;}return Collections.emptyList();}private String createJavaString(CtMethod currentMethod, String className,List indexParameters) {StringBuilder sb = new StringBuilder();sb.append("{StringBuilder sb = new StringBuilder");sb.append("(\"A call was made to method '\");");sb.append("sb.append(\"");sb.append(currentMethod.getName());sb.append("\");sb.append(\"' on class '\");");sb.append("sb.append(\"");sb.append(className);sb.append("\");sb.append(\"'.\");");sb.append("sb.append(\"\\n Important params:\");");for (String index : indexParameters) {try {// add one because 0 is "this" for instance variable// if were a static method 0 would not be anythingint localVar = Integer.parseInt(index) + 1;sb.append("sb.append(\"\\n Index \");");sb.append("sb.append(\"");sb.append(index);sb.append("\");sb.append(\" value: \");");sb.append("sb.append($" + localVar + ");");} catch (NumberFormatException e) {e.printStackTrace();}}sb.append("System.out.println(sb.toString());}");return sb.toString();} }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    完成了!我們可以運行應用程序,并將日志記錄輸出到“System.out”。

    積極的一面是寫入的代碼量非常小,而且實際上不需要使用Javassist編寫字節碼。 最大的缺點是用引號編寫Java代碼可能會變得乏味。幸運的是,其他一些字節碼操作框架更快。我們來看看其中一個更快的框架。

    如何使用ASM修改字節?

    ASM是一個字節碼操作框架,使用較小的內存占用并且速度相對較快。我認為ASM是字節碼操作的行業標準,即使是Javassist也在使用ASM。ASM提供基于對象和事件的庫,但在這里我將重點介紹基于事件的模型。

    要理解ASM,我將從ASM自己的文檔的一個Java類圖(下圖)開始。它表明Java類由幾個部分組成,包括一個超類,接口,注釋,字段和方法。在ASM基于事件的模型中,所有這些類組件都可以被認為是事件。

    可以在ClassVisitor上找到ASM的類事件。為了“看到”這些事件,必須創建一個classVisitor來覆蓋您想要查看的所需組件。

    除了類訪問者,我們需要一些東西來解析類并生成事件。為此,ASM提供了一個名為ClassReader的對象。reader解析課程并產生事件。類被解析后,需要ClassWriter來消耗事件,將它們轉換成一個類字節數組。在下圖中,我們BankTransactions類的字節傳遞給ClassReader,該字節將字節發送到ClassWriter,該ClassWriter會輸出生成的BankTransaction。當沒有ClassVisitor存在時,輸入BankTransactions字節應基本上匹配其輸出字節。

    public byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {ClassReader cr = new ClassReader(classfileBuffer);ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);cr.accept(cw, 0);return cw.toByteArray(); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    ClassReader得到類的字節,ClassWriter從類讀取器獲取。ClassReader的accept調用解析該類。接下來,我們從ClassWriter訪問生成的字節。

    現在我們想修改BankTransaction字節。首先,我們需要鏈接在ClassVisitor中。 此ClassVisitor將覆蓋諸如visitField或visitMethod之類的方法來接收關于該特定類組件的通知。

    以下是上圖的代碼實現。 類訪問者LogMethodClassVisitor已添加。請注意,可以添加多個類訪問者。

    public byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined, ProtectionDomain protectionDomain,byte[] classfileBuffer) throws IllegalClassFormatException {ClassReader cr = new ClassReader(classfileBuffer);ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);ClassVisitor cv = new LogMethodClassVisitor(cw, className);cr.accept(cv, 0);return cw.toByteArray(); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    對于審核日志應用,我們需要檢查類中的每個方法。這意味著ClassVisitor只需要覆蓋’visitMethod’。

    public class LogMethodClassVisitor extends ClassVisitor {private String className;public LogMethodClassVisitor(ClassVisitor cv, String pClassName) {super(Opcodes.ASM5, cv);className = pClassName;}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) {//put logic in here} }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    請注意,visitMethod返回一個MethodVisitor。 就像一個類有多個組件,一個方法也有很多的組件,當解析該方法時,它可以被認為是事件。

    MethodVisitor在方法上提供事件。對于審核日志應用,我們要檢查帶注釋的方法上。基于注釋,我們可能需要修改方法中的實際代碼。為了進行這些修改,我們需要在一個methodVisitor鏈接,如下所示。

    @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature,exceptions);return new PrintMessageMethodVisitor(mv, name, className); }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    這個PrintMessageMethodVisitor將需要覆蓋visitAnnotation和visitCode。請注意,visitAnnotation返回一個AnnotationVisitor。就像類和方法具有組件一樣,還有一個注釋的多個組件。AnnotationVisitor允許我們訪問注釋的所有部分。

    下面我簡要介紹了visitAnnotation和visitCode的步驟。

    public class PrintMessageMethodVisitor extends MethodVisitor {@Overridepublic AnnotationVisitor visitAnnotation(String desc, boolean visible) {// 1. check method for annotation @ImportantLog// 2. if annotation present, then get important method param indexes}@Overridepublic void visitCode() {// 3. if annotation present, add logging to beginning of the method} }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    當使用ASM編寫Java代碼時,請注意以下問題:

    • 在事件模型中,類或方法的事件將始終以特定順序發生。 例如,帶注解的方法將始終在實際代碼之前訪問。
    • 當使用12等引用方法參數值時,知道0this1。

    實際Java代碼如下:

    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {if ("Lcom/example/spring2gx/mains/ImportantLog;".equals(desc)) {isAnnotationPresent = true;return new AnnotationVisitor(Opcodes.ASM5,super.visitAnnotation(desc, visible)) {public AnnotationVisitor visitArray(String name, Object value) {if ("fields".equals(name)) {return new AnnotationVisitor(Opscodes.ASM5,super.visitArray(name)) { public void visit(String name, Object value) {parameterIndexes.add((String) value);super.visit(name, value);}};} else {return super.visitArray(name);}}};}return super.visitAnnotation(desc, visible); } public void visitCode() {if (isAnnotationPresent) {// create string buildermv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out","Ljava/io/PrintStream;");mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");mv.visitInsn(Opcodes.DUP);// add everything to the string buildermv.visitLdcInsn("A call was made to method \"");mv.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/StringBuilder", "","(Ljava/lang/String;)V", false);mv.visitLdcInsn(methodName);mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/lang/StringBuilder", "append","(Ljava/lang/String;)Ljava/lang/StringBuilder;", false); . . .
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    以上可以看出Javassist和ASM之間的主要區別之一。使用ASM,必須在修改方法時在字節碼級別編寫代碼,這意味著需要很好地了解JVM的工作原理。需要在給定的時刻確切知道堆棧和局部變量的內容。 在字節碼級別的編寫方面,在功能和優化方面提高了門檻,這意味著開發人員需要較長的時間熟悉ASM開發。

    家庭作業

    現在你已經看到如何使用ASM和Javassist的一個場景,我鼓勵你嘗試一個字節碼操作框架。字節碼操作不僅可以讓您更好地了解JVM,而且還有無數的應用程序。一旦開始,你會發現天空的極限。

    from:?http://blog.csdn.net/lihenair/article/details/69948918

    《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀

    總結

    以上是生活随笔為你收集整理的深入字节码操作:使用ASM和Javassist创建审核日志的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 在线视频一区二区三区 | 日韩久久不卡 | 亚洲狼人社区 | 天堂精品视频 | 又污又黄又爽的网站 | 最近中文字幕在线mv视频在线 | 国产性一乱一性一伧一色 | 成人黄色网页 | 黄色a毛片| 有码中文 | 午夜精品久久久久久久四虎美女版 | 99资源在线 | 国产午夜视频在线 | 国产三级精品三级在线 | 精品免费囯产一区二区三区 | 国产精品第5页 | 国产ts在线观看 | 69日影院 | 亚洲中文字幕第一区 | 人妻换人妻a片爽麻豆 | av在线资源播放 | 美女又爽又黄 | 成人精品动漫 | 夜夜操导航 | 亚洲精品欧美在线 | 奇米888一区二区三区 | 亚洲综合影视 | 最新视频 - 8mav | 中文字幕精品三级久久久 | 小蝌蚪视频色 | 性人久久久久 | 国产精品传媒一区二区 | 国产91成人 | 人人超碰人人 | 亚洲小说在线 | 欧美交| 香港三日本三级少妇66 | 夜夜撸小说 | 日韩精品理论 | 久久精品夜色噜噜亚洲a∨ 中文字幕av网 | 禁网站在线观看免费视频 | 国产精品国色综合久久 | 日韩欧美在线观看一区二区三区 | 女同av在线播放 | 一区二区三区国产av | 蝌蚪网在线视频 | 国产精品毛片一区二区 | 蜜桃91丨九色丨蝌蚪91桃色 | av免费看网站 | 精品国产a | 国产无遮挡免费观看视频网站 | 女生抠逼视频 | 免费看黄色小视频 | 欧美日韩一区二区在线播放 | 免费在线看污 | 亚洲欧美婷婷 | 在线免费观看黄网站 | 男生把女生困困的视频 | 91免费网站在线观看 | 一区二区三区四区欧美 | 黑人性生活视频 | 日韩欧美不卡在线 | 艳妇av | 欧美黑人又粗又大的性格特点 | 97久久国产 | 日本一二三不卡 | 中文字幕 欧美 日韩 | 免费毛片视频 | 欧美综合图区 | www国产| 亚洲欧美日韩国产 | 在线观看免费视频a | 久久久国产片 | 在线色网 | 伊人色网| 日韩视频在线观看一区二区 | 亚洲不卡视频 | 国产人澡人澡澡澡人碰视频 | 奇米777色 | 国产无套免费网站69 | 99热免费观看 | 伊人综合影院 | 亚洲欧美视频一区二区 | 国产免费成人 | 综合视频一区二区 | 国产婷婷色一区二区三区在线 | 成人在线免费观看网址 | 久国产精品 | 女女综合网 | 在线免费观看成人 | 少妇又紧又爽视频 | 欧美日韩一区电影 | 亚洲黄色一区二区三区 | 福利所第一导航 | 国产精品一页 | 综合激情网站 | 台湾色综合 | www.亚洲一区二区 | 污免费视频 |