<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot集成shiro+jwt簡易教程

          共 38942字,需瀏覽 78分鐘

           ·

          2021-04-27 18:42


           作者:Smith-Cruise

          github.com/Smith-Cruise/Spring-Boot-Shiro

          特性

          • 完全使用了 Shiro 的注解配置,保持高度的靈活性。
          • 放棄 Cookie ,Session ,使用JWT進(jìn)行鑒權(quán),完全實(shí)現(xiàn)無狀態(tài)鑒權(quán)。
          • JWT 密鑰支持過期時(shí)間。
          • 對跨域提供支持。

          準(zhǔn)備工作

          在開始本教程之前,請保證已經(jīng)熟悉以下幾點(diǎn)。

          • Spring Boot 基本語法,至少要懂得 Controller  RestController  Autowired 等這些基本注釋。其實(shí)看看官方的 Getting-Start 教程就差不多了。
          • JWT (Json Web Token)的基本概念,并且會(huì)簡單操作JWT的 JAVA SDK。
          • Shiro 的基本操作,看下官方的 10 Minute Tutorial 即可。
          • 模擬 HTTP 請求工具,我使用的是 PostMan。

          簡要的說明下我們?yōu)槭裁匆?JWT ,因?yàn)槲覀円獙?shí)現(xiàn)完全的前后端分離,所以不可能使用 session cookie 的方式進(jìn)行鑒權(quán),所以 JWT 就被派上了用場,你可以通過一個(gè)加密密鑰來進(jìn)行前后端的鑒權(quán)。

          程序邏輯

          1. 我們 POST 用戶名與密碼到 /login 進(jìn)行登入,如果成功返回一個(gè)加密 token,失敗的話直接返回 401 錯(cuò)誤。
          2. 之后用戶訪問每一個(gè)需要權(quán)限的網(wǎng)址請求必須在 header 中添加 Authorization 字段,例如 Authorization: token token 為密鑰。
          3. 后臺(tái)會(huì)進(jìn)行 token 的校驗(yàn),如果有誤會(huì)直接返回 401。

          Token加密說明

          • 攜帶了 username 信息在 token 中。
          • 設(shè)定了過期時(shí)間。
          • 使用用戶登入密碼對 token 進(jìn)行加密。

          Token校驗(yàn)流程

          1. 獲得 token 中攜帶的 username 信息。
          2. 進(jìn)入數(shù)據(jù)庫搜索這個(gè)用戶,得到他的密碼。
          3. 使用用戶的密碼來檢驗(yàn) token 是否正確。

          準(zhǔn)備Maven文件

          新建一個(gè) Maven 工程,添加相關(guān)的 dependencies。

          <?xml version="1.0" encoding="UTF-8"?>
          <project xmlns="http://maven.apache.org/POM/4.0.0"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

              <modelVersion>4.0.0</modelVersion>

              <groupId>org.inlighting</groupId>
              <artifactId>shiro-study</artifactId>
              <version>1.0-SNAPSHOT</version>

              <dependencies>

                  <dependency>
                      <groupId>org.apache.shiro</groupId>
                      <artifactId>shiro-spring</artifactId>
                      <version>1.3.2</version>
                  </dependency>
                  <dependency>
                      <groupId>com.auth0</groupId>
                      <artifactId>java-jwt</artifactId>
                      <version>3.2.0</version>
                  </dependency>
                  <dependency>
                      <groupId>org.springframework.boot</groupId>
                      <artifactId>spring-boot-starter-web</artifactId>
                      <version>1.5.8.RELEASE</version>
                  </dependency>
              </dependencies>

              <build>
                  <plugins>
                    <!-- Srping Boot 打包工具 -->
                      <plugin>
                          <groupId>org.springframework.boot</groupId>
                          <artifactId>spring-boot-maven-plugin</artifactId>
                          <version>1.5.7.RELEASE</version>
                          <executions>
                              <execution>
                                  <goals>
                                      <goal>repackage</goal>
                                  </goals>
                              </execution>
                          </executions>
                      </plugin>
                      <!-- 指定JDK編譯版本 -->
                      <plugin>
                          <groupId>org.apache.maven.plugins</groupId>
                          <artifactId>maven-compiler-plugin</artifactId>
                          <configuration>
                              <source>1.8</source>
                              <target>1.8</target>
                              <encoding>UTF-8</encoding>
                          </configuration>
                      </plugin>
                  </plugins>
              </build>
          </project>

          注意指定JDK版本和編碼。

          構(gòu)建簡易的數(shù)據(jù)源

          為了縮減教程的代碼,我使用 HashMap 本地模擬了一個(gè)數(shù)據(jù)庫,結(jié)構(gòu)如下:

          usernamepasswordrolepermission
          smithsmith123userview
          dannydanny123adminview,edit

          這是一個(gè)最簡單的用戶權(quán)限表,如果想更加進(jìn)一步了解,自行百度 RBAC。

          之后再構(gòu)建一個(gè) UserService 來模擬數(shù)據(jù)庫查詢,并且把結(jié)果放到 UserBean 之中。

          UserService.java

          @Component
          public class UserService {

              public UserBean getUser(String username) {
                  // 沒有此用戶直接返回null
                  if (! DataSource.getData().containsKey(username))
                      return null;

                  UserBean user = new UserBean();
                  Map<String, String> detail = DataSource.getData().get(username);

                  user.setUsername(username);
                  user.setPassword(detail.get("password"));
                  user.setRole(detail.get("role"));
                  user.setPermission(detail.get("permission"));
                  return user;
              }
          }

          UserBean.java

          public class UserBean {
              private String username;

              private String password;

              private String role;

              private String permission;

              public String getUsername() {
                  return username;
              }

              public void setUsername(String username) {
                  this.username = username;
              }

              public String getPassword() {
                  return password;
              }

              public void setPassword(String password) {
                  this.password = password;
              }

              public String getRole() {
                  return role;
              }

              public void setRole(String role) {
                  this.role = role;
              }

              public String getPermission() {
                  return permission;
              }

              public void setPermission(String permission) {
                  this.permission = permission;
              }
          }

          配置 JWT

          我們寫一個(gè)簡單的 JWT 加密,校驗(yàn)工具,并且使用用戶自己的密碼充當(dāng)加密密鑰,這樣保證了 token 即使被他人截獲也無法破解。并且我們在 token 中附帶了 username 信息,并且設(shè)置密鑰5分鐘就會(huì)過期。

          public class JWTUtil {

              // 過期時(shí)間5分鐘
              private static final long EXPIRE_TIME = 5*60*1000;

              /**
               * 校驗(yàn)token是否正確
               * @param token 密鑰
               * @param secret 用戶的密碼
               * @return 是否正確
               */

              public static boolean verify(String token, String username, String secret) {
                  try {
                      Algorithm algorithm = Algorithm.HMAC256(secret);
                      JWTVerifier verifier = JWT.require(algorithm)
                              .withClaim("username", username)
                              .build();
                      DecodedJWT jwt = verifier.verify(token);
                      return true;
                  } catch (Exception exception) {
                      return false;
                  }
              }

              /**
               * 獲得token中的信息無需secret解密也能獲得
               * @return token中包含的用戶名
               */

              public static String getUsername(String token) {
                  try {
                      DecodedJWT jwt = JWT.decode(token);
                      return jwt.getClaim("username").asString();
                  } catch (JWTDecodeException e) {
                      return null;
                  }
              }

              /**
               * 生成簽名,5min后過期
               * @param username 用戶名
               * @param secret 用戶的密碼
               * @return 加密的token
               */

              public static String sign(String username, String secret) {
                  try {
                      Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
                      Algorithm algorithm = Algorithm.HMAC256(secret);
                      // 附帶username信息
                      return JWT.create()
                              .withClaim("username", username)
                              .withExpiresAt(date)
                              .sign(algorithm);
                  } catch (UnsupportedEncodingException e) {
                      return null;
                  }
              }
          }

          構(gòu)建URL

          ResponseBean.java

          既然想要實(shí)現(xiàn) restful,那我們要保證每次返回的格式都是相同的,因此我建立了一個(gè) ResponseBean 來統(tǒng)一返回的格式。(搜索公眾號(hào)Java知音,回復(fù)“2021”,送你一份Java面試題寶典)

          public class ResponseBean {
              
              // http 狀態(tài)碼
              private int code;

              // 返回信息
              private String msg;

              // 返回的數(shù)據(jù)
              private Object data;

              public ResponseBean(int code, String msg, Object data) {
                  this.code = code;
                  this.msg = msg;
                  this.data = data;
              }

              public int getCode() {
                  return code;
              }

              public void setCode(int code) {
                  this.code = code;
              }

              public String getMsg() {
                  return msg;
              }

              public void setMsg(String msg) {
                  this.msg = msg;
              }

              public Object getData() {
                  return data;
              }

              public void setData(Object data) {
                  this.data = data;
              }
          }

          自定義異常

          為了實(shí)現(xiàn)我自己能夠手動(dòng)拋出異常,我自己寫了一個(gè) UnauthorizedException.java

          public class UnauthorizedException extends RuntimeException {
              public UnauthorizedException(String msg) {
                  super(msg);
              }

              public UnauthorizedException() {
                  super();
              }
          }

          URL結(jié)構(gòu)

          URL作用
          /login登入
          /article所有人都可以訪問,但是用戶與游客看到的內(nèi)容不同
          /require_auth登入的用戶才可以進(jìn)行訪問
          /require_roleadmin的角色用戶才可以登入
          /require_permission擁有view和edit權(quán)限的用戶才可以訪問

          Controller

          @RestController
          public class WebController {

              private static final Logger LOGGER = LogManager.getLogger(WebController.class);

              private UserService userService;

              @Autowired
              public void setService(UserService userService) {
                  this.userService = userService;
              }

              @PostMapping("/login")
              public ResponseBean login(@RequestParam("username") String username,
                                        @RequestParam("password") String password) 
          {
                  UserBean userBean = userService.getUser(username);
                  if (userBean.getPassword().equals(password)) {
                      return new ResponseBean(200"Login success", JWTUtil.sign(username, password));
                  } else {
                      throw new UnauthorizedException();
                  }
              }

              @GetMapping("/article")
              public ResponseBean article() {
                  Subject subject = SecurityUtils.getSubject();
                  if (subject.isAuthenticated()) {
                      return new ResponseBean(200"You are already logged in"null);
                  } else {
                      return new ResponseBean(200"You are guest"null);
                  }
              }

              @GetMapping("/require_auth")
              @RequiresAuthentication
              public ResponseBean requireAuth() {
                  return new ResponseBean(200"You are authenticated"null);
              }

              @GetMapping("/require_role")
              @RequiresRoles("admin")
              public ResponseBean requireRole() {
                  return new ResponseBean(200"You are visiting require_role"null);
              }

              @GetMapping("/require_permission")
              @RequiresPermissions(logical = Logical.AND, value = {"view""edit"})
              public ResponseBean requirePermission() {
                  return new ResponseBean(200"You are visiting permission require edit,view"null);
              }

              @RequestMapping(path = "/401")
              @ResponseStatus(HttpStatus.UNAUTHORIZED)
              public ResponseBean unauthorized() {
                  return new ResponseBean(401"Unauthorized"null);
              }
          }

          處理框架異常

          之前說過 restful 要統(tǒng)一返回的格式,所以我們也要全局處理 Spring Boot 的拋出異常。利用 @RestControllerAdvice 能很好的實(shí)現(xiàn)。

          @RestControllerAdvice
          public class ExceptionController {

              // 捕捉shiro的異常
              @ResponseStatus(HttpStatus.UNAUTHORIZED)
              @ExceptionHandler(ShiroException.class)
              public ResponseBean handle401(ShiroException e
          {
                  return new ResponseBean(401, e.getMessage(), null);
              }

              // 捕捉UnauthorizedException
              @ResponseStatus(HttpStatus.UNAUTHORIZED)
              @ExceptionHandler(UnauthorizedException.class)
              public ResponseBean handle401() 
          {
                  return new ResponseBean(401"Unauthorized"null);
              }

              // 捕捉其他所有異常
              @ExceptionHandler(Exception.class)
              @ResponseStatus(HttpStatus.BAD_REQUEST)
              public ResponseBean globalException(HttpServletRequest requestThrowable ex
          {
                  return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);
              }

              private HttpStatus getStatus(HttpServletRequest request) {
                  Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
                  if (statusCode == null) {
                      return HttpStatus.INTERNAL_SERVER_ERROR;
                  }
                  return HttpStatus.valueOf(statusCode);
              }
          }

          配置 Shiro

          大家可以先看下官方的 Spring-Shiro 整合教程,有個(gè)初步的了解。不過既然我們用了 Spring-Boot,那我們肯定要爭取零配置文件。(搜索公眾號(hào)Java知音,回復(fù)“2021”,送你一份Java面試題寶典)

          實(shí)現(xiàn)JWTToken

          JWTToken 差不多就是 Shiro 用戶名密碼的載體。因?yàn)槲覀兪乔昂蠖朔蛛x,服務(wù)器無需保存用戶狀態(tài),所以不需要 RememberMe 這類功能,我們簡單的實(shí)現(xiàn)下 AuthenticationToken 接口即可。因?yàn)?span id="go7utgvlrp" class="Apple-converted-space"> token 自己已經(jīng)包含了用戶名等信息,所以這里我就弄了一個(gè)字段。如果你喜歡鉆研,可以看看官方的 UsernamePasswordToken 是如何實(shí)現(xiàn)的。

          public class JWTToken implements AuthenticationToken {

              // 密鑰
              private String token;

              public JWTToken(String token) {
                  this.token = token;
              }

              @Override
              public Object getPrincipal() {
                  return token;
              }

              @Override
              public Object getCredentials() {
                  return token;
              }
          }

          實(shí)現(xiàn)Realm

          realm 的用于處理用戶是否合法的這一塊,需要我們自己實(shí)現(xiàn)。

          @Service
          public class MyRealm extends AuthorizingRealm {

              private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);

              private UserService userService;

              @Autowired
              public void setUserService(UserService userService) {
                  this.userService = userService;
              }

              /**
               * 大坑!,必須重寫此方法,不然Shiro會(huì)報(bào)錯(cuò)
               */

              @Override
              public boolean supports(AuthenticationToken token) {
                  return token instanceof JWTToken;
              }

              /**
               * 只有當(dāng)需要檢測用戶權(quán)限的時(shí)候才會(huì)調(diào)用此方法,例如checkRole,checkPermission之類的
               */

              @Override
              protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
                  String username = JWTUtil.getUsername(principals.toString());
                  UserBean user = userService.getUser(username);
                  SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
                  simpleAuthorizationInfo.addRole(user.getRole());
                  Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
                  simpleAuthorizationInfo.addStringPermissions(permission);
                  return simpleAuthorizationInfo;
              }

              /**
               * 默認(rèn)使用此方法進(jìn)行用戶名正確與否驗(yàn)證,錯(cuò)誤拋出異常即可。
               */

              @Override
              protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
                  String token = (String) auth.getCredentials();
                  // 解密獲得username,用于和數(shù)據(jù)庫進(jìn)行對比
                  String username = JWTUtil.getUsername(token);
                  if (username == null) {
                      throw new AuthenticationException("token invalid");
                  }

                  UserBean userBean = userService.getUser(username);
                  if (userBean == null) {
                      throw new AuthenticationException("User didn't existed!");
                  }

                  if (! JWTUtil.verify(token, username, userBean.getPassword())) {
                      throw new AuthenticationException("Username or password error");
                  }

                  return new SimpleAuthenticationInfo(token, token, "my_realm");
              }
          }

           doGetAuthenticationInfo() 中用戶可以自定義拋出很多異常,詳情見文檔。

          重寫 Filter

          所有的請求都會(huì)先經(jīng)過 Filter,所以我們繼承官方的 BasicHttpAuthenticationFilter ,并且重寫鑒權(quán)的方法。

          代碼的執(zhí)行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin 

          public class JWTFilter extends BasicHttpAuthenticationFilter {

              private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

              /**
               * 判斷用戶是否想要登入。
               * 檢測header里面是否包含Authorization字段即可
               */

              @Override
              protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
                  HttpServletRequest req = (HttpServletRequest) request;
                  String authorization = req.getHeader("Authorization");
                  return authorization != null;
              }

              /**
               *
               */

              @Override
              protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
                  HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                  String authorization = httpServletRequest.getHeader("Authorization");

                  JWTToken token = new JWTToken(authorization);
                  // 提交給realm進(jìn)行登入,如果錯(cuò)誤他會(huì)拋出異常并被捕獲
                  getSubject(request, response).login(token);
                  // 如果沒有拋出異常則代表登入成功,返回true
                  return true;
              }

              /**
               * 這里我們詳細(xì)說明下為什么最終返回的都是true,即允許訪問
               * 例如我們提供一個(gè)地址 GET /article
               * 登入用戶和游客看到的內(nèi)容是不同的
               * 如果在這里返回了false,請求會(huì)被直接攔截,用戶看不到任何東西
               * 所以我們在這里返回true,Controller中可以通過 subject.isAuthenticated() 來判斷用戶是否登入
               * 如果有些資源只有登入用戶才能訪問,我們只需要在方法上面加上 @RequiresAuthentication 注解即可
               * 但是這樣做有一個(gè)缺點(diǎn),就是不能夠?qū)ET,POST等請求進(jìn)行分別過濾鑒權(quán)(因?yàn)槲覀冎貙懥斯俜降姆椒?,但實(shí)際上對應(yīng)用影響不大
               */

              @Override
              protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
                  if (isLoginAttempt(request, response)) {
                      try {
                          executeLogin(request, response);
                      } catch (Exception e) {
                          response401(request, response);
                      }
                  }
                  return true;
              }

              /**
               * 對跨域提供支持
               */

              @Override
              protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
                  HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                  HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                  httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
                  httpServletResponse.setHeader("Access-Control-Allow-Methods""GET,POST,OPTIONS,PUT,DELETE");
                  httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
                  // 跨域時(shí)會(huì)首先發(fā)送一個(gè)option請求,這里我們給option請求直接返回正常狀態(tài)
                  if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
                      httpServletResponse.setStatus(HttpStatus.OK.value());
                      return false;
                  }
                  return super.preHandle(request, response);
              }

              /**
               * 將非法請求跳轉(zhuǎn)到 /401
               */

              private void response401(ServletRequest req, ServletResponse resp) {
                  try {
                      HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
                      httpServletResponse.sendRedirect("/401");
                  } catch (IOException e) {
                      LOGGER.error(e.getMessage());
                  }
              }
          }

          getSubject(request, response).login(token); 這一步就是提交給了 realm 進(jìn)行處理。

          配置Shiro

          @Configuration
          public class ShiroConfig {

              @Bean("securityManager")
              public DefaultWebSecurityManager getManager(MyRealm realm) {
                  DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
                  // 使用自己的realm
                  manager.setRealm(realm);

                  /*
                   * 關(guān)閉shiro自帶的session,詳情見文檔
                   * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
                   */

                  DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
                  DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
                  defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
                  subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
                  manager.setSubjectDAO(subjectDAO);

                  return manager;
              }

              @Bean("shiroFilter")
              public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
                  ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

                  // 添加自己的過濾器并且取名為jwt
                  Map<String, Filter> filterMap = new HashMap<>();
                  filterMap.put("jwt"new JWTFilter());
                  factoryBean.setFilters(filterMap);

                  factoryBean.setSecurityManager(securityManager);
                  factoryBean.setUnauthorizedUrl("/401");

                  /*
                   * 自定義url規(guī)則
                   * http://shiro.apache.org/web.html#urls-
                   */

                  Map<String, String> filterRuleMap = new HashMap<>();
                  // 所有請求通過我們自己的JWT Filter
                  filterRuleMap.put("/**""jwt");
                  // 訪問401和404頁面不通過我們的Filter
                  filterRuleMap.put("/401""anon");
                  factoryBean.setFilterChainDefinitionMap(filterRuleMap);
                  return factoryBean;
              }

              /**
               * 下面的代碼是添加注解支持
               */

              @Bean
              @DependsOn("lifecycleBeanPostProcessor")
              public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
                  DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
                  // 強(qiáng)制使用cglib,防止重復(fù)代理和可能引起代理出錯(cuò)的問題
                  // https://zhuanlan.zhihu.com/p/29161098
                  defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
                  return defaultAdvisorAutoProxyCreator;
              }

              @Bean
              public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
                  return new LifecycleBeanPostProcessor();
              }

              @Bean
              public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
                  AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
                  advisor.setSecurityManager(securityManager);
                  return advisor;
              }
          }

          里面 URL 規(guī)則自己參考文檔即可 http://shiro.apache.org/web.html 。

          總結(jié)

          我就說下代碼還有哪些可以進(jìn)步的地方吧

          • 沒有實(shí)現(xiàn) Shiro 的 Cache 功能。
          • Shiro 中鑒權(quán)失敗時(shí)不能夠直接返回 401 信息,而是通過跳轉(zhuǎn)到 /401 地址實(shí)現(xiàn)。

          GitHub 項(xiàng)目地址:

          https://github.com/Smith-Cruise/Spring-Boot-Shiro


          瀏覽 34
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人毛片18女人毛片 | 精品无无码一区二区 | 国产欧美成人 | 对白av| 骚逼逼影院 |