一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|JavaScript|易語言|

服務器之家 - 編程語言 - Java教程 - Spring Security架構以及源碼詳析

Spring Security架構以及源碼詳析

2021-05-08 10:54JadePeng Java教程

這篇文章主要給大家介紹了關于Spring Security架構以及源碼的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧

前言

現在流行的通用授權框架有apache的shiro和spring家族的spring security,在涉及今天的微服務鑒權時,需要利用我們的授權框架搭建自己的鑒權服務,今天總理了spring security。

spring security 主要實現了authentication(認證,解決who are you? ) 和 access control(訪問控制,也就是what are you allowed to do?,也稱為authorization)。spring security在架構上將認證與授權分離,并提供了擴展點。

核心對象

主要代碼在spring-security-core包下面。要了解spring security,需要先關注里面的核心對象。

securitycontextholder, securitycontext 和 authentication

securitycontextholder 是 securitycontext的存放容器,默認使用threadlocal 存儲,意味securitycontext在相同線程中的方法都可用。

securitycontext主要是存儲應用的principal信息,在spring security中用authentication 來表示。

獲取principal:

?
1
2
3
4
5
6
7
object principal = securitycontextholder.getcontext().getauthentication().getprincipal();
 
if (principal instanceof userdetails) {
string username = ((userdetails)principal).getusername();
} else {
string username = principal.tostring();
}

在spring security中,可以看一下authentication定義:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface authentication extends principal, serializable {
 collection<? extends grantedauthority> getauthorities();
 /**
 * 通常是密碼
 */
 object getcredentials();
 /**
 * stores additional details about the authentication request. these might be an ip
 * address, certificate serial number etc.
 */
 object getdetails();
 
 /**
 * 用來標識是否已認證,如果使用用戶名和密碼登錄,通常是用戶名
 */
 object getprincipal();
 /**
 * 是否已認證
 */
 boolean isauthenticated();
 void setauthenticated(boolean isauthenticated) throws illegalargumentexception;
}

在實際應用中,通常使用usernamepasswordauthenticationtoken:

?
1
2
3
4
5
public abstract class abstractauthenticationtoken implements authentication,
 credentialscontainer {
 }
public class usernamepasswordauthenticationtoken extends abstractauthenticationtoken {
}

一個常見的認證過程通常是這樣的,創建一個usernamepasswordauthenticationtoken,然后交給authenticationmanager認證(后面詳細說明),認證通過則通過securitycontextholder存放authentication信息。

?
1
2
3
4
5
usernamepasswordauthenticationtoken authenticationtoken =
 new usernamepasswordauthenticationtoken(loginvm.getusername(), loginvm.getpassword());
 
authentication authentication = this.authenticationmanager.authenticate(authenticationtoken);
securitycontextholder.getcontext().setauthentication(authentication);

userdetails與userdetailsservice

userdetails 是spring security里的一個關鍵接口,他用來表示一個principal。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface userdetails extends serializable {
 /**
 * 用戶的授權信息,可以理解為角色
 */
 collection<? extends grantedauthority> getauthorities();
 /**
 * 用戶密碼
 *
 * @return the password
 */
 string getpassword();
 /**
 * 用戶名
 * */
 string getusername();
 boolean isaccountnonexpired();
 boolean isaccountnonlocked();
 boolean iscredentialsnonexpired();
 boolean isenabled();
}

userdetails提供了認證所需的必要信息,在實際使用里,可以自己實現userdetails,并增加額外的信息,比如email、mobile等信息。

在authentication中的principal通常是用戶名,我們可以通過userdetailsservice來通過principal獲取userdetails:

?
1
2
3
public interface userdetailsservice {
 userdetails loaduserbyusername(string username) throws usernamenotfoundexception;
}

grantedauthority

在userdetails里說了,grantedauthority可以理解為角色,例如 role_administrator or role_hr_supervisor。

小結

  • securitycontextholder, 用來訪問 securitycontext.
  • securitycontext, 用來存儲authentication .
  • authentication, 代表憑證.
  • grantedauthority, 代表權限.
  • userdetails, 用戶信息.
  • userdetailsservice,獲取用戶信息.

authentication認證

authenticationmanager

實現認證主要是通過authenticationmanager接口,它只包含了一個方法:

?
1
2
3
4
public interface authenticationmanager {
 authentication authenticate(authentication authentication)
 throws authenticationexception;
}

authenticate()方法主要做三件事:

  • 如果驗證通過,返回authentication(通常帶上authenticated=true)。
  • 認證失敗拋出authenticationexception
  • 如果無法確定,則返回null

authenticationexception是運行時異常,它通常由應用程序按通用方式處理,用戶代碼通常不用特意被捕獲和處理這個異常。

authenticationmanager的默認實現是providermanager,它委托一組authenticationprovider實例來實現認證。
authenticationprovider和authenticationmanager類似,都包含authenticate,但它有一個額外的方法supports,以允許查詢調用方是否支持給定authentication類型:

?
1
2
3
4
5
public interface authenticationprovider {
 authentication authenticate(authentication authentication)
 throws authenticationexception;
 boolean supports(class<?> authentication);
}

providermanager包含一組authenticationprovider,執行authenticate時,遍歷providers,然后調用supports,如果支持,則執行遍歷當前provider的authenticate方法,如果一個provider認證成功,則break。

?
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
public authentication authenticate(authentication authentication)
 throws authenticationexception {
 class<? extends authentication> totest = authentication.getclass();
 authenticationexception lastexception = null;
 authentication result = null;
 boolean debug = logger.isdebugenabled();
 
 for (authenticationprovider provider : getproviders()) {
 if (!provider.supports(totest)) {
 continue;
 }
 
 if (debug) {
 logger.debug("authentication attempt using "
  + provider.getclass().getname());
 }
 
 try {
 result = provider.authenticate(authentication);
 
 if (result != null) {
  copydetails(authentication, result);
  break;
 }
 }
 catch (accountstatusexception e) {
 prepareexception(e, authentication);
 // sec-546: avoid polling additional providers if auth failure is due to
 // invalid account status
 throw e;
 }
 catch (internalauthenticationserviceexception e) {
 prepareexception(e, authentication);
 throw e;
 }
 catch (authenticationexception e) {
 lastexception = e;
 }
 }
 
 if (result == null && parent != null) {
 // allow the parent to try.
 try {
 result = parent.authenticate(authentication);
 }
 catch (providernotfoundexception e) {
 // ignore as we will throw below if no other exception occurred prior to
 // calling parent and the parent
 // may throw providernotfound even though a provider in the child already
 // handled the request
 }
 catch (authenticationexception e) {
 lastexception = e;
 }
 }
 
 if (result != null) {
 if (erasecredentialsafterauthentication
  && (result instanceof credentialscontainer)) {
 // authentication is complete. remove credentials and other secret data
 // from authentication
 ((credentialscontainer) result).erasecredentials();
 }
 eventpublisher.publishauthenticationsuccess(result);
 return result;
 }
 
 // parent was null, or didn't authenticate (or throw an exception).
 if (lastexception == null) {
 lastexception = new providernotfoundexception(messages.getmessage(
  "providermanager.providernotfound",
  new object[] { totest.getname() },
  "no authenticationprovider found for {0}"));
 }
 prepareexception(lastexception, authentication);
 throw lastexception;
 }

從上面的代碼可以看出, providermanager有一個可選parent,如果parent不為空,則調用parent.authenticate(authentication)

authenticationprovider

authenticationprovider有多種實現,大家最關注的通常是daoauthenticationprovider,繼承于abstractuserdetailsauthenticationprovider,核心是通過userdetails來實現認證,daoauthenticationprovider默認會自動加載,不用手動配。

先來看abstractuserdetailsauthenticationprovider,看最核心的authenticate:

?
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
public authentication authenticate(authentication authentication)
 throws authenticationexception {
 // 必須是usernamepasswordauthenticationtoken
 assert.isinstanceof(usernamepasswordauthenticationtoken.class, authentication,
 messages.getmessage(
  "abstractuserdetailsauthenticationprovider.onlysupports",
  "only usernamepasswordauthenticationtoken is supported"));
 
 // 獲取用戶名
 string username = (authentication.getprincipal() == null) ? "none_provided"
 : authentication.getname();
 
 boolean cachewasused = true;
 // 從緩存獲取
 userdetails user = this.usercache.getuserfromcache(username);
 
 if (user == null) {
 cachewasused = false;
 
 try {
 // retrieveuser 抽象方法,獲取用戶
 user = retrieveuser(username,
  (usernamepasswordauthenticationtoken) authentication);
 }
 catch (usernamenotfoundexception notfound) {
 logger.debug("user '" + username + "' not found");
 
 if (hideusernotfoundexceptions) {
  throw new badcredentialsexception(messages.getmessage(
  "abstractuserdetailsauthenticationprovider.badcredentials",
  "bad credentials"));
 }
 else {
  throw notfound;
 }
 }
 
 assert.notnull(user,
  "retrieveuser returned null - a violation of the interface contract");
 }
 
 try {
 // 預先檢查,defaultpreauthenticationchecks,檢查用戶是否被lock或者賬號是否可用
 preauthenticationchecks.check(user);
 
 // 抽象方法,自定義檢驗
 additionalauthenticationchecks(user,
  (usernamepasswordauthenticationtoken) authentication);
 }
 catch (authenticationexception exception) {
 if (cachewasused) {
 // there was a problem, so try again after checking
 // we're using latest data (i.e. not from the cache)
 cachewasused = false;
 user = retrieveuser(username,
  (usernamepasswordauthenticationtoken) authentication);
 preauthenticationchecks.check(user);
 additionalauthenticationchecks(user,
  (usernamepasswordauthenticationtoken) authentication);
 }
 else {
 throw exception;
 }
 }
 
 // 后置檢查 defaultpostauthenticationchecks,檢查iscredentialsnonexpired
 postauthenticationchecks.check(user);
 if (!cachewasused) {
 this.usercache.putuserincache(user);
 }
 
 object principaltoreturn = user;
 if (forceprincipalasstring) {
 principaltoreturn = user.getusername();
 }
 
 return createsuccessauthentication(principaltoreturn, authentication, user);
 }

上面的檢驗主要基于userdetails實現,其中獲取用戶和檢驗邏輯由具體的類去實現,默認實現是daoauthenticationprovider,這個類的核心是讓開發者提供userdetailsservice來獲取userdetails以及 passwordencoder來檢驗密碼是否有效:

?
1
2
private userdetailsservice userdetailsservice;
private passwordencoder passwordencoder;

看具體的實現,retrieveuser,直接調用userdetailsservice獲取用戶:

?
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
protected final userdetails retrieveuser(string username,
 usernamepasswordauthenticationtoken authentication)
 throws authenticationexception {
 userdetails loadeduser;
 
 try {
 loadeduser = this.getuserdetailsservice().loaduserbyusername(username);
 }
 catch (usernamenotfoundexception notfound) {
 if (authentication.getcredentials() != null) {
 string presentedpassword = authentication.getcredentials().tostring();
 passwordencoder.ispasswordvalid(usernotfoundencodedpassword,
  presentedpassword, null);
 }
 throw notfound;
 }
 catch (exception repositoryproblem) {
 throw new internalauthenticationserviceexception(
  repositoryproblem.getmessage(), repositoryproblem);
 }
 
 if (loadeduser == null) {
 throw new internalauthenticationserviceexception(
  "userdetailsservice returned null, which is an interface contract violation");
 }
 return loadeduser;
 }

再來看驗證:

?
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
protected void additionalauthenticationchecks(userdetails userdetails,
 usernamepasswordauthenticationtoken authentication)
 throws authenticationexception {
 object salt = null;
 
 if (this.saltsource != null) {
 salt = this.saltsource.getsalt(userdetails);
 }
 
 if (authentication.getcredentials() == null) {
 logger.debug("authentication failed: no credentials provided");
 
 throw new badcredentialsexception(messages.getmessage(
  "abstractuserdetailsauthenticationprovider.badcredentials",
  "bad credentials"));
 }
 // 獲取用戶密碼
 string presentedpassword = authentication.getcredentials().tostring();
 // 比較passwordencoder后的密碼是否和userdetails的密碼一致
 if (!passwordencoder.ispasswordvalid(userdetails.getpassword(),
 presentedpassword, salt)) {
 logger.debug("authentication failed: password does not match stored value");
 
 throw new badcredentialsexception(messages.getmessage(
  "abstractuserdetailsauthenticationprovider.badcredentials",
  "bad credentials"));
 }
 }

小結:要自定義認證,使用daoauthenticationprovider,只需要為其提供passwordencoder和userdetailsservice就可以了。

定制 authentication managers

spring security提供了一個builder類authenticationmanagerbuilder,借助它可以快速實現自定義認證。

看官方源碼說明:

securitybuilder used to create an authenticationmanager . allows for easily building in memory authentication, ldap authentication, jdbc based authentication, adding userdetailsservice , and adding authenticationprovider's.

authenticationmanagerbuilder可以用來build一個authenticationmanager,可以創建基于內存的認證、ldap認證、 jdbc認證,以及添加userdetailsservice和authenticationprovider。

簡單使用:

?
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
@configuration
@enablewebsecurity
@enableglobalmethodsecurity(prepostenabled = true, securedenabled = true)
public class applicationsecurity extends websecurityconfigureradapter {
 public securityconfiguration(authenticationmanagerbuilder authenticationmanagerbuilder, userdetailsservice userdetailsservice,tokenprovider tokenprovider,corsfilter corsfilter, securityproblemsupport problemsupport) {
 this.authenticationmanagerbuilder = authenticationmanagerbuilder;
 this.userdetailsservice = userdetailsservice;
 this.tokenprovider = tokenprovider;
 this.corsfilter = corsfilter;
 this.problemsupport = problemsupport;
 }
 
 @postconstruct
 public void init() {
 try {
 authenticationmanagerbuilder
 .userdetailsservice(userdetailsservice)
 .passwordencoder(passwordencoder());
 } catch (exception e) {
 throw new beaninitializationexception("security configuration failed", e);
 }
 }
 
 @override
 protected void configure(httpsecurity http) throws exception {
 http
 .addfilterbefore(corsfilter, usernamepasswordauthenticationfilter.class)
 .exceptionhandling()
 .authenticationentrypoint(problemsupport)
 .accessdeniedhandler(problemsupport)
 .and()
 .csrf()
 .disable()
 .headers()
 .frameoptions()
 .disable()
 .and()
 .sessionmanagement()
 .sessioncreationpolicy(sessioncreationpolicy.stateless)
 .and()
 .authorizerequests()
 .antmatchers("/api/register").permitall()
 .antmatchers("/api/activate").permitall()
 .antmatchers("/api/authenticate").permitall()
 .antmatchers("/api/account/reset-password/init").permitall()
 .antmatchers("/api/account/reset-password/finish").permitall()
 .antmatchers("/api/profile-info").permitall()
 .antmatchers("/api/**").authenticated()
 .antmatchers("/management/health").permitall()
 .antmatchers("/management/**").hasauthority(authoritiesconstants.admin)
 .antmatchers("/v2/api-docs/**").permitall()
 .antmatchers("/swagger-resources/configuration/ui").permitall()
 .antmatchers("/swagger-ui/index.html").hasauthority(authoritiesconstants.admin)
 .and()
 .apply(securityconfigureradapter());
 }
}

授權與訪問控制

一旦認證成功,我們可以繼續進行授權,授權是通過accessdecisionmanager來實現的。框架有三種實現,默認是affirmativebased,通過accessdecisionvoter決策,有點像providermanager委托給authenticationproviders來認證。

?
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
public void decide(authentication authentication, object object,
 collection<configattribute> configattributes) throws accessdeniedexception {
 int deny = 0;
 // 遍歷decisionvoter
 for (accessdecisionvoter voter : getdecisionvoters()) {
 // 投票
 int result = voter.vote(authentication, object, configattributes);
 
 if (logger.isdebugenabled()) {
 logger.debug("voter: " + voter + ", returned: " + result);
 }
 
 switch (result) {
 case accessdecisionvoter.access_granted:
 return;
 
 case accessdecisionvoter.access_denied:
 deny++;
 
 break;
 
 default:
 break;
 }
 }
 
 // 一票否決
 if (deny > 0) {
 throw new accessdeniedexception(messages.getmessage(
  "abstractaccessdecisionmanager.accessdenied", "access is denied"));
 }
 
 // to get this far, every accessdecisionvoter abstained
 checkallowifallabstaindecisions();
 }

來看accessdecisionvoter:

?
1
2
3
4
boolean supports(configattribute attribute);
boolean supports(class<?> clazz);
int vote(authentication authentication, s object,
 collection<configattribute> attributes);

object是用戶要訪問的資源,configattribute則是訪問object要滿足的條件,通常payload是字符串,比如role_admin 。所以我們來看下rolevoter的實現,其核心就是從authentication提取出grantedauthority,然后和configattribute比較是否滿足條件。

?
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
public boolean supports(configattribute attribute) {
 if ((attribute.getattribute() != null)
 && attribute.getattribute().startswith(getroleprefix())) {
 return true;
 }
 else {
 return false;
 }
 }
 
public boolean supports(class<?> clazz) {
 return true;
 }
 
public int vote(authentication authentication, object object,
 collection<configattribute> attributes) {
 if(authentication == null) {
 return access_denied;
 }
 int result = access_abstain;
 
 // 獲取grantedauthority信息
 collection<? extends grantedauthority> authorities = extractauthorities(authentication);
 
 for (configattribute attribute : attributes) {
 if (this.supports(attribute)) {
 // 默認拒絕訪問
 result = access_denied;
 
 // attempt to find a matching granted authority
 for (grantedauthority authority : authorities) {
  // 判斷是否有匹配的 authority
  if (attribute.getattribute().equals(authority.getauthority())) {
  // 可訪問
  return access_granted;
  }
 }
 }
 }
 return result;
 }

這里要疑問,configattribute哪來的?其實就是上面applicationsecurity的configure里的。

web security 如何實現

web層中的spring security(用于ui和http后端)基于servlet filters,下圖顯示了單個http請求的處理程序的典型分層。

Spring Security架構以及源碼詳析

spring security通過filterchainproxy作為單一的filter注冊到web層,proxy內部的filter。

Spring Security架構以及源碼詳析

filterchainproxy相當于一個filter的容器,通過virtualfilterchain來依次調用各個內部filter

?
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
public void dofilter(servletrequest request, servletresponse response,
  filterchain chain) throws ioexception, servletexception {
 boolean clearcontext = request.getattribute(filter_applied) == null;
 if (clearcontext) {
  try {
  request.setattribute(filter_applied, boolean.true);
  dofilterinternal(request, response, chain);
  }
  finally {
  securitycontextholder.clearcontext();
  request.removeattribute(filter_applied);
  }
 }
 else {
  dofilterinternal(request, response, chain);
 }
 }
 
 private void dofilterinternal(servletrequest request, servletresponse response,
  filterchain chain) throws ioexception, servletexception {
 
 firewalledrequest fwrequest = firewall
  .getfirewalledrequest((httpservletrequest) request);
 httpservletresponse fwresponse = firewall
  .getfirewalledresponse((httpservletresponse) response);
 
 list<filter> filters = getfilters(fwrequest);
 
 if (filters == null || filters.size() == 0) {
  if (logger.isdebugenabled()) {
  logger.debug(urlutils.buildrequesturl(fwrequest)
   + (filters == null ? " has no matching filters"
    : " has an empty filter list"));
  }
 
  fwrequest.reset();
 
  chain.dofilter(fwrequest, fwresponse);
 
  return;
 }
 
 virtualfilterchain vfc = new virtualfilterchain(fwrequest, chain, filters);
 vfc.dofilter(fwrequest, fwresponse);
 }
 
 private static class virtualfilterchain implements filterchain {
 private final filterchain originalchain;
 private final list<filter> additionalfilters;
 private final firewalledrequest firewalledrequest;
 private final int size;
 private int currentposition = 0;
 
 private virtualfilterchain(firewalledrequest firewalledrequest,
  filterchain chain, list<filter> additionalfilters) {
  this.originalchain = chain;
  this.additionalfilters = additionalfilters;
  this.size = additionalfilters.size();
  this.firewalledrequest = firewalledrequest;
 }
 
 public void dofilter(servletrequest request, servletresponse response)
  throws ioexception, servletexception {
  if (currentposition == size) {
  if (logger.isdebugenabled()) {
   logger.debug(urlutils.buildrequesturl(firewalledrequest)
    + " reached end of additional filter chain; proceeding with original chain");
  }
 
  // deactivate path stripping as we exit the security filter chain
  this.firewalledrequest.reset();
 
  originalchain.dofilter(request, response);
  }
  else {
  currentposition++;
 
  filter nextfilter = additionalfilters.get(currentposition - 1);
 
  if (logger.isdebugenabled()) {
   logger.debug(urlutils.buildrequesturl(firewalledrequest)
    + " at position " + currentposition + " of " + size
    + " in additional filter chain; firing filter: '"
    + nextfilter.getclass().getsimplename() + "'");
  }
 
  nextfilter.dofilter(request, response, this);
  }
 }
 }

參考

https://spring.io/guides/topicals/spring-security-architecture/

https://docs.spring.io/spring-security/site/docs/5.0.5.release/reference/htmlsingle/#overall-architecture

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對服務器之家的支持。

原文鏈接:http://www.cnblogs.com/xiaoqi/p/spring-security.html

延伸 · 閱讀

精彩推薦
主站蜘蛛池模板: 好湿好紧太硬了我好爽 | 欧美国产日产精品免费视频 | 全肉一女n男np高h双龙养成 | 高h全肉np触手 | 3d动漫美女物被遭强视频 | 动漫精品一区二区三区3d | china外卖员gay帮口 | 日本在线不卡免 | 国产精品永久免费视频 | 亚洲欧美一级夜夜爽w | 国产高清免费午夜在线视频 | 风间由美被义子中文字幕 | 亚洲欧美自偷自拍另类小说 | 国色天香社区在线视频免费观看 | 国产美女做爰免费视频网址 | 精品国产视频 | 亚洲无人区乱码中文字幕 | 欧美精品一区二区三区免费观看 | 9久爱午夜视频 | 欧美福利在线观看 | 国产精品免费精品自在线观看 | 国产一区精品视频 | 亚洲黄色免费在线观看 | 免费欧美一级片 | 国产精品欧美韩国日本久久 | 猫咪社区免费资源在线观看 | 99国产精品热久久久久久夜夜嗨 | 青青成人福利国产在线视频 | 亚洲sss视频 | 久久re这里精品在线视频7 | 色婷综合 | 国产99er66在线视频 | 亚洲国产免费观看视频 | 桃色视频破解版 | 91热国内精品永久免费观看 | 99久久伊人精品波多野结衣 | 青草青草久热精品视频在线网站 | 色哟哟在线视频 | 精品久久久久久久高清 | 免费抽搐一进一出印度 | 校花被老头夺去第一次动图 |