亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 開發 > JS > 正文

詳解使用JWT實現單點登錄(完全跨域方案)

2024-05-06 16:53:57
字體:
來源:轉載
供稿:網友

首先介紹一下什么是JSON Web Token(JWT)?

官方文檔是這樣解釋的:JSON Web Token(JWT)是一個開放標準(RFC 7519),它定義了一種緊湊且獨立的方式,可以在各方之間作為JSON對象安全地傳輸信息。此信息可以通過數字簽名進行驗證和信任。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公鑰/私鑰對進行簽名。

雖然JWT可以加密以在各方之間提供保密,但只將專注于簽名令牌。簽名令牌可以驗證其中包含的聲明的完整性,而加密令牌則隱藏其他方的聲明。當使用公鑰/私鑰對簽署令牌時,簽名還證明只有持有私鑰的一方是簽署私鑰的一方。

通俗來講,JWT是一個含簽名并攜帶用戶相關信息的加密串,頁面請求校驗登錄接口時,請求頭中攜帶JWT串到后端服務,后端通過簽名加密串匹配校驗,保證信息未被篡改。校驗通過則認為是可靠的請求,將正常返回數據。

什么情況下使用JWT比較適合?

  • 授權:這是最常見的使用場景,解決單點登錄問題。因為JWT使用起來輕便,開銷小,服務端不用記錄用戶狀態信息(無狀態),所以使用比較廣泛;
  • 信息交換:JWT是在各個服務之間安全傳輸信息的好方法。因為JWT可以簽名,例如,使用公鑰/私鑰對兒 - 可以確定請求方是合法的。此外,由于使用標頭和有效負載計算簽名,還可以驗證內容是否未被篡改。

JWT的結構體是什么樣的?

JWT由三部分組成,分別是頭信息、有效載荷、簽名,中間以(.)分隔,如下格式:

xxx.yyy.zzz

header(頭信息)

由兩部分組成,令牌類型(即:JWT)、散列算法(HMAC、RSASSA、RSASSA-PSS等),例如:

{ "alg": "HS256", "typ": "JWT"}

然后,這個JSON被編碼為Base64Url,形成JWT的第一部分。

Payload(有效載荷)

JWT的第二部分是payload,其中包含claims。claims是關于實體(常用的是用戶信息)和其他數據的聲明,claims有三種類型: registered, public, and private claims。

Registered claims: 這些是一組預定義的claims,非強制性的,但是推薦使用, iss(發行人), exp(到期時間), sub(主題), aud(觀眾)等;

Public claims: 自定義claims,注意不要和JWT注冊表中屬性沖突,這里可以查看JWT注冊表

Private claims: 這些是自定義的claims,用于在同意使用這些claims的各方之間共享信息,它們既不是Registered claims,也不是Public claims。

以下是payload示例:

{ "sub": "1234567890", "name": "John Doe", "admin": true}

然后,再經過Base64Url編碼,形成JWT的第二部分;

注意:對于簽名令牌,此信息雖然可以防止篡改,但任何人都可以讀取。除非加密,否則不要將敏感信息放入到Payload或Header元素中。

Signature

要創建簽名部分,必須采用編碼的Header,編碼的Payload,秘鑰,Header中指定的算法,并對其進行簽名。

例如,如果要使用HMAC SHA256算法,將按以下方式創建簽名:

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

簽名用于驗證消息在此過程中未被篡改,并且,在使用私鑰簽名令牌的情況下,它還可以驗證JWT的請求方是否是它所聲明的請求方。

輸出是三個由點分隔的Base64-URL字符串,可以在HTML和HTTP環境中輕松傳遞,與SAML等基于XML的標準相比更加緊湊。
例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

JWT工作機制?

在身份驗證中,當用戶使用其憑據成功登錄時,將返回JSON Web Token(即:JWT)。由于令牌是憑證,因此必須非常小心以防止出現安全問題。一般情況下,不應將令牌保留的時間超過要求。理論上超時時間越短越好。

每當用戶想要訪問受保護的路由或資源時,用戶代理應該使用Bearer模式發送JWT,通常在Authorization header中。標題內容應如下所示:

Authorization: Bearer <token>

在某些情況下,這可以作為無狀態授權機制。服務器的受保護路由將檢查Authorization header中的有效JWT ,如果有效,則允許用戶訪問受保護資源。如果JWT包含必要的數據,則可以減少查詢數據庫或緩存信息。
如果在Authorization header中發送令牌,則跨域資源共享(CORS)將不會成為問題,因為它不使用cookie。

注意:使用簽名令牌,雖然他們無法更改,但是令牌中包含的所有信息都會向用戶或其他方公開。這意味著不應該在令牌中放置敏感信息。

使用JWT的好處是什么?

相比Simple Web Tokens (SWT)(簡單Web令牌) and Security Assertion Markup Language Tokens (SAML)(安全斷言標記語言令牌);

  • JWT比SAML更簡潔,在HTML和HTTP環境中傳遞更方便;
  • 在安全方面,SWT只能使用HMAC算法通過共享密鑰對稱簽名。但是,JWT和SAML令牌可以使用X.509證書形式的公鑰/私鑰對進行簽名。與簽名JSON的簡單性相比,使用XML數字簽名可能會存在安全漏洞;
  • JSON解析成對象相比XML更流行、方便。

以下是我實際項目中的應用分析

首先看一下大致的架構及流程圖:

JWT,單點登錄

JWT,單點登錄

主要有以下三步:

項目一開始我先封裝了一個JWTHelper工具包(GitHub下載),主要提供了生成JWT、解析JWT以及校驗JWT的方法,其他還有一些加密相關操作,稍后我會以代碼的形式介紹下代碼。工具包寫好后我將打包上傳到私服,能夠隨時依賴下載使用;

接下來,我在客戶端項目中依賴JWTHelper工具包,并添加Interceptor攔截器,攔截需要校驗登錄的接口。攔截器中校驗JWT有效性,并在response中重新設置JWT的新值;

最后在JWT服務端,依賴JWT工具包,在登錄方法中,需要在登錄校驗成功后調用生成JWT方法,生成一個JWT令牌并且設置到response的header中。

以下是部分代碼分享:

JwtHelper工具類:

/** * @Author: Helon * @Description: JWT工具類 * 參考官網:https://jwt.io/ * JWT的數據結構為:A.B.C三部分數據,由字符點"."分割成三部分數據 * A-header頭信息 * B-payload 有效負荷 一般包括:已注冊信息(registered claims),公開數據(public claims),私有數據(private claims) * C-signature 簽名信息 是將header和payload進行加密生成的 * @Data: Created in 2018/7/19 14:11 * @Modified By: */public class JwtHelper {  private static Logger logger = LoggerFactory.getLogger(JwtHelper.class);  /**   * @Author: Helon   * @Description: 生成JWT字符串   * 格式:A.B.C   * A-header頭信息   * B-payload 有效負荷   * C-signature 簽名信息 是將header和payload進行加密生成的   * @param userId - 用戶編號   * @param userName - 用戶名   * @param identities - 客戶端信息(變長參數),目前包含瀏覽器信息,用于客戶端攔截器校驗,防止跨域非法訪問   * @Data: 2018/7/28 19:26   * @Modified By:   */  public static String generateJWT(String userId, String userName, String ...identities) {    //簽名算法,選擇SHA-256    SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;    //獲取當前系統時間    long nowTimeMillis = System.currentTimeMillis();    Date now = new Date(nowTimeMillis);    //將BASE64SECRET常量字符串使用base64解碼成字節數組    byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET);    //使用HmacSHA256簽名算法生成一個HS256的簽名秘鑰Key    Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());    //添加構成JWT的參數    Map<String, Object> headMap = new HashMap<>();    /*      Header      {       "alg": "HS256",       "typ": "JWT"      }     */    headMap.put("alg", SignatureAlgorithm.HS256.getValue());    headMap.put("typ", "JWT");    JwtBuilder builder = Jwts.builder().setHeader(headMap)        /*          Payload          {           "userId": "1234567890",           "userName": "John Doe",          }         */        //加密后的客戶編號        .claim("userId", AESSecretUtil.encryptToStr(userId, SecretConstant.DATAKEY))        //客戶名稱        .claim("userName", userName)        //客戶端瀏覽器信息        .claim("userAgent", identities[0])        //Signature        .signWith(signatureAlgorithm, signingKey);    //添加Token過期時間    if (SecretConstant.EXPIRESSECOND >= 0) {      long expMillis = nowTimeMillis + SecretConstant.EXPIRESSECOND;      Date expDate = new Date(expMillis);      builder.setExpiration(expDate).setNotBefore(now);    }    return builder.compact();  }  /**   * @Author: Helon   * @Description: 解析JWT   * 返回Claims對象   * @param jsonWebToken - JWT   * @Data: 2018/7/28 19:25   * @Modified By:   */  public static Claims parseJWT(String jsonWebToken) {    Claims claims = null;    try {      if (StringUtils.isNotBlank(jsonWebToken)) {        //解析jwt        claims = Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(SecretConstant.BASE64SECRET))            .parseClaimsJws(jsonWebToken).getBody();      }else {        logger.warn("[JWTHelper]-json web token 為空");      }    } catch (Exception e) {      logger.error("[JWTHelper]-JWT解析異常:可能因為token已經超時或非法token");    }    return claims;  }  /**   * @Author: Helon   * @Description: 校驗JWT是否有效   * 返回json字符串的demo:   * {"freshToken":"A.B.C","userName":"Judy","userId":"123", "userAgent":"xxxx"}   * freshToken-刷新后的jwt   * userName-客戶名稱   * userId-客戶編號   * userAgent-客戶端瀏覽器信息   * @param jsonWebToken - JWT   * @Data: 2018/7/24 15:28   * @Modified By:   */  public static String validateLogin(String jsonWebToken) {    Map<String, Object> retMap = null;    Claims claims = parseJWT(jsonWebToken);    if (claims != null) {      //解密客戶編號      String decryptUserId = AESSecretUtil.decryptToStr((String)claims.get("userId"), SecretConstant.DATAKEY);      retMap = new HashMap<>();      //加密后的客戶編號      retMap.put("userId", decryptUserId);      //客戶名稱      retMap.put("userName", claims.get("userName"));      //客戶端瀏覽器信息      retMap.put("userAgent", claims.get("userAgent"));      //刷新JWT      retMap.put("freshToken", generateJWT(decryptUserId, (String)claims.get("userName"), (String)claims.get("userAgent"), (String)claims.get("domainName")));    }else {      logger.warn("[JWTHelper]-JWT解析出claims為空");    }    return retMap!=null?JSONObject.toJSONString(retMap):null;  }  public static void main(String[] args) {    String jsonWebKey = generateJWT("123", "Judy",        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36");    System.out.println(jsonWebKey);    Claims claims = parseJWT(jsonWebKey);    System.out.println(claims);    System.out.println(validateLogin(jsonWebKey));  }

AES加密工具類:

/** * @Author: Helon * @Description: AES加密工具類 * @Data: Created in 2018/7/28 18:38 * @Modified By: */public class AESSecretUtil {  /**秘鑰的大小*/  private static final int KEYSIZE = 128;    /**   * @Author: Helon   * @Description: AES加密   * @param data - 待加密內容   * @param key - 加密秘鑰   * @Data: 2018/7/28 18:42   * @Modified By:   */  public static byte[] encrypt(String data, String key) {    if(StringUtils.isNotBlank(data)){      try {        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");        //選擇一種固定算法,為了避免不同java實現的不同算法,生成不同的密鑰,而導致解密失敗        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");        random.setSeed(key.getBytes());        keyGenerator.init(KEYSIZE, random);        SecretKey secretKey = keyGenerator.generateKey();        byte[] enCodeFormat = secretKey.getEncoded();        SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");        Cipher cipher = Cipher.getInstance("AES");// 創建密碼器        byte[] byteContent = data.getBytes("utf-8");        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);// 初始化        byte[] result = cipher.doFinal(byteContent);        return result; // 加密      } catch (Exception e) {        e.printStackTrace();      }    }    return null;  }  /**   * @Author: Helon   * @Description: AES加密,返回String   * @param data - 待加密內容   * @param key - 加密秘鑰   * @Data: 2018/7/28 18:59   * @Modified By:   */  public static String encryptToStr(String data, String key){    return StringUtils.isNotBlank(data)?parseByte2HexStr(encrypt(data, key)):null;  }  /**   * @Author: Helon   * @Description: AES解密   * @param data - 待解密字節數組   * @param key - 秘鑰   * @Data: 2018/7/28 19:01   * @Modified By:   */  public static byte[] decrypt(byte[] data, String key) {    if (ArrayUtils.isNotEmpty(data)) {      try {        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");        //選擇一種固定算法,為了避免不同java實現的不同算法,生成不同的密鑰,而導致解密失敗        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");        random.setSeed(key.getBytes());        keyGenerator.init(KEYSIZE, random);        SecretKey secretKey = keyGenerator.generateKey();        byte[] enCodeFormat = secretKey.getEncoded();        SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");        Cipher cipher = Cipher.getInstance("AES");// 創建密碼器        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);// 初始化        byte[] result = cipher.doFinal(data);        return result; // 加密      } catch (Exception e) {        e.printStackTrace();      }    }    return null;  }  /**   * @Author: Helon   * @Description: AES解密,返回String   * @param enCryptdata - 待解密字節數組   * @param key - 秘鑰   * @Data: 2018/7/28 19:01   * @Modified By:   */  public static String decryptToStr(String enCryptdata, String key) {    return StringUtils.isNotBlank(enCryptdata)?new String(decrypt(parseHexStr2Byte(enCryptdata), key)):null;  }  /**   * @Author: Helon   * @Description: 將二進制轉換成16進制   * @param buf - 二進制數組   * @Data: 2018/7/28 19:12   * @Modified By:   */  public static String parseByte2HexStr(byte buf[]) {    StringBuffer sb = new StringBuffer();    for (int i = 0; i < buf.length; i++) {      String hex = Integer.toHexString(buf[i] & 0xFF);      if (hex.length() == 1) {        hex = '0' + hex;      }      sb.append(hex.toUpperCase());    }    return sb.toString();  }  /**   * @Author: Helon   * @Description: 將16進制轉換為二進制   * @param hexStr - 16進制字符串   * @Data: 2018/7/28 19:13   * @Modified By:   */  public static byte[] parseHexStr2Byte(String hexStr) {    if (hexStr.length() < 1)      return null;    byte[] result = new byte[hexStr.length()/2];    for (int i = 0;i< hexStr.length()/2; i++) {      int high = Integer.parseInt(hexStr.substring(i*2, i*2+1), 16);      int low = Integer.parseInt(hexStr.substring(i*2+1, i*2+2), 16);      result[i] = (byte) (high * 16 + low);    }    return result;  }  public static void main(String[] args) {    String ss = encryptToStr("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiIxMjMiLCJ1c2VyTmFtZSI6Ikp1ZHkiLCJleHAiOjE1MzI3Nzk2MjIsIm5iZiI6MTUzMjc3NzgyMn0.sIw_leDZwG0pJ8ty85Iecd_VXjObYutILNEwPUyeVSo", SecretConstant.DATAKEY);    System.out.println(ss);    System.out.println(decryptToStr(ss, SecretConstant.DATAKEY));  }

所使用的常量類:

/** * @Author: Helon * @Description: JWT使用常量值 * @Data: Created in 2018/7/27 14:37 * @Modified By: */public class SecretConstant {  //簽名秘鑰 自定義  public static final String BASE64SECRET = "***********";  //超時毫秒數(默認30分鐘)  public static final int EXPIRESSECOND = 1800000;  //用于JWT加密的密匙 自定義  public static final String DATAKEY = "************";}

客戶端pom依賴:

 <!--jwt工具類-->    <dependency>      <groupId>com.chtwm.component</groupId>      <artifactId>jwt-helper</artifactId>      <version>xxx</version>    </dependency>

客戶端攔截器:

/** * @Author: Helon * @Description: 校驗是否登錄攔截器 * @Data: Created in 2018/7/30 14:30 * @Modified By: */@Slf4jpublic class ValidateLoginInterceptor implements HandlerInterceptor {  @Override  public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {    //首先從請求頭中獲取jwt串,與頁面約定好存放jwt值的請求頭屬性名為User-Token    String jwt = httpServletRequest.getHeader("User-Token");    log.info("[登錄校驗攔截器]-從header中獲取的jwt為:{}", jwt);    //判斷jwt是否有效    if(StringUtils.isNotBlank(jwt)){      //校驗jwt是否有效,有效則返回json信息,無效則返回空      String retJson = JwtHelper.validateLogin(jwt);      log.info("[登錄校驗攔截器]-校驗JWT有效性返回結果:{}", retJson);      //retJSON為空則說明jwt超時或非法      if(StringUtils.isNotBlank(retJson)){        JSONObject jsonObject = JSONObject.parseObject(retJson);        //校驗客戶端信息        String userAgent = httpServletRequest.getHeader("User-Agent");        if (userAgent.equals(jsonObject.getString("userAgent"))) {          //獲取刷新后的jwt值,設置到響應頭中          httpServletResponse.setHeader("User-Token", jsonObject.getString("freshToken"));          //將客戶編號設置到session中          httpServletRequest.getSession().setAttribute(GlobalConstant.SESSION_CUSTOMER_NO_KEY, jsonObject.getString("userId"));          return true;        }else{          log.warn("[登錄校驗攔截器]-客戶端瀏覽器信息與JWT中存的瀏覽器信息不一致,重新登錄。當前瀏覽器信息:{}", userAgent);        }      }else {        log.warn("[登錄校驗攔截器]-JWT非法或已超時,重新登錄");      }    }    //輸出響應流    JSONObject jsonObject = new JSONObject();    jsonObject.put("hmac", "");    jsonObject.put("status", "");    jsonObject.put("code", "4007");    jsonObject.put("msg", "未登錄");    jsonObject.put("data", "");    httpServletResponse.setCharacterEncoding("UTF-8");    httpServletResponse.setContentType("application/json; charset=utf-8");    httpServletResponse.getOutputStream().write(jsonObject.toJSONString().getBytes("UTF-8"));    return false;  }  @Override  public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {  }  @Override  public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {  }}

客戶端攔截器在XML文件中配置:

<!--攔截器配置-->  <mvc:interceptors>    <mvc:interceptor>	    <!--需攔截url配置-->      <mvc:exclude-mapping path="/api/aa/bb/**" />      <mvc:exclude-mapping path="/api/aa/cc/test" />      <bean id="validateLoginInterceptor" class="com.xxx.ValidateLoginInterceptor" />    </mvc:interceptor>  </mvc:interceptors>

到此,后臺服務的配置已經完成,下一步就需要前端頁面將JWT令牌從response響應頭中取出,然后存入localstorage或cookie中。但是遇到跨域場景,處理起來就會比較復雜,因為一旦在瀏覽器中跨域將獲取不到localstorage中的JWT令牌。例如www.a.com域下的JWT,在www.b.com域下是獲取不到的,所以我選擇了一種頁面跨域的方式進行處理,使用iframe+H5的postMessage(參考博文),具體我使用代碼分享的方式來分析。

前端頁面js代碼(服務端):

 /**CURD本地存儲信息 start**/     (function(doc,win){       var fn=function(){};       fn.prototype={         /*本地數據存儲 t:cookie有效時間,單位s; domain:cookie存儲所屬的domain域*/         setLocalCookie: function (k, v, t,domain) {           //如果當前瀏覽器不支持localStorage將存儲在cookie中           typeof window.localStorage !== "undefined" ? localStorage.setItem(k, v) :             (function () {               t = t || 365 * 12 * 60 * 60;               domain=domain?domain:".jwtserver.com";               document.cookie = k + "=" + v + ";max-age=" + t+";domain="+domain+";path=/";             })()         },         /*獲取本地存儲數據*/         getLocalCookie: function (k) {           k = k || "localDataTemp";           return typeof window.localStorage !== "undefined" ? localStorage.getItem(k) :             (function () {               var all = document.cookie.split(";");               var cookieData = {};               for (var i = 0, l = all.length; i < l; i++) {                 var p = all[i].indexOf("=");                 var dataName = all[i].substring(0, p).replace(/^[/s/uFEFF/xA0]+|[/s/uFEFF/xA0]+$/g,"");                 cookieData[dataName] = all[i].substring(p + 1);               }               return cookieData[k]             })();         },         /*刪除本地存儲數據*/         clearLocalData: function (k) {           k = k || "localDataTemp";           typeof window.localStorage !== "undefined" ? localStorage.removeItem(k) :             (function () {               document.cookie = k + "=temp" + ";max-age=0";             })()         },         init:function(){           this.bindEvent();         },         //事件綁定         bindEvent:function(){           var _this=this;           win.addEventListener("message",function(evt){             if(win.parent!=evt.source){return}             var options=JSON.parse(evt.data);             if(options.type=="GET"){               var data=tools.getLocalCookie(options.key);               win.parent.postMessage(data, "*");             }             options.type=="SET"&&_this.setLocalCookie(options.key,options.value);             options.type=="REM"&&_this.clearLocalData(options.key);           },false)         }       };       var tools=new fn();       tools.init();     })(document,window);     /**CURD本地存儲信息 end**/

前端頁面js代碼(客戶端):

   //頁面初始化向iframe域名發送消息    window.onload = function() {      console.log('get key value......................')      window.frames[0].postMessage(JSON.stringify({type:"GET",key:"User-Token"}),'*');    }    //監聽message信息,接收從iframe域下獲取到的token信息,然后存儲到localstorage或cookie中    window.addEventListener('message', function(e) {      console.log('listen.....');      var data = e.data;      console.log(data);      if(data != null){        localStorage.setItem("User-Token", data);      }    }, false);

總結:

優點:在非跨域環境下使用JWT機制是一個非常不錯的選擇,實現方式簡單,操作方便,能夠快速實現。由于服務端不存儲用戶狀態信息,因此大用戶量,對后臺服務也不會造成壓力;

缺點:跨域實現相對比較麻煩,安全性也有待探討。因為JWT令牌返回到頁面中,可以使用js獲取到,如果遇到XSS攻擊令牌可能會被盜取,在JWT還沒超時的情況下,就會被獲取到敏感數據信息。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。


注:相關教程知識閱讀請移步到JavaScript/Ajax教程頻道。
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
国产精品久久久久免费a∨| 国产精品福利在线观看| 国产精品久久不能| 奇门遁甲1982国语版免费观看高清| 欧美激情欧美狂野欧美精品| 亚洲精品福利免费在线观看| 国产精品99久久99久久久二8| 日韩女在线观看| 国产精品白丝av嫩草影院| 欧美日韩爱爱视频| 国产精品27p| 亚洲一区二区在线播放| 久热精品在线视频| 久久久极品av| 亚洲三级免费看| 91chinesevideo永久地址| 欧美视频中文字幕在线| 91精品综合视频| 大伊人狠狠躁夜夜躁av一区| 91亚洲国产成人精品性色| 久久91精品国产91久久久| 亚洲国产欧美一区二区三区久久| 亚洲一区二区三区视频播放| 26uuu国产精品视频| 亚洲已满18点击进入在线看片| 成人福利网站在线观看11| 国产成人精品免高潮在线观看| 久久久久久久久久国产| 欧美日韩日本国产| 69精品小视频| 欧美野外wwwxxx| 插插插亚洲综合网| 国产日韩精品一区二区| 久久激情五月丁香伊人| 日韩美女写真福利在线观看| 日本三级韩国三级久久| 亚洲女人天堂色在线7777| 美女av一区二区| 欧美一级淫片丝袜脚交| 尤物tv国产一区| 精品夜色国产国偷在线| 国产精品高清免费在线观看| 海角国产乱辈乱精品视频| 亚洲成av人乱码色午夜| 少妇精69xxtheporn| 在线看日韩av| 国产欧美一区二区三区视频| 国产一区二区三区在线看| 欧美极品第一页| 日韩av影院在线观看| 美日韩精品免费视频| 日韩av片免费在线观看| 日本韩国欧美精品大片卡二| 亚洲色图第三页| 久久人人爽人人| 日本三级韩国三级久久| 成人性生交大片免费观看嘿嘿视频| 久久av红桃一区二区小说| 91精品国产91久久久久久| 亚洲精品久久久久中文字幕二区| 精品国产91久久久久久老师| 亚洲精品白浆高清久久久久久| 在线亚洲男人天堂| 亚洲最大福利视频网| 国产精品永久在线| 亚洲激情视频在线播放| 国产精品日韩欧美大师| 57pao国产精品一区| 久久精品2019中文字幕| 中文字幕9999| 日韩一区二区av| 成人中文字幕在线观看| 俺去了亚洲欧美日韩| 亚洲精品美女久久久| 亚洲天堂av在线播放| 一夜七次郎国产精品亚洲| 亚洲一区制服诱惑| 国产精品18久久久久久麻辣| 欧美人与性动交a欧美精品| 久久人人爽人人| 欧美xxxx综合视频| 亚洲人成网站色ww在线| 欧美第一淫aaasss性| 亚洲欧美一区二区三区情侣bbw| 亚洲人成在线一二| 欧美日韩国产中字| 亚洲第一精品夜夜躁人人爽| 欧洲日韩成人av| 欧美一级大片在线观看| 久久久久五月天| 欧美色欧美亚洲高清在线视频| 另类视频在线观看| 在线a欧美视频| 亚洲欧美国产另类| 国内精品一区二区三区四区| 亚洲成人精品在线| 78色国产精品| 欧美黄色性视频| 亚洲亚裔videos黑人hd| 成人福利在线视频| 91精品国产高清久久久久久久久| 欧美性xxxxx极品| 日本不卡视频在线播放| 一区二区欧美激情| 欧美情侣性视频| 久久精品视频网站| 国产免费一区视频观看免费| 中日韩美女免费视频网址在线观看| 永久免费毛片在线播放不卡| 51ⅴ精品国产91久久久久久| 国产精品99久久久久久www| 国产精品久久久久久久9999| 国产精品美女免费| 九九九久久国产免费| 最近2019好看的中文字幕免费| 正在播放国产一区| 日韩视频免费在线| 国产精品女人网站| 福利微拍一区二区| 777午夜精品福利在线观看| 亚洲精品久久久久| 日韩精品免费观看| 国产日韩欧美电影在线观看| 欧美精品福利在线| 亚洲大胆人体视频| 久久视频在线观看免费| 亚洲va久久久噜噜噜久久天堂| 亚洲欧洲中文天堂| 欧美日韩亚洲天堂| 欧美猛少妇色xxxxx| 国产美女精彩久久| 91夜夜未满十八勿入爽爽影院| 日韩精品高清在线| 国产91精品最新在线播放| 亚洲美女性视频| 992tv成人免费视频| 91精品国产91久久久久福利| 91精品国产99久久久久久| 日韩电影在线观看永久视频免费网站| 国产精品久久久91| 欧美色欧美亚洲高清在线视频| 中文字幕av一区中文字幕天堂| 欧美肥臀大乳一区二区免费视频| 亚洲综合视频1区| 日韩中文字幕在线视频播放| 国产成人精品a视频一区www| 美女黄色丝袜一区| 久久男人资源视频| 久色乳综合思思在线视频| 狠狠操狠狠色综合网| 久久久99免费视频| 日韩美女中文字幕| 日韩一二三在线视频播| 国内成人精品视频| 亚洲电影免费观看高清完整版在线| 色婷婷av一区二区三区久久| 国产欧美婷婷中文| 亚洲日韩中文字幕| 一本大道香蕉久在线播放29| 国产精品久久久久久亚洲调教| 欧美在线观看一区二区三区| 91啪国产在线| 国语自产偷拍精品视频偷| 色午夜这里只有精品|