篇文章我們來(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 是一個(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 能做什么呢?
等等——都集成到一個(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 是一個(gè)全面的、蘊(yùn)含豐富功能的安全框架。下圖為描述 Shiro 功能的框架圖:
Authentication(認(rèn)證), Authorization(授權(quán)), Session Management(會(huì)話管理), Cryptography(加密)被 Shiro 框架的開發(fā)團(tuán)隊(duì)稱之為應(yīng)用安全的四大基石。那么就讓我們來(lái)看看它們吧:
還有其他的功能來(lái)支持和加強(qiáng)這些不同應(yīng)用環(huán)境下安全領(lǐng)域的關(guān)注點(diǎn)。特別是對(duì)以下的功能支持:
注意: Shiro 不會(huì)去維護(hù)用戶、維護(hù)權(quán)限,這些需要我們自己去設(shè)計(jì)/提供,然后通過(guò)相應(yīng)的接口注入給 Shiro
在概念層,Shiro 架構(gòu)包含三個(gè)主要的理念:Subject,SecurityManager和 Realm。下面的圖展示了這些組件如何相互作用,我們將在下面依次對(duì)其進(jìn)行描述。
我們需要實(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è)資源文件等。
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è)試:
除過(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 是基于角色的訪問控制(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);
首先要配置的是 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ō)明:
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
登錄認(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í)行以下操作:
而在我們的應(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)了大家直接看代碼。
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)更新用戶信息
基于前篇,新增功能:
源碼已集成到項(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權(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通過(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; }
場(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è)異常:普通異常和異步異常!
<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。
[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>。
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)的類:
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(); }
@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類:
debug跟蹤源碼:即DispatcherServlet在doDispatch過(guò)程中有異常拋出時(shí):
一. 先由HandlerExceptionResolver.resolveException解析異常并保存在request中;
二. 再DefaultErrorAttributes.getErrorAttributes處理;DefaultErrorAttributes在處理過(guò)程中,從request中獲取錯(cuò)誤信息,將錯(cuò)誤信息保存到RequestAttributes中;
三. 最后在獲取錯(cuò)誤信息getError(RequestAttributes)時(shí),從RequestAttributes中取到錯(cuò)誤信息。
@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è)面。
/** * {@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容器中。
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; } }
如果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和2的方法可單獨(dú)使用,也可以結(jié)合使用。
自定義異常頁(yè)面
可以根據(jù)不同的錯(cuò)誤狀態(tài)碼,在前端細(xì)分不同的響應(yīng)界面給用戶進(jìn)行提示;資源路徑必須是:靜態(tài)資源路徑下/error/HttpStats(比如:/error/404等)
<!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ù)編碼流程
具體編碼細(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è)置方式:
# 默認(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ì)依舊亂碼。
@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方式提交,中文亂碼解決方案:
param=new String(param.getBytes("iso8859-1"), "utf-8");
<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" />
//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é)合具體情況進(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)中拋出異常。
解決方案
//刪除cookie Cookie co=new Cookie("username", ""); co.setMaxAge(0);// 設(shè)置立即過(guò)期 co.setPath("/");// 根目錄,整個(gè)網(wǎng)站有效 servletResponse.addCookie(co);
@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; }
源碼
源碼已集成到項(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;
'
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ù)。注意端口;
。
……
……
。
搭建完成!!!!
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。