整合營(yíng)銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          Spring Boot 整合 Shiro-登錄認(rèn)證和

          Spring Boot 整合 Shiro-登錄認(rèn)證和權(quán)限管理

          篇文章我們來(lái)學(xué)習(xí)如何使用 Spring Boot 集成 Apache Shiro 。安全應(yīng)該是互聯(lián)網(wǎng)公司的一道生命線,幾乎任何的公司都會(huì)涉及到這方面的需求。在 Java 領(lǐng)域一般有 Spring Security、 Apache Shiro 等安全框架,但是由于 Spring Security 過(guò)于龐大和復(fù)雜,大多數(shù)公司會(huì)選擇 Apache Shiro 來(lái)使用,這篇文章會(huì)先介紹一下 Apache Shiro ,在結(jié)合 Spring Boot 給出使用案例。

          Apache Shiro

          What is Apache Shiro?

          Apache Shiro 是一個(gè)功能強(qiáng)大、靈活的,開源的安全框架。它可以干凈利落地處理身份驗(yàn)證、授權(quán)、企業(yè)會(huì)話管理和加密。

          Apache Shiro 的首要目標(biāo)是易于使用和理解。安全通常很復(fù)雜,甚至讓人感到很痛苦,但是 Shiro 卻不是這樣子的。一個(gè)好的安全框架應(yīng)該屏蔽復(fù)雜性,向外暴露簡(jiǎn)單、直觀的 API,來(lái)簡(jiǎn)化開發(fā)人員實(shí)現(xiàn)應(yīng)用程序安全所花費(fèi)的時(shí)間和精力。

          Shiro 能做什么呢?

          • 驗(yàn)證用戶身份
          • 用戶訪問權(quán)限控制,比如:1、判斷用戶是否分配了一定的安全角色。2、判斷用戶是否被授予完成某個(gè)操作的權(quán)限
          • 在非 Web 或 EJB 容器的環(huán)境下可以任意使用 Session API
          • 可以響應(yīng)認(rèn)證、訪問控制,或者 Session 生命周期中發(fā)生的事件
          • 可將一個(gè)或以上用戶安全數(shù)據(jù)源數(shù)據(jù)組合成一個(gè)復(fù)合的用戶 “view”(視圖)
          • 支持單點(diǎn)登錄(SSO)功能
          • 支持提供“Remember Me”服務(wù),獲取用戶關(guān)聯(lián)信息而無(wú)需登錄

          等等——都集成到一個(gè)有凝聚力的易于使用的 API。

          Shiro 致力在所有應(yīng)用環(huán)境下實(shí)現(xiàn)上述功能,小到命令行應(yīng)用程序,大到企業(yè)應(yīng)用中,而且不需要借助第三方框架、容器、應(yīng)用服務(wù)器等。當(dāng)然 Shiro 的目的是盡量的融入到這樣的應(yīng)用環(huán)境中去,但也可以在它們之外的任何環(huán)境下開箱即用。

          Apache Shiro Features 特性

          Apache Shiro 是一個(gè)全面的、蘊(yùn)含豐富功能的安全框架。下圖為描述 Shiro 功能的框架圖:

          Authentication(認(rèn)證), Authorization(授權(quán)), Session Management(會(huì)話管理), Cryptography(加密)被 Shiro 框架的開發(fā)團(tuán)隊(duì)稱之為應(yīng)用安全的四大基石。那么就讓我們來(lái)看看它們吧:

          • Authentication(認(rèn)證):用戶身份識(shí)別,通常被稱為用戶“登錄”
          • Authorization(授權(quán)):訪問控制。比如某個(gè)用戶是否具有某個(gè)操作的使用權(quán)限。
          • Session Management(會(huì)話管理):特定于用戶的會(huì)話管理,甚至在非web 或 EJB 應(yīng)用程序。
          • Cryptography(加密):在對(duì)數(shù)據(jù)源使用加密算法加密的同時(shí),保證易于使用。

          還有其他的功能來(lái)支持和加強(qiáng)這些不同應(yīng)用環(huán)境下安全領(lǐng)域的關(guān)注點(diǎn)。特別是對(duì)以下的功能支持:

          • Web支持:Shiro 提供的 Web 支持 api ,可以很輕松的保護(hù) Web 應(yīng)用程序的安全。
          • 緩存:緩存是 Apache Shiro 保證安全操作快速、高效的重要手段。
          • 并發(fā):Apache Shiro 支持多線程應(yīng)用程序的并發(fā)特性。
          • 測(cè)試:支持單元測(cè)試和集成測(cè)試,確保代碼和預(yù)想的一樣安全。
          • “Run As”:這個(gè)功能允許用戶假設(shè)另一個(gè)用戶的身份(在許可的前提下)。
          • “Remember Me”:跨 session 記錄用戶的身份,只有在強(qiáng)制需要時(shí)才需要登錄。

          注意: Shiro 不會(huì)去維護(hù)用戶、維護(hù)權(quán)限,這些需要我們自己去設(shè)計(jì)/提供,然后通過(guò)相應(yīng)的接口注入給 Shiro

          High-Level Overview 高級(jí)概述

          在概念層,Shiro 架構(gòu)包含三個(gè)主要的理念:Subject,SecurityManager和 Realm。下面的圖展示了這些組件如何相互作用,我們將在下面依次對(duì)其進(jìn)行描述。

          • Subject:當(dāng)前用戶,Subject 可以是一個(gè)人,但也可以是第三方服務(wù)、守護(hù)進(jìn)程帳戶、時(shí)鐘守護(hù)任務(wù)或者其它–當(dāng)前和軟件交互的任何事件。
          • SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架構(gòu)的核心,配合內(nèi)部安全組件共同組成安全傘。
          • Realms:用于進(jìn)行權(quán)限信息的驗(yàn)證,我們自己實(shí)現(xiàn)。Realm 本質(zhì)上是一個(gè)特定的安全 DAO:它封裝與數(shù)據(jù)源連接的細(xì)節(jié),得到Shiro 所需的相關(guān)的數(shù)據(jù)。在配置 Shiro 的時(shí)候,你必須指定至少一個(gè)Realm 來(lái)實(shí)現(xiàn)認(rèn)證(authentication)和/或授權(quán)(authorization)。

          我們需要實(shí)現(xiàn)Realms的Authentication 和 Authorization。其中 Authentication 是用來(lái)驗(yàn)證用戶身份,Authorization 是授權(quán)訪問控制,用于對(duì)用戶進(jìn)行的操作授權(quán),證明該用戶是否允許進(jìn)行當(dāng)前操作,如訪問某個(gè)鏈接,某個(gè)資源文件等。

          快速上手

          基礎(chǔ)信息

          pom包依賴

          <dependencies>
          	<dependency>
          		<groupId>org.springframework.boot</groupId>
          		<artifactId>spring-boot-starter-data-jpa</artifactId>
          	</dependency>
          	<dependency>
          		<groupId>org.springframework.boot</groupId>
          		<artifactId>spring-boot-starter-thymeleaf</artifactId>
          	</dependency>
          	<dependency>
          		<groupId>net.sourceforge.nekohtml</groupId>
          		<artifactId>nekohtml</artifactId>
          		<version>1.9.22</version>
          	</dependency>
          	<dependency>
          		<groupId>org.springframework.boot</groupId>
          		<artifactId>spring-boot-starter-web</artifactId>
          	</dependency>
          	<dependency>
          		<groupId>org.apache.shiro</groupId>
          		<artifactId>shiro-spring</artifactId>
          		<version>1.4.0</version>
          	</dependency>
          	<dependency>
          		<groupId>mysql</groupId>
          		<artifactId>mysql-connector-java</artifactId>
          		<scope>runtime</scope>
          	</dependency>
          </dependencies>
          

          重點(diǎn)是 shiro-spring 包

          配置文件

          spring:
              datasource:
                url: jdbc:mysql://localhost:3306/test
                username: root
                password: root
                driver-class-name: com.mysql.jdbc.Driver
          
              jpa:
                database: mysql
                show-sql: true
                hibernate:
                  ddl-auto: update
                  naming:
                    strategy: org.hibernate.cfg.DefaultComponentSafeNamingStrategy
                properties:
                   hibernate:
                      dialect: org.hibernate.dialect.MySQL5Dialect
          
              thymeleaf:
                 cache: false
                 mode: LEGACYHTML5
          

          thymeleaf的配置是為了去掉html的校驗(yàn)

          頁(yè)面

          我們新建了六個(gè)頁(yè)面用來(lái)測(cè)試:

          • index.html :首頁(yè)
          • login.html :登錄頁(yè)
          • userInfo.html : 用戶信息頁(yè)面
          • userInfoAdd.html :添加用戶頁(yè)面
          • userInfoDel.html :刪除用戶頁(yè)面
          • 403.html : 沒有權(quán)限的頁(yè)面

          除過(guò)登錄頁(yè)面其它都很簡(jiǎn)單,大概如下:

          <!DOCTYPE html>
          <html lang="en">
          <head>
              <meta charset="UTF-8">
              <title>Title</title>
          </head>
          <body>
          <h1>index</h1>
          </body>
          </html>
          

          RBAC

          RBAC 是基于角色的訪問控制(Role-Based Access Control )在 RBAC 中,權(quán)限與角色相關(guān)聯(lián),用戶通過(guò)成為適當(dāng)角色的成員而得到這些角色的權(quán)限。這就極大地簡(jiǎn)化了權(quán)限的管理。這樣管理都是層級(jí)相互依賴的,權(quán)限賦予給角色,而把角色又賦予用戶,這樣的權(quán)限設(shè)計(jì)很清楚,管理起來(lái)很方便。

          采用 Jpa 技術(shù)來(lái)自動(dòng)生成基礎(chǔ)表格,對(duì)應(yīng)的實(shí)體如下:

          用戶信息

          @Entity
          public class UserInfo implements Serializable {
              @Id
              @GeneratedValue
              private Integer uid;
              @Column(unique=true)
              private String username;//帳號(hào)
              private String name;//名稱(昵稱或者真實(shí)姓名,不同系統(tǒng)不同定義)
              private String password; //密碼;
              private String salt;//加密密碼的鹽
              private byte state;//用戶狀態(tài),0:創(chuàng)建未認(rèn)證(比如沒有激活,沒有輸入驗(yàn)證碼等等)--等待驗(yàn)證的用戶 , 1:正常狀態(tài),2:用戶被鎖定.
              @ManyToMany(fetch=FetchType.EAGER)//立即從數(shù)據(jù)庫(kù)中進(jìn)行加載數(shù)據(jù);
              @JoinTable(name="SysUserRole", joinColumns={ @JoinColumn(name="uid") }, inverseJoinColumns={@JoinColumn(name="roleId") })
              private List<SysRole> roleList;// 一個(gè)用戶具有多個(gè)角色
          
              // 省略 get set 方法
           }
          

          角色信息

          @Entity
          public class SysRole {
              @Id@GeneratedValue
              private Integer id; // 編號(hào)
              private String role; // 角色標(biāo)識(shí)程序中判斷使用,如"admin",這個(gè)是唯一的:
              private String description; // 角色描述,UI界面顯示使用
              private Boolean available=Boolean.FALSE; // 是否可用,如果不可用將不會(huì)添加給用戶
          
              //角色 -- 權(quán)限關(guān)系:多對(duì)多關(guān)系;
              @ManyToMany(fetch=FetchType.EAGER)
              @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
              private List<SysPermission> permissions;
          
              // 用戶 - 角色關(guān)系定義;
              @ManyToMany
              @JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
              private List<UserInfo> userInfos;// 一個(gè)角色對(duì)應(yīng)多個(gè)用戶
          
              // 省略 get set 方法
           }
          

          權(quán)限信息

          @Entity
          public class SysPermission implements Serializable {
              @Id@GeneratedValue
              private Integer id;//主鍵.
              private String name;//名稱.
              @Column(columnDefinition="enum('menu','button')")
              private String resourceType;//資源類型,[menu|button]
              private String url;//資源路徑.
              private String permission; //權(quán)限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
              private Long parentId; //父編號(hào)
              private String parentIds; //父編號(hào)列表
              private Boolean available=Boolean.FALSE;
              @ManyToMany
              @JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
              private List<SysRole> roles;
          
              // 省略 get set 方法
           }
          

          根據(jù)以上的代碼會(huì)自動(dòng)生成 user_info(用戶信息表)、sys_role(角色表)、sys_permission(權(quán)限表)、sys_user_role(用戶角色表)、sys_role_permission(角色權(quán)限表)這五張表,為了方便測(cè)試我們給這五張表插入一些初始化數(shù)據(jù):

          INSERT INTO `user_info` (`uid`,`username`,`name`,`password`,`salt`,`state`) VALUES ('1', 'admin', '管理員', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 0);
          INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (1,0,'用戶管理',0,'0/','userInfo:view','menu','userInfo/userList');
          INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (2,0,'用戶添加',1,'0/1','userInfo:add','button','userInfo/userAdd');
          INSERT INTO `sys_permission` (`id`,`available`,`name`,`parent_id`,`parent_ids`,`permission`,`resource_type`,`url`) VALUES (3,0,'用戶刪除',1,'0/1','userInfo:del','button','userInfo/userDel');
          INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (1,0,'管理員','admin');
          INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (2,0,'VIP會(huì)員','vip');
          INSERT INTO `sys_role` (`id`,`available`,`description`,`role`) VALUES (3,1,'test','test');
          INSERT INTO `sys_role_permission` VALUES ('1', '1');
          INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
          INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
          INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (3,2);
          INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
          

          Shiro 配置

          首先要配置的是 ShiroConfig 類,Apache Shiro 核心通過(guò) Filter 來(lái)實(shí)現(xiàn),就好像 SpringMvc 通過(guò) DispachServlet 來(lái)主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過(guò) URL 規(guī)則來(lái)進(jìn)行過(guò)濾和權(quán)限校驗(yàn),所以我們需要定義一系列關(guān)于 URL 的規(guī)則和訪問權(quán)限。

          ShiroConfig

          @Configuration
          public class ShiroConfig {
          	@Bean
          	public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
          		System.out.println("ShiroConfiguration.shirFilter()");
          		ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
          		shiroFilterFactoryBean.setSecurityManager(securityManager);
          		//攔截器.
          		Map<String,String> filterChainDefinitionMap=new LinkedHashMap<String,String>();
          		// 配置不會(huì)被攔截的鏈接 順序判斷
          		filterChainDefinitionMap.put("/static/**", "anon");
          		//配置退出 過(guò)濾器,其中的具體的退出代碼Shiro已經(jīng)替我們實(shí)現(xiàn)了
          		filterChainDefinitionMap.put("/logout", "logout");
          		//<!-- 過(guò)濾鏈定義,從上向下順序執(zhí)行,一般將/**放在最為下邊 -->:這是一個(gè)坑呢,一不小心代碼就不好使了;
          		//<!-- authc:所有url都必須認(rèn)證通過(guò)才可以訪問; anon:所有url都都可以匿名訪問-->
          		filterChainDefinitionMap.put("/**", "authc");
          		// 如果不設(shè)置默認(rèn)會(huì)自動(dòng)尋找Web工程根目錄下的"/login.jsp"頁(yè)面
          		shiroFilterFactoryBean.setLoginUrl("/login");
          		// 登錄成功后要跳轉(zhuǎn)的鏈接
          		shiroFilterFactoryBean.setSuccessUrl("/index");
          
          		//未授權(quán)界面;
          		shiroFilterFactoryBean.setUnauthorizedUrl("/403");
          		shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
          		return shiroFilterFactoryBean;
          	}
          
          	@Bean
          	public MyShiroRealm myShiroRealm(){
          		MyShiroRealm myShiroRealm=new MyShiroRealm();
          		return myShiroRealm;
          	}
          
          
          	@Bean
          	public SecurityManager securityManager(){
          		DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
          		securityManager.setRealm(myShiroRealm());
          		return securityManager;
          	}
          }
          

          Filter Chain 定義說(shuō)明:

          • 1、一個(gè)URL可以配置多個(gè) Filter,使用逗號(hào)分隔
          • 2、當(dāng)設(shè)置多個(gè)過(guò)濾器時(shí),全部驗(yàn)證通過(guò),才視為通過(guò)
          • 3、部分過(guò)濾器可指定參數(shù),如 perms,roles

          Shiro 內(nèi)置的 FilterChain

          Filter NameClassanonorg.apache.shiro.web.filter.authc.AnonymousFilterauthcorg.apache.shiro.web.filter.authc.FormAuthenticationFilterauthcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilterpermsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilterportorg.apache.shiro.web.filter.authz.PortFilterrestorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilterrolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFiltersslorg.apache.shiro.web.filter.authz.SslFilteruserorg.apache.shiro.web.filter.authc.UserFilter

          • anon:所有 url 都都可以匿名訪問
          • authc: 需要認(rèn)證才能進(jìn)行訪問
          • user:配置記住我或認(rèn)證通過(guò)可以訪問

          登錄認(rèn)證實(shí)現(xiàn)

          在認(rèn)證、授權(quán)內(nèi)部實(shí)現(xiàn)機(jī)制中都有提到,最終處理都將交給Real進(jìn)行處理。因?yàn)樵?Shiro 中,最終是通過(guò) Realm 來(lái)獲取應(yīng)用程序中的用戶、角色及權(quán)限信息的。通常情況下,在 Realm 中會(huì)直接從我們的數(shù)據(jù)源中獲取 Shiro 需要的驗(yàn)證信息。可以說(shuō),Realm 是專用于安全框架的 DAO. Shiro 的認(rèn)證過(guò)程最終會(huì)交由 Realm 執(zhí)行,這時(shí)會(huì)調(diào)用 Realm 的getAuthenticationInfo(token)方法。

          該方法主要執(zhí)行以下操作:

          • 1、檢查提交的進(jìn)行認(rèn)證的令牌信息
          • 2、根據(jù)令牌信息從數(shù)據(jù)源(通常為數(shù)據(jù)庫(kù))中獲取用戶信息
          • 3、對(duì)用戶信息進(jìn)行匹配驗(yàn)證。
          • 4、驗(yàn)證通過(guò)將返回一個(gè)封裝了用戶信息的AuthenticationInfo實(shí)例。
          • 5、驗(yàn)證失敗則拋出AuthenticationException異常信息。

          而在我們的應(yīng)用程序中要做的就是自定義一個(gè) Realm 類,繼承AuthorizingRealm 抽象類,重載 doGetAuthenticationInfo(),重寫獲取用戶信息的方法。

          doGetAuthenticationInfo 的重寫

          @Override
          protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
                  throws AuthenticationException {
              System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
              //獲取用戶的輸入的賬號(hào).
              String username=(String)token.getPrincipal();
              System.out.println(token.getCredentials());
              //通過(guò)username從數(shù)據(jù)庫(kù)中查找 User對(duì)象,如果找到,沒找到.
              //實(shí)際項(xiàng)目中,這里可以根據(jù)實(shí)際情況做緩存,如果不做,Shiro自己也是有時(shí)間間隔機(jī)制,2分鐘內(nèi)不會(huì)重復(fù)執(zhí)行該方法
              UserInfo userInfo=userInfoService.findByUsername(username);
              System.out.println("----->>userInfo="+userInfo);
              if(userInfo==null){
                  return null;
              }
              SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(
                      userInfo, //用戶名
                      userInfo.getPassword(), //密碼
                      ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
                      getName()  //realm name
              );
              return authenticationInfo;
          }
          

          鏈接權(quán)限的實(shí)現(xiàn)

          Shiro 的權(quán)限授權(quán)是通過(guò)繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo();當(dāng)訪問到頁(yè)面的時(shí)候,鏈接配置了相應(yīng)的權(quán)限或者 Shiro 標(biāo)簽才會(huì)執(zhí)行此方法否則不會(huì)執(zhí)行,所以如果只是簡(jiǎn)單的身份認(rèn)證沒有權(quán)限的控制的話,那么這個(gè)方法可以不進(jìn)行實(shí)現(xiàn),直接返回 null 即可。在這個(gè)方法中主要是使用類:SimpleAuthorizationInfo進(jìn)行角色的添加和權(quán)限的添加。

          @Override
          protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
              System.out.println("權(quán)限配置-->MyShiroRealm.doGetAuthorizationInfo()");
              SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
              UserInfo userInfo=(UserInfo)principals.getPrimaryPrincipal();
              for(SysRole role:userInfo.getRoleList()){
                  authorizationInfo.addRole(role.getRole());
                  for(SysPermission p:role.getPermissions()){
                      authorizationInfo.addStringPermission(p.getPermission());
                  }
              }
              return authorizationInfo;
          }
          

          當(dāng)然也可以添加 set 集合:roles 是從數(shù)據(jù)庫(kù)查詢的當(dāng)前用戶的角色,stringPermissions 是從數(shù)據(jù)庫(kù)查詢的當(dāng)前用戶對(duì)應(yīng)的權(quán)限

          authorizationInfo.setRoles(roles);
          authorizationInfo.setStringPermissions(stringPermissions);
          

          就是說(shuō)如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[權(quán)限添加]”);就說(shuō)明訪問/add這個(gè)鏈接必須要有“權(quán)限添加”這個(gè)權(quán)限才可以訪問,如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權(quán)限添加]”);就說(shuō)明訪問/add這個(gè)鏈接必須要有“權(quán)限添加”這個(gè)權(quán)限和具有“100002”這個(gè)角色才可以訪問。

          登錄實(shí)現(xiàn)

          登錄過(guò)程其實(shí)只是處理異常的相關(guān)信息,具體的登錄驗(yàn)證交給 Shiro 來(lái)處理

          @RequestMapping("/login")
          public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
              System.out.println("HomeController.login()");
              // 登錄失敗從request中獲取shiro處理的異常信息。
              // shiroLoginFailure:就是shiro異常類的全類名.
              String exception=(String) request.getAttribute("shiroLoginFailure");
              System.out.println("exception=" + exception);
              String msg="";
              if (exception !=null) {
                  if (UnknownAccountException.class.getName().equals(exception)) {
                      System.out.println("UnknownAccountException -- > 賬號(hào)不存在:");
                      msg="UnknownAccountException -- > 賬號(hào)不存在:";
                  } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                      System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
                      msg="IncorrectCredentialsException -- > 密碼不正確:";
                  } else if ("kaptchaValidateFailed".equals(exception)) {
                      System.out.println("kaptchaValidateFailed -- > 驗(yàn)證碼錯(cuò)誤");
                      msg="kaptchaValidateFailed -- > 驗(yàn)證碼錯(cuò)誤";
                  } else {
                      msg="else >> "+exception;
                      System.out.println("else -- >" + exception);
                  }
              }
              map.put("msg", msg);
              // 此方法不處理登錄成功,由shiro進(jìn)行處理
              return "/login";
          }
          

          其它 Dao 層和 Service 的代碼就不貼出來(lái)了大家直接看代碼。

          測(cè)試

          1、編寫好后就可以啟動(dòng)程序,訪問http://localhost:8080/userInfo/userList頁(yè)面,由于沒有登錄就會(huì)跳轉(zhuǎn)到http://localhost:8080/login頁(yè)面。登錄之后就會(huì)跳轉(zhuǎn)到 index 頁(yè)面,登錄后,直接在瀏覽器中輸入http://localhost:8080/userInfo/userList訪問就會(huì)看到用戶信息。上面這些操作時(shí)候觸發(fā)MyShiroRealm.doGetAuthenticationInfo()這個(gè)方法,也就是登錄認(rèn)證的方法。

          2、登錄admin賬戶,訪問:http://127.0.0.1:8080/userInfo/userAdd顯示用戶添加界面,訪問http://127.0.0.1:8080/userInfo/userDel顯示403沒有權(quán)限。上面這些操作時(shí)候觸發(fā)MyShiroRealm.doGetAuthorizationInfo()這個(gè)方面,也就是權(quán)限校驗(yàn)的方法。

          3、修改 admin不 同的權(quán)限進(jìn)行測(cè)試

          Shiro 很強(qiáng)大,這僅僅是完成了登錄認(rèn)證和權(quán)限管理這兩個(gè)功能,更多內(nèi)容以后有時(shí)間再做探討。

          后臺(tái)權(quán)限管理系統(tǒng)

          相關(guān):

          spring boot + mybatis + layui + shiro后臺(tái)權(quán)限管理系統(tǒng)

          springboot + shiro之登錄人數(shù)限制、登錄判斷重定向、session時(shí)間設(shè)置

          springboot + shiro 動(dòng)態(tài)更新用戶信息

          基于前篇,新增功能:

          1. 新增shiro權(quán)限注解;
          2. 請(qǐng)求亂碼問題解決;
          3. 統(tǒng)一異常處理。

          源碼已集成到項(xiàng)目中:

          github源碼: https://github.com/wyait/manage.git

          碼云:https://gitee.com/wyait/manage.git

          github對(duì)應(yīng)項(xiàng)目源碼目錄:wyait-manage-1.2.0

          碼云對(duì)應(yīng)項(xiàng)目源碼目錄:wyait-manage-1.2.0

          shiro注解的使用

          shiro權(quán)限注解

          Shiro 提供了相應(yīng)的注解用于權(quán)限控制,如果使用這些注解就需要使用AOP 的功能來(lái)進(jìn)行判斷,如Spring AOP;Shiro 提供了Spring AOP 集成用于權(quán)限注解的解析和驗(yàn)證。

           @RequiresAuthentication
            表示當(dāng)前Subject已經(jīng)通過(guò)login 進(jìn)行了身份驗(yàn)證;即Subject.isAuthenticated()返回true。
            @RequiresUser
            表示當(dāng)前Subject已經(jīng)身份驗(yàn)證或者通過(guò)記住我登錄的。
            @RequiresGuest
            表示當(dāng)前Subject沒有身份驗(yàn)證或通過(guò)記住我登錄過(guò),即是游客身份。
            @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND)
            @RequiresRoles(value={“admin”})
            @RequiresRoles({“admin“})
            表示當(dāng)前Subject需要角色admin 和user。
            @RequiresPermissions (value={“user:a”, “user:b”}, logical=Logical.OR)
            表示當(dāng)前Subject需要權(quán)限user:a或user:b。
          

          Shiro的認(rèn)證注解處理是有內(nèi)定的處理順序的,如果有多個(gè)注解的話,前面的通過(guò)了會(huì)繼續(xù)檢查后面的,若不通過(guò)則直接返回,處理順序依次為(與實(shí)際聲明順序無(wú)關(guān)):

          RequiresRoles
          RequiresPermissions
          RequiresAuthentication
          RequiresUser
          RequiresGuest
          

          以上注解既可以用在controller中,也可以用在service中使用;

          建議將shiro注解放在controller中,因?yàn)槿绻鹲ervice層使用了spring的事物注解,那么shiro注解將無(wú)效。

          shiro權(quán)限注解springAOP配置

          shiro權(quán)限注解要生效,必須配置springAOP通過(guò)設(shè)置shiro的SecurityManager進(jìn)行權(quán)限驗(yàn)證。

          /**
           * 
           * @描述:開啟Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類,并在必要時(shí)進(jìn)行安全邏輯驗(yàn)證
           * 配置以下兩個(gè)bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可實(shí)現(xiàn)此功能
           * </br>Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor(保證實(shí)現(xiàn)了Shiro內(nèi)部lifecycle函數(shù)的bean執(zhí)行) has run
           * </br>不使用注解的話,可以注釋掉這兩個(gè)配置
           * @創(chuàng)建人:wyait
           * @創(chuàng)建時(shí)間:2018年5月21日 下午6:07:56
           * @return
           */
           @Bean
           public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
           DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
           advisorAutoProxyCreator.setProxyTargetClass(true);
           return advisorAutoProxyCreator;
           }
           @Bean
           public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
           AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
           authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
           return authorizationAttributeSourceAdvisor;
           }
          

          springboot異常處理原理

          場(chǎng)景:當(dāng)用戶正常訪問網(wǎng)站時(shí),因?yàn)槟撤N原因后端出現(xiàn)exception的時(shí)候,直接暴露異常信息或頁(yè)面顯示給用戶;

          這種操作體驗(yàn)不是我們想要的。所以要對(duì)異常進(jìn)行統(tǒng)一管理,能提高用戶體驗(yàn)的同時(shí),后臺(tái)能詳細(xì)定位到異常的問題點(diǎn)。

          springboot異常概況

          Spring Boot提供了默認(rèn)的統(tǒng)一錯(cuò)誤頁(yè)面,這是Spring MVC沒有提供的。在理解了Spring Boot提供的錯(cuò)誤處理相關(guān)內(nèi)容之后,我們可以方便的定義自己的錯(cuò)誤返回的格式和內(nèi)容。

          編寫by zero異常

          在home頁(yè)面,手動(dòng)創(chuàng)建兩個(gè)異常:普通異常和異步異常!

          • 前端頁(yè)面:
          <p>
           普通請(qǐng)求異常:
           <a href="/error/getError">點(diǎn)擊</a>
          </p>
          <p>
           ajax異步請(qǐng)求異常:
           <a href="javascript:void(0)" onclick="ajaxError()">點(diǎn)擊</a>
          </p>
          ... 
          //js代碼
          function ajaxError(){
           $.get("/error/ajaxError",function(data){
           layer.alert(data);
           });
          }
          
          • 后端代碼:
          /**
           * 
           * @描述:普通請(qǐng)求異常
           * @創(chuàng)建人:wyait
           * @創(chuàng)建時(shí)間:2018年5月24日 下午5:30:50
           */
          @RequestMapping("getError")
          public void toError(){
           System.out.println(1/0);
          }
          /**
           * 
           * @描述:異步異常
           * @創(chuàng)建人:wyait
           * @創(chuàng)建時(shí)間:2018年5月24日 下午5:30:39
           */
          @RequestMapping("ajaxError")
          @ResponseBody
          public String ajaxError(){
           System.out.println(1/0);
           return "異步請(qǐng)求成功!";
          }
          

          異常效果

          • 普通異常:

          console錯(cuò)誤信息:

          [2018-05-25 09:30:04.669][http-nio-8077-exec-8][ERROR][org.apache.juli.logging.DirectJDKLog][181]:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
          java.lang.ArithmeticException: / by zero
           at com.wyait.manage.web.error.IndexErrorController.toError(IndexErrorController.java:18) ~[classes/:?]
           ...
           at java.lang.Thread.run(Thread.java:748) [?:1.8.0_131]
          ...
          [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)]
          [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)]
          [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
          [2018-05-25 09:30:04.676][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
          ...
          [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][263]:Requested media types are [text/html, text/html;q=0.8] based on Accept header types and producible media types [text/html])
          [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][263]:Requested media types are [text/html, text/html;q=0.8] based on Accept header types and producible media types [text/html])
          [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'error'
          [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'error'
          [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][338]:Returning [org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView@6ffd99fb] based on requested media type 'text/html'
          [2018-05-25 09:30:04.686][http-nio-8077-exec-8][DEBUG][org.springframework.web.servlet.view.ContentNegotiatingViewResolver][338]:Returning [org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView@6ffd99fb] based on requested media type 'text/html'
          ...
          

          通過(guò)日志可知,springboot返回的錯(cuò)誤頁(yè)面,是通過(guò):org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml處理返回ModelAndView。

          • 異步異常:


          • console日志信息:
          [2018-05-25 09:31:19.958][http-nio-8077-exec-6][ERROR][org.apache.juli.logging.DirectJDKLog][181]:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
          java.lang.ArithmeticException: / by zero
           at com.wyait.manage.web.error.IndexErrorController.ajaxError(IndexErrorController.java:29) ~[classes/:?]
           ...
           at java.lang.Thread.run(Thread.java:748) [?:1.8.0_131]
          ...
          [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
          [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.handler.AbstractHandlerMethodMapping][317]:Returning handler method [public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)]
          [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
          [2018-05-25 09:31:19.960][http-nio-8077-exec-6][DEBUG][org.springframework.beans.factory.support.AbstractBeanFactory][251]:Returning cached instance of singleton bean 'basicErrorController'
          ...
          [2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor][234]:Written [{timestamp=Fri May 25 09:31:19 CST 2018, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, message=/ by zero, path=/error/ajaxError}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@2729eae5]
          [2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor][234]:Written [{timestamp=Fri May 25 09:31:19 CST 2018, status=500, error=Internal Server Error, exception=java.lang.ArithmeticException, message=/ by zero, path=/error/ajaxError}] as "application/json" using [org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@2729eae5]
          [2018-05-25 09:31:19.961][http-nio-8077-exec-6][DEBUG][org.springframework.web.servlet.DispatcherServlet][1048]:Null ModelAndView returned to DispatcherServlet with name 'dispatcherServlet': assuming HandlerAdapter completed request handling
          ...
          

          通過(guò)日志可知,springboot返回的錯(cuò)誤信息,是通過(guò):org.springframework.boot.autoconfigure.web.BasicErrorController.error處理返回ResponseEntity<String,Object>。

          • 異常都是通過(guò)org.springframework.boot.autoconfigure.web.BasicErrorController控制處理的。

          springboot異常處理解析

          查看org.springframework.boot.autoconfigure.web包下面的類,跟蹤springboot對(duì)error異常處理機(jī)制。自動(dòng)配置通過(guò)一個(gè)MVC error控制器處理錯(cuò)誤

          通過(guò)spring-boot-autoconfigure引入

          查看springboot 處理error的類

          springboot的自動(dòng)配置,在web中處理error相關(guān)的自動(dòng)配置類:ErrorMvcAutoConfiguration。查看與處理error相關(guān)的類:

          • ErrorMvcAutoConfiguration.class
          • ErrorAttibutes.class
          • ErrorController.class
          • ErrorProperties.class
          • ErrorViewResolver.class
          • ...

          ErrorAutoConfiguration類源碼//TODO

          ErrorAutoConfiguration注冊(cè)的bean

          //4個(gè)BEAN
          @Bean
          @ConditionalOnMissingBean(value=ErrorAttributes.class, search=SearchStrategy.CURRENT)
          public DefaultErrorAttributes errorAttributes() {
           return new DefaultErrorAttributes();
          }
          @Bean
          @ConditionalOnMissingBean(value=ErrorController.class, search=SearchStrategy.CURRENT)
          public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
           return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
           this.errorViewResolvers);
          }
          @Bean
          public ErrorPageCustomizer errorPageCustomizer() {
           return new ErrorPageCustomizer(this.serverProperties);
          }
          @Bean
          public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
           return new PreserveErrorControllerTargetClassPostProcessor();
          }
          
          1. DefaultErrorAttributes類
          @Order(Ordered.HIGHEST_PRECEDENCE)
          public class DefaultErrorAttributes
           implements ErrorAttributes, HandlerExceptionResolver, Ordered {
           ... 
           }
          

          ErrorAttributes:

          public interface ErrorAttributes {
           Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
           boolean includeStackTrace);
           Throwable getError(RequestAttributes requestAttributes);
          }
          

          HandlerExceptionResolver:

          public interface HandlerExceptionResolver {
           /**
           * Try to resolve the given exception that got thrown during handler execution,
           * returning a {@link ModelAndView} that represents a specific error page if appropriate.
           */
           ModelAndView resolveException(
           HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
          }
          

          DefaultErrorAttributes類:

          • 實(shí)現(xiàn)了ErrorAttributes接口,當(dāng)處理/error錯(cuò)誤頁(yè)面時(shí),可以在該bean中讀取錯(cuò)誤信息響應(yīng)返回;
          • 實(shí)現(xiàn)了HandlerExceptionResolver接口。

          debug跟蹤源碼:即DispatcherServlet在doDispatch過(guò)程中有異常拋出時(shí):

          一. 先由HandlerExceptionResolver.resolveException解析異常并保存在request中;

          二. 再DefaultErrorAttributes.getErrorAttributes處理;DefaultErrorAttributes在處理過(guò)程中,從request中獲取錯(cuò)誤信息,將錯(cuò)誤信息保存到RequestAttributes中;

          三. 最后在獲取錯(cuò)誤信息getError(RequestAttributes)時(shí),從RequestAttributes中取到錯(cuò)誤信息。

          1. BasicErrorController類
          @Controller
          @RequestMapping("${server.error.path:${error.path:/error}}")
          public class BasicErrorController extends AbstractErrorController {
           private final ErrorProperties errorProperties;
           ...
           @RequestMapping(produces="text/html")
           public ModelAndView errorHtml(HttpServletRequest request,
           HttpServletResponse response) {
           HttpStatus status=getStatus(request);
           Map<String, Object> model=Collections.unmodifiableMap(getErrorAttributes(
           request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
           response.setStatus(status.value());
           ModelAndView modelAndView=resolveErrorView(request, response, status, model);
           return (modelAndView==null ? new ModelAndView("error", model) : modelAndView);
           }
           @RequestMapping
           @ResponseBody
           public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
           Map<String, Object> body=getErrorAttributes(request,
           isIncludeStackTrace(request, MediaType.ALL));
           HttpStatus status=getStatus(request);
           return new ResponseEntity<Map<String, Object>>(body, status);
           }
           ...
          }
          

          resolveErrorView方法(查找=error/“錯(cuò)誤狀態(tài)碼”;的資源):

          如果不是異常請(qǐng)求,會(huì)執(zhí)行resolveErrorView方法;該方法會(huì)先在默認(rèn)或配置的靜態(tài)資源路徑下查找error/HttpStatus(錯(cuò)誤狀態(tài)碼)的資源文件,如果沒有;使用默認(rèn)的error頁(yè)面。

          public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
           ...
           @Override
           public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
           Map<String, Object> model) {
           //status:異常錯(cuò)誤狀態(tài)碼
           ModelAndView modelAndView=resolve(String.valueOf(status), model);
           if (modelAndView==null && SERIES_VIEWS.containsKey(status.series())) {
           modelAndView=resolve(SERIES_VIEWS.get(status.series()), model);
           }
           return modelAndView;
           }
           private ModelAndView resolve(String viewName, Map<String, Object> model) {
           //視圖名稱,默認(rèn)是error/+“status”錯(cuò)誤狀態(tài)碼;比如:error/500、error/404
           String errorViewName="error/" + viewName;
           TemplateAvailabilityProvider provider=this.templateAvailabilityProviders
           .getProvider(errorViewName, this.applicationContext);
           if (provider !=null) {
           return new ModelAndView(errorViewName, model);
           }
           return resolveResource(errorViewName, model);
           }
           //在資源文件中查找error/500或error/404等頁(yè)面
           private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
           for (String location : this.resourceProperties.getStaticLocations()) {
           try {
           Resource resource=this.applicationContext.getResource(location);
           resource=resource.createRelative(viewName + ".html");
           if (resource.exists()) {
           return new ModelAndView(new HtmlResourceView(resource), model);
           }
           }
           catch (Exception ex) {
           }
           }
           return null;
           }
           ...
          }
          

          BasicErrorController根據(jù)Accept頭的內(nèi)容,輸出不同格式的錯(cuò)誤響應(yīng)。比如針對(duì)瀏覽器的請(qǐng)求生成html頁(yè)面,針對(duì)其它請(qǐng)求生成json格式的返回。

          可以通過(guò)配置error/HttpStatus頁(yè)面實(shí)現(xiàn)自定義錯(cuò)誤頁(yè)面。

          1. ErrorPageCustomizer類
          /**
           * {@link EmbeddedServletContainerCustomizer} that configures the container's error
           * pages.
           */
          private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
           private final ServerProperties properties;
           protected ErrorPageCustomizer(ServerProperties properties) {
           this.properties=properties;
           }
           @Override
           public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
           ErrorPage errorPage=new ErrorPage(this.properties.getServletPrefix()
           + this.properties.getError().getPath());
           errorPageRegistry.addErrorPages(errorPage);
           }
           @Override
           public int getOrder() {
           return 0;
           }
          }
          

          將錯(cuò)誤頁(yè)面注冊(cè)到內(nèi)嵌的tomcat的servlet容器中。

          1. PreserveErrorControllerTargetClassPostProcessor實(shí)現(xiàn)BeanFactoryPostProcessor接口,可以修改BEAN的配置信息

          ErrorAutoConfiguration內(nèi)的兩個(gè)配置

          //2個(gè)config配置
          @Configuration
          static class DefaultErrorViewResolverConfiguration {
           private final ApplicationContext applicationContext;
           private final ResourceProperties resourceProperties;
           DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext,
           ResourceProperties resourceProperties) {
           this.applicationContext=applicationContext;
           this.resourceProperties=resourceProperties;
           }
           @Bean
           @ConditionalOnBean(DispatcherServlet.class)
           @ConditionalOnMissingBean
           public DefaultErrorViewResolver conventionErrorViewResolver() {
           return new DefaultErrorViewResolver(this.applicationContext,
           this.resourceProperties);
           }
          }
          @Configuration
          @ConditionalOnProperty(prefix="server.error.whitelabel", name="enabled", matchIfMissing=true)
          @Conditional(ErrorTemplateMissingCondition.class)
          protected static class WhitelabelErrorViewConfiguration {
           private final SpelView defaultErrorView=new SpelView(
           "<html><body><h1>Whitelabel Error Page</h1>"
           + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
           + "<div id='created'>${timestamp}</div>"
           + "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
           + "<div>${message}</div></body></html>");
           @Bean(name="error")
           @ConditionalOnMissingBean(name="error")
           public View defaultErrorView() {
           return this.defaultErrorView;
           }
           // If the user adds @EnableWebMvc then the bean name view resolver from
           // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
           @Bean
           @ConditionalOnMissingBean(BeanNameViewResolver.class)
           public BeanNameViewResolver beanNameViewResolver() {
           BeanNameViewResolver resolver=new BeanNameViewResolver();
           resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
           return resolver;
           }
          }
          
          1. DefaultErrorViewResolverConfiguration:默認(rèn)的error視圖解析配置;
          2. WhitelabelErrorViewConfiguration:默認(rèn)設(shè)置了/error的頁(yè)面,和Whitelabel Error Page頁(yè)面響應(yīng)內(nèi)容。

          如果Spring MVC在處理業(yè)務(wù)的過(guò)程中拋出異常,會(huì)被 Servlet 容器捕捉到,Servlet 容器再將請(qǐng)求轉(zhuǎn)發(fā)給注冊(cè)好的異常處理映射 /error 做響應(yīng)處理。

          springboot配置文件默認(rèn)error相關(guān)配置

          springboot配置文件application.properties中關(guān)于error默認(rèn)配置:

          server.error.include-stacktrace=never # When to include a "stacktrace" attribute.
          server.error.path=/error # Path of the error controller.
          server.error.whitelabel.enabled=true # Enable the default error page displayed in browsers in case of a server error.
          

          springboot 自定義異常處理

          通過(guò)跟蹤springboot對(duì)異常處理得源碼跟蹤,根據(jù)業(yè)務(wù)需要,可以細(xì)分前端響應(yīng)的錯(cuò)誤頁(yè)面,也可以統(tǒng)一使用/error頁(yè)面+錯(cuò)誤提示信息進(jìn)行處理。

          根據(jù)自己的需求自定義異常處理機(jī)制;具體可實(shí)施的操作如下:

          1. 可以通過(guò)配置error/HttpStatus(錯(cuò)誤狀態(tài)碼)頁(yè)面實(shí)現(xiàn)自定義錯(cuò)誤頁(yè)面【底層實(shí)現(xiàn),詳見:BasicErrorController源碼】;
          2. 可以實(shí)現(xiàn)BasicErrorController,自定義普通請(qǐng)求的異常頁(yè)面響應(yīng)信息和異步請(qǐng)求的響應(yīng)信息,統(tǒng)一使用/error頁(yè)面進(jìn)行錯(cuò)誤響應(yīng)提示;
          3. 自定義實(shí)現(xiàn)ErrorAttributes接口,覆蓋DefaultErrorAttributes實(shí)現(xiàn),或是繼承DefaultErrorAttributes類,重寫里面的方法【TODO,不推薦】。

          1和2的方法可單獨(dú)使用,也可以結(jié)合使用。

          自定義異常頁(yè)面

          可以根據(jù)不同的錯(cuò)誤狀態(tài)碼,在前端細(xì)分不同的響應(yīng)界面給用戶進(jìn)行提示;資源路徑必須是:靜態(tài)資源路徑下/error/HttpStats(比如:/error/404等)

          1. 自定義異常頁(yè)面
          <!DOCTYPE html>
          <html lang="en">
          <head>
           <meta charset="UTF-8"></meta>
           <title>404友情提示</title>
          </head>
          <body>
          <h1>訪問的資源未找到(404)</h1>
          </body>
          </html>
          

          404.html

          500.html等,這里只演示404。

          統(tǒng)一異常處理

          普通請(qǐng)求,前端使用error頁(yè)面+自定義錯(cuò)誤響應(yīng)信息;

          其他請(qǐng)求(異步),統(tǒng)一自定義錯(cuò)誤響應(yīng)信息,規(guī)范處理異步響應(yīng)的錯(cuò)誤判斷和處理。

          使用springMVC注解ControllerAdvice

          /**
           * 
           * @項(xiàng)目名稱:wyait-manage
           * @類名稱:GlobalExceptionHandler
           * @類描述:統(tǒng)一異常處理,包括【普通調(diào)用和ajax調(diào)用】
           * </br>ControllerAdvice來(lái)做controller內(nèi)部的全局異常處理,但對(duì)于未進(jìn)入controller前的異常,該處理方法是無(wú)法進(jìn)行捕獲處理的,SpringBoot提供了ErrorController的處理類來(lái)處理所有的異常(TODO)。
           * </br>1.當(dāng)普通調(diào)用時(shí),跳轉(zhuǎn)到自定義的錯(cuò)誤頁(yè)面;2.當(dāng)ajax調(diào)用時(shí),可返回約定的json數(shù)據(jù)對(duì)象,方便頁(yè)面統(tǒng)一處理。
           * @創(chuàng)建人:wyait
           * @創(chuàng)建時(shí)間:2018年5月22日 上午11:44:55 
           * @version:
           */
          @ControllerAdvice
          public class GlobalExceptionHandler {
           private static final Logger logger=LoggerFactory
           .getLogger(GlobalExceptionHandler.class);
           public static final String DEFAULT_ERROR_VIEW="error";
           /**
           * 
           * @描述:針對(duì)普通請(qǐng)求和ajax異步請(qǐng)求的異常進(jìn)行處理
           * @創(chuàng)建人:wyait
           * @創(chuàng)建時(shí)間:2018年5月22日 下午4:48:58
           * @param req
           * @param e
           * @return
           * @throws Exception
           */
           @ExceptionHandler(value=Exception.class)
           @ResponseBody
           public ModelAndView errorHandler(HttpServletRequest request,
           HttpServletResponse response, Exception e) {
           logger.debug(getClass().getName() + ".errorHandler】統(tǒng)一異常處理:request="+request);
           ModelAndView mv=new ModelAndView();
           logger.info(getClass().getName() + ".errorHandler】統(tǒng)一異常處理:"+e.getMessage());
           //1 獲取錯(cuò)誤狀態(tài)碼
           HttpStatus httpStatus=getStatus(request);
           logger.info(getClass().getName() + ".errorHandler】統(tǒng)一異常處理!錯(cuò)誤狀態(tài)碼httpStatus:"+httpStatus);
           //2 返回錯(cuò)誤提示
           ExceptionEnum ee=getMessage(httpStatus);
           //3 將錯(cuò)誤信息放入mv中
           mv.addObject("type", ee.getType());
           mv.addObject("code", ee.getCode());
           mv.addObject("msg", ee.getMsg());
           if(!ShiroFilterUtils.isAjax(request)){
           //不是異步請(qǐng)求
           mv.setViewName(DEFAULT_ERROR_VIEW);
           logger.debug(getClass().getName() + ".errorHandler】統(tǒng)一異常處理:普通請(qǐng)求。");
           }
           logger.debug(getClass().getName() + ".errorHandler】統(tǒng)一異常處理響應(yīng)結(jié)果:MV="+mv);
           return mv;
           }
           ...
          }
          

          運(yùn)行測(cè)試:先走GlobalExceptionHandler(使用注解@ControllerAdvice)類里面的方法,而后又執(zhí)行了BasicErrorController方法;被springboot自帶的BasicErrorController覆蓋。

          實(shí)現(xiàn)springboot的AbstractErrorController

          自定義實(shí)現(xiàn)AbstractErrorController,添加響應(yīng)的錯(cuò)誤提示信息。

          @RequestMapping(produces="text/html")
           public ModelAndView errorHtml(HttpServletRequest request,
           HttpServletResponse response) {
           ModelAndView mv=new ModelAndView(ERROR_PATH);
           /** model對(duì)象包含了異常信息 */
           Map<String, Object> model=getErrorAttributes(request,
           isIncludeStackTrace(request, MediaType.TEXT_HTML));
           // 1 獲取錯(cuò)誤狀態(tài)碼(也可以根據(jù)異常對(duì)象返回對(duì)應(yīng)的錯(cuò)誤信息)
           HttpStatus httpStatus=getStatus(request);
           // 2 返回錯(cuò)誤提示
           ExceptionEnum ee=getMessage(httpStatus);
           Result<String> result=new Result<String>(
           String.valueOf(ee.getType()), ee.getCode(), ee.getMsg());
           // 3 將錯(cuò)誤信息放入mv中
           mv.addObject("result", result);
           logger.info("統(tǒng)一異常處理【" + getClass().getName()
           + ".errorHtml】統(tǒng)一異常處理!錯(cuò)誤信息mv:" + mv);
           return mv;
           }
           @RequestMapping
           @ResponseBody
           //設(shè)置響應(yīng)狀態(tài)碼為:200,結(jié)合前端約定的規(guī)范處理。也可不設(shè)置狀態(tài)碼,前端ajax調(diào)用使用error函數(shù)進(jìn)行控制處理
           @ResponseStatus(value=HttpStatus.OK)
           public Result<String> error(HttpServletRequest request, Exception e) {
           /** model對(duì)象包含了異常信息 */
           Map<String, Object> model=getErrorAttributes(request,
           isIncludeStackTrace(request, MediaType.TEXT_HTML));
           // 1 獲取錯(cuò)誤狀態(tài)碼(也可以根據(jù)異常對(duì)象返回對(duì)應(yīng)的錯(cuò)誤信息)
           HttpStatus httpStatus=getStatus(request);
           // 2 返回錯(cuò)誤提示
           ExceptionEnum ee=getMessage(httpStatus);
           Result<String> result=new Result<String>(
           String.valueOf(ee.getType()), ee.getCode(), ee.getMsg());
           // 3 將錯(cuò)誤信息返回
          // ResponseEntity
           logger.info("統(tǒng)一異常處理【" + getClass().getName()
           + ".error】統(tǒng)一異常處理!錯(cuò)誤信息result:" + result);
           return result;
           }
          

          針對(duì)異步請(qǐng)求,統(tǒng)一指定響應(yīng)狀態(tài)碼:200;也可以不指定,前端在處理異步請(qǐng)求的時(shí)候,可以通過(guò)ajax的error函數(shù)進(jìn)行控制。

          這里是繼承的AbstractErrorController類,自定義實(shí)現(xiàn)統(tǒng)一異常處理,也可以直接實(shí)現(xiàn)ErrorController接口。

          前端ajax異步統(tǒng)一處理:

          通過(guò)約定,前端ajax異步請(qǐng)求,進(jìn)行統(tǒng)一的錯(cuò)誤處理。

          /**
           * 針對(duì)不同的錯(cuò)誤可結(jié)合業(yè)務(wù)自定義處理方式
           * @param result
           * @returns {Boolean}
           */
          function isError(result){
           var flag=true;
           if(result && result.status){
           flag=false;
           if(result.status=='-1' || result.status=='-101' || result.status=='400' || result.status=='404' || result.status=='500'){
           layer.alert(result.data);
           }else if(result.status=='403'){
           layer.alert(result.data,function(){
           //跳轉(zhuǎn)到未授權(quán)界面
           window.location.href="/403";
           });
           }
           }
           return flag;//返回true
          }
          

          使用方式:

           ...
           success:function(data){
           //異常過(guò)濾處理
           if(isError(data)){
           alert(data);
           }
           },
           ...
          

          error.html頁(yè)面:

          <!DOCTYPE html>
          <html lang="en" xmlns:th="http://www.thymeleaf.org">
          <head th:include="layout :: htmlhead" th:with="title='wyait后臺(tái)管理'">
           <meta charset="UTF-8"></meta>
           <title th:text="${result.status}"></title>
          </head>
          <body>
          <h1>出錯(cuò)了</h1>
          <p><span th:text="${result.message}"></span>(<span th:text="${result.data}"></span>)</p>
          </body>
          </html>
          

          測(cè)試效果

          普通請(qǐng)求:

          異步請(qǐng)求:

          線上get請(qǐng)求亂碼

          問題描述

          前臺(tái)通過(guò)html頁(yè)面,發(fā)送請(qǐng)求到后臺(tái)查詢數(shù)據(jù),在日志中打印的sql語(yǔ)句顯示傳入的參數(shù)亂碼:

           SELECT ... 
          [2018-05-11 09:15:00.582][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:==> Parameters: 1(Integer), ???è′o(String)
          [2018-05-11 09:15:00.585][http-bio-8280-exec-2][DEBUG][org.apache.ibatis.logging.jdbc.BaseJdbcLogger][159]:<==Total: 1
          ...
          

          本地windows開發(fā)環(huán)境測(cè)試沒有亂碼問題;

          請(qǐng)求信息

          前端頁(yè)面發(fā)送get請(qǐng)求,瀏覽器默認(rèn)對(duì)get請(qǐng)求路徑進(jìn)行URL編碼處理。

          后臺(tái)Controller打印的日志

          分頁(yè)查詢用戶列表!搜索條件:userSearch:UserSearchDTO{page=1, limit=10, uname='??????', umobile='', insertTimeStart='', insertTimeEnd=''},page:1,每頁(yè)記錄數(shù)量limit:10,請(qǐng)求編碼:UTF-8
          

          Controller層在接收到這個(gè)uname參數(shù)時(shí),已經(jīng)是亂碼,ISO-8859-1解碼后的結(jié)果。

          請(qǐng)求參數(shù)編碼流程

          1. 前端頁(yè)面發(fā)送get請(qǐng)求,瀏覽器默認(rèn)在中文的UTF-8后加上上%得到URL編碼,比如:%e8%b4%b9%e7...;
          2. get請(qǐng)求到tomcat應(yīng)用服務(wù)器后,會(huì)以默認(rèn)的ISO-8859-1進(jìn)行解碼;
          3. 在controller中,接收到的是經(jīng)過(guò)URL編碼和iso-8859-1解碼后的參數(shù)值。

          具體編碼細(xì)節(jié):TODO

          解決方案

          項(xiàng)目編碼配置【可以不配置】

          開發(fā)前,默認(rèn)必須統(tǒng)一編碼環(huán)境;正常都是設(shè)置為utf-8。

          spring boot 與spring mvc不同,在web應(yīng)用中,spring boot默認(rèn)的編碼格式為UTF-8,而spring mvc的默認(rèn)編碼格式為iso-8859-1。

          spring boot項(xiàng)目中如果沒有特殊需求,該編碼不需要修改。如果要強(qiáng)制其他編碼格式,spring boot提供了設(shè)置方式:

          1. 通過(guò)application.properties配置文件設(shè)置:
          # 默認(rèn)utf-8配置
          spring.http.encoding.force=true
          spring.http.encoding.charset=UTF-8
          spring.http.encoding.enabled=true
          server.tomcat.uri-encoding=UTF-8
          

          此時(shí)攔截器中返回的中文已經(jīng)不亂碼了,但是controller中返回的數(shù)據(jù)可能會(huì)依舊亂碼。

          1. 參考spring MVC的方式,自定義實(shí)現(xiàn)WebMvcConfigurerAdapter類,處理響應(yīng)數(shù)據(jù)亂碼問題:
          @Bean
          public HttpMessageConverter<String> responseBodyConverter() {
           StringHttpMessageConverter converter=new StringHttpMessageConverter(Charset.forName("UTF-8"));
           return converter;
          }
          @Override
          public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
           super.configureMessageConverters(converters);
           converters.add(responseBodyConverter());
          }
          

          也可以在controller方法@RequestMapping上添加:

          produces="text/plain;charset=UTF-8"
          

          這種方法的弊端是限定了數(shù)據(jù)類型。

          亂碼解決方案

          表單采用get方式提交,中文亂碼解決方案:

          1. 改為post請(qǐng)求;
          2. 手動(dòng)編解碼:
          param=new String(param.getBytes("iso8859-1"), "utf-8");
          
          1. 修改tomcat配置server.xml文件:
          2. 找到如下代碼:
          <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
          

          在這里添加一個(gè)屬性:URIEncoding,將該屬性值設(shè)置為UTF-8,即可讓Tomcat(默認(rèn)ISO-8859-1編碼)以UTF-8的編碼處理get請(qǐng)求。

          修改完成后:

          <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />
          
          1. 發(fā)送get請(qǐng)求前,瀏覽器中兩次URL編碼:
          2. 兩次編碼兩次解碼的過(guò)程為:
          3. ==UTF-8編碼->UTF-8(iso-8859-1)編碼->iso-8859-1解碼->UTF-8解碼,編碼和解碼的過(guò)程是對(duì)稱的,所以不會(huì)出現(xiàn)亂碼。==
           //js代碼
           param=encodeURI(param);
           // alert("第一次URL編碼:" + param);
           param=encodeURI(param);
           // alert("第二次URL編碼:" + param);
          

          后臺(tái)代碼:

          //兩次解碼
          URLDecoder.decode(URLDecoder.decode(param,"utf-8"),"utf-8");
          

          總結(jié)

          以上四種解決方案,可結(jié)合具體情況進(jìn)行使用。

          no session異常

          異常日志1:

          [2018-05-21 18:00:51.574][http-nio-8280-exec-6][DEBUG][org.apache.shiro.web.servlet.SimpleCookie][389]:Found 'SHRIOSESSIONID' cookie value [fc6b7b64-6c59-4f82-853b-e2ca20135b99]
          [2018-05-21 18:00:51.575][http-nio-8280-exec-6][DEBUG][org.apache.shiro.mgt.DefaultSecurityManager][447]:Resolved SubjectContext context session is invalid. Ignoring and creating an anonymous (session-less) Subject instance.
          org.apache.shiro.session.UnknownSessionException: There is no session with id [fc6b7b64-6c59-4f82-853b-e2ca20135b99]
           at org.apache.shiro.session.mgt.eis.AbstractSessionDAO.readSession(AbstractSessionDAO.java:170) ~[shiro-all-1.3.1.jar:1.3.1]
          

          異常日志2【偶爾出現(xiàn)】:

          Caused by: javax.crypto.BadPaddingException: Given final block not properly padded
           at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:811) ~[sunjce_provider.jar:1.7.0_85]
          

          UnknownSessionException

          UnknownSessionException: There is no session with id [...]

          原因

          結(jié)合項(xiàng)目配置,分析問題原因:

          1,用戶退出后,瀏覽器中的SHIROSESSIONID依然存在;

          2,再次發(fā)送請(qǐng)求時(shí),攜帶SHIROSESSIONID,會(huì)在shiro的DefaultWebSecurityManager.getSessionKey(context)中,逐層跟蹤對(duì)應(yīng)在sessionManager中session值,沒有的話,最終在AbstractSessionDAO.readSession(sessionID)中拋出異常。

          解決方案

          1. 在程序中退出的地方,清除cookie:
          //刪除cookie
          Cookie co=new Cookie("username", "");
          co.setMaxAge(0);// 設(shè)置立即過(guò)期
          co.setPath("/");// 根目錄,整個(gè)網(wǎng)站有效
          servletResponse.addCookie(co);
          
          1. 設(shè)置SimpleCookie的過(guò)期時(shí)間,和session、ehcache緩存時(shí)間保持一致;
          @Bean
          public SimpleCookie sessionIdCookie() {
           //DefaultSecurityManager
           SimpleCookie simpleCookie=new SimpleCookie();
           //如果在Cookie中設(shè)置了"HttpOnly"屬性,那么通過(guò)程序(JS腳本、Applet等)將無(wú)法讀取到Cookie信息,這樣能防止XSS×××。
           simpleCookie.setHttpOnly(true);
           simpleCookie.setName("SHRIOSESSIONID");
           simpleCookie.setMaxAge(86400000*3);
           return simpleCookie;
          }
          
          1. 手動(dòng)實(shí)現(xiàn)shiro的logout方法,清除瀏覽器cookie;
          2. 重寫AbstractSessionDAO.readSession方法,如果session為null,清空瀏覽器cookie;
          3. 不做處理;實(shí)際項(xiàng)目運(yùn)行中,不影響功能執(zhí)行。

          源碼

          源碼已集成到項(xiàng)目中:

          github源碼: https://github.com/wyait/manage.git

          碼云:https://gitee.com/wyait/manage.git

          github對(duì)應(yīng)項(xiàng)目源碼目錄:wyait-manage-1.2.0

          碼云對(duì)應(yīng)項(xiàng)目源碼目錄:wyait-manage-1.2.0

          轉(zhuǎn)載:https://blog.51cto.com/wyait/2125708

          作者:wyait



          .基本信息。

          [查看開源項(xiàng)目](https://gitee.com/yadong.zhang/DBlog)

          | 工具 | 版本或描述 |

          | ----- | -------------------- |

          | OS | Windows 10 |

          | JDK | 1.8+ |

          | IDE | eclipse|

          | Maven | 3.3.1 |

          | MySQL | 5.6.4 |

          #### 模塊劃分

          | 模塊 | 釋義 |

          | ---------- | ----------------------- |

          | shiro-core | 核心業(yè)務(wù)類模塊,提供基本的數(shù)據(jù)操作、工具處理等 |

          | shiro-admin | 后臺(tái)管理模塊 |


          使用說(shuō)明

          1. 使用IDE導(dǎo)入本項(xiàng)目

          2. 新建數(shù)據(jù)庫(kù)`CREATE DATABASE shiro;`

          3. 導(dǎo)入數(shù)據(jù)庫(kù)`docs/db/shiro.sql`

          4. 修改(`resources/application.yml`)配置文件

          1. 數(shù)據(jù)庫(kù)鏈接屬性(可搜索`datasource`或定位到L.19)

          2. redis配置(可搜索`redis`或定位到L.69)

          5. 運(yùn)行項(xiàng)目(三種方式)

          1. 項(xiàng)目根目錄下執(zhí)行`mvn -X clean package -Dmaven.test.skip=true`編譯打包,然后執(zhí)行`java -jar shiro-admin/target/shiro-admin.jar`

          2. 項(xiàng)目根目錄下執(zhí)行`mvn springboot:run`

          3. 直接運(yùn)行`ShiroAdminApplication.java`

          6. 瀏覽器訪問`http://127.0.0.1:8080`

          **用戶密碼**

          _超級(jí)管理員_: 賬號(hào):root 密碼:123456

          _普通管理員_: 賬號(hào):admin 密碼:123456

          **Druid監(jiān)控**

          _鏈接_: `http://127.0.0.1:8080/druid/index.html`

          用戶名:zyd-druid 密碼:zyd-druid

          2.安裝數(shù)據(jù)表:shiro.sql;

          1. sys_resources


          '

          2.sys_role


          .

          3.sys_role_resources



          .

          4.sys_user


          .

          5.sys_user_role


          .

          6.eclipse導(dǎo)入成功后頁(yè)面


          /

          7.類對(duì)象,mapper對(duì)象,xml關(guān)系如圖


          8.打開application.yml,注意。redis,mysql,啟動(dòng)服務(wù)。注意端口;




          ……


          ……



          搭建完成!!!!


          主站蜘蛛池模板: 国产一区二区在线视频| 精品动漫一区二区无遮挡| 国产丝袜无码一区二区三区视频| 国产一区二区精品| 精品人妻无码一区二区三区蜜桃一| 97久久精品午夜一区二区| 丰满爆乳无码一区二区三区| 亚洲国产精品第一区二区| 亚洲国产精品成人一区| 综合一区自拍亚洲综合图区| 亚洲色偷精品一区二区三区| 亚洲AV美女一区二区三区| 丝袜人妻一区二区三区| 高清一区二区在线观看| 99久久综合狠狠综合久久一区| 亚洲无码一区二区三区| 亚洲一区二区三区播放在线| 国产在线第一区二区三区| 97一区二区三区四区久久| 日韩免费无码视频一区二区三区| 日韩精品无码视频一区二区蜜桃 | 国产精品538一区二区在线| 精品国产一区二区三区久久久狼| 人妻无码视频一区二区三区| 精品乱人伦一区二区三区| 国产成人精品一区二区三区免费| 国产亚洲福利精品一区| 无码日韩精品一区二区三区免费| 久久国产午夜一区二区福利| 亚洲国产老鸭窝一区二区三区| 色窝窝无码一区二区三区色欲| 亚洲一区二区三区日本久久九| 亚洲AV色香蕉一区二区| 日本一区二区在线播放| 最新欧美精品一区二区三区| 成人精品一区二区电影| 国产精品视频一区二区猎奇| 国精品无码一区二区三区在线蜜臀| 一区二区三区精密机械| 亚洲免费一区二区| 视频一区在线播放|