Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
篇文章我們來學習如何使用 Spring Boot 集成 Apache Shiro 。安全應該是互聯網公司的一道生命線,幾乎任何的公司都會涉及到這方面的需求。在 Java 領域一般有 Spring Security、 Apache Shiro 等安全框架,但是由于 Spring Security 過于龐大和復雜,大多數公司會選擇 Apache Shiro 來使用,這篇文章會先介紹一下 Apache Shiro ,在結合 Spring Boot 給出使用案例。
Apache Shiro 是一個功能強大、靈活的,開源的安全框架。它可以干凈利落地處理身份驗證、授權、企業會話管理和加密。
Apache Shiro 的首要目標是易于使用和理解。安全通常很復雜,甚至讓人感到很痛苦,但是 Shiro 卻不是這樣子的。一個好的安全框架應該屏蔽復雜性,向外暴露簡單、直觀的 API,來簡化開發人員實現應用程序安全所花費的時間和精力。
Shiro 能做什么呢?
等等——都集成到一個有凝聚力的易于使用的 API。
Shiro 致力在所有應用環境下實現上述功能,小到命令行應用程序,大到企業應用中,而且不需要借助第三方框架、容器、應用服務器等。當然 Shiro 的目的是盡量的融入到這樣的應用環境中去,但也可以在它們之外的任何環境下開箱即用。
Apache Shiro 是一個全面的、蘊含豐富功能的安全框架。下圖為描述 Shiro 功能的框架圖:
Authentication(認證), Authorization(授權), Session Management(會話管理), Cryptography(加密)被 Shiro 框架的開發團隊稱之為應用安全的四大基石。那么就讓我們來看看它們吧:
還有其他的功能來支持和加強這些不同應用環境下安全領域的關注點。特別是對以下的功能支持:
注意: Shiro 不會去維護用戶、維護權限,這些需要我們自己去設計/提供,然后通過相應的接口注入給 Shiro
在概念層,Shiro 架構包含三個主要的理念:Subject,SecurityManager和 Realm。下面的圖展示了這些組件如何相互作用,我們將在下面依次對其進行描述。
我們需要實現Realms的Authentication 和 Authorization。其中 Authentication 是用來驗證用戶身份,Authorization 是授權訪問控制,用于對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。
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>
重點是 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的校驗
頁面
我們新建了六個頁面用來測試:
除過登錄頁面其它都很簡單,大概如下:
<!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 中,權限與角色相關聯,用戶通過成為適當角色的成員而得到這些角色的權限。這就極大地簡化了權限的管理。這樣管理都是層級相互依賴的,權限賦予給角色,而把角色又賦予用戶,這樣的權限設計很清楚,管理起來很方便。
采用 Jpa 技術來自動生成基礎表格,對應的實體如下:
用戶信息
@Entity
public class UserInfo implements Serializable {
@Id
@GeneratedValue
private Integer uid;
@Column(unique=true)
private String username;//帳號
private String name;//名稱(昵稱或者真實姓名,不同系統不同定義)
private String password; //密碼;
private String salt;//加密密碼的鹽
private byte state;//用戶狀態,0:創建未認證(比如沒有激活,沒有輸入驗證碼等等)--等待驗證的用戶 , 1:正常狀態,2:用戶被鎖定.
@ManyToMany(fetch=FetchType.EAGER)//立即從數據庫中進行加載數據;
@JoinTable(name="SysUserRole", joinColumns={ @JoinColumn(name="uid") }, inverseJoinColumns={@JoinColumn(name="roleId") })
private List<SysRole> roleList;// 一個用戶具有多個角色
// 省略 get set 方法
}
角色信息
@Entity
public class SysRole {
@Id@GeneratedValue
private Integer id; // 編號
private String role; // 角色標識程序中判斷使用,如"admin",這個是唯一的:
private String description; // 角色描述,UI界面顯示使用
private Boolean available=Boolean.FALSE; // 是否可用,如果不可用將不會添加給用戶
//角色 -- 權限關系:多對多關系;
@ManyToMany(fetch=FetchType.EAGER)
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="permissionId")})
private List<SysPermission> permissions;
// 用戶 - 角色關系定義;
@ManyToMany
@JoinTable(name="SysUserRole",joinColumns={@JoinColumn(name="roleId")},inverseJoinColumns={@JoinColumn(name="uid")})
private List<UserInfo> userInfos;// 一個角色對應多個用戶
// 省略 get set 方法
}
權限信息
@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; //權限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
private Long parentId; //父編號
private String parentIds; //父編號列表
private Boolean available=Boolean.FALSE;
@ManyToMany
@JoinTable(name="SysRolePermission",joinColumns={@JoinColumn(name="permissionId")},inverseJoinColumns={@JoinColumn(name="roleId")})
private List<SysRole> roles;
// 省略 get set 方法
}
根據以上的代碼會自動生成 user_info(用戶信息表)、sys_role(角色表)、sys_permission(權限表)、sys_user_role(用戶角色表)、sys_role_permission(角色權限表)這五張表,為了方便測試我們給這五張表插入一些初始化數據:
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會員','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 核心通過 Filter 來實現,就好像 SpringMvc 通過 DispachServlet 來主控制一樣。 既然是使用 Filter 一般也就能猜到,是通過 URL 規則來進行過濾和權限校驗,所以我們需要定義一系列關于 URL 的規則和訪問權限。
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>();
// 配置不會被攔截的鏈接 順序判斷
filterChainDefinitionMap.put("/static/**", "anon");
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/logout", "logout");
//<!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心代碼就不好使了;
//<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->
filterChainDefinitionMap.put("/**", "authc");
// 如果不設置默認會自動尋找Web工程根目錄下的"/login.jsp"頁面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登錄成功后要跳轉的鏈接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授權界面;
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 定義說明:
Shiro 內置的 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
登錄認證實現
在認證、授權內部實現機制中都有提到,最終處理都將交給Real進行處理。因為在 Shiro 中,最終是通過 Realm 來獲取應用程序中的用戶、角色及權限信息的。通常情況下,在 Realm 中會直接從我們的數據源中獲取 Shiro 需要的驗證信息。可以說,Realm 是專用于安全框架的 DAO. Shiro 的認證過程最終會交由 Realm 執行,這時會調用 Realm 的getAuthenticationInfo(token)方法。
該方法主要執行以下操作:
而在我們的應用程序中要做的就是自定義一個 Realm 類,繼承AuthorizingRealm 抽象類,重載 doGetAuthenticationInfo(),重寫獲取用戶信息的方法。
doGetAuthenticationInfo 的重寫
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
//獲取用戶的輸入的賬號.
String username=(String)token.getPrincipal();
System.out.println(token.getCredentials());
//通過username從數據庫中查找 User對象,如果找到,沒找到.
//實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重復執行該方法
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;
}
鏈接權限的實現
Shiro 的權限授權是通過繼承AuthorizingRealm抽象類,重載doGetAuthorizationInfo();當訪問到頁面的時候,鏈接配置了相應的權限或者 Shiro 標簽才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有權限的控制的話,那么這個方法可以不進行實現,直接返回 null 即可。在這個方法中主要是使用類:SimpleAuthorizationInfo進行角色的添加和權限的添加。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("權限配置-->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;
}
當然也可以添加 set 集合:roles 是從數據庫查詢的當前用戶的角色,stringPermissions 是從數據庫查詢的當前用戶對應的權限
authorizationInfo.setRoles(roles);
authorizationInfo.setStringPermissions(stringPermissions);
就是說如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限才可以訪問,如果在shiro配置文件中添加了filterChainDefinitionMap.put(“/add”, “roles[100002],perms[權限添加]”);就說明訪問/add這個鏈接必須要有“權限添加”這個權限和具有“100002”這個角色才可以訪問。
登錄實現
登錄過程其實只是處理異常的相關信息,具體的登錄驗證交給 Shiro 來處理
@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 -- > 賬號不存在:");
msg="UnknownAccountException -- > 賬號不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("IncorrectCredentialsException -- > 密碼不正確:");
msg="IncorrectCredentialsException -- > 密碼不正確:";
} else if ("kaptchaValidateFailed".equals(exception)) {
System.out.println("kaptchaValidateFailed -- > 驗證碼錯誤");
msg="kaptchaValidateFailed -- > 驗證碼錯誤";
} else {
msg="else >> "+exception;
System.out.println("else -- >" + exception);
}
}
map.put("msg", msg);
// 此方法不處理登錄成功,由shiro進行處理
return "/login";
}
其它 Dao 層和 Service 的代碼就不貼出來了大家直接看代碼。
1、編寫好后就可以啟動程序,訪問http://localhost:8080/userInfo/userList頁面,由于沒有登錄就會跳轉到http://localhost:8080/login頁面。登錄之后就會跳轉到 index 頁面,登錄后,直接在瀏覽器中輸入http://localhost:8080/userInfo/userList訪問就會看到用戶信息。上面這些操作時候觸發MyShiroRealm.doGetAuthenticationInfo()這個方法,也就是登錄認證的方法。
2、登錄admin賬戶,訪問:http://127.0.0.1:8080/userInfo/userAdd顯示用戶添加界面,訪問http://127.0.0.1:8080/userInfo/userDel顯示403沒有權限。上面這些操作時候觸發MyShiroRealm.doGetAuthorizationInfo()這個方面,也就是權限校驗的方法。
3、修改 admin不 同的權限進行測試
Shiro 很強大,這僅僅是完成了登錄認證和權限管理這兩個功能,更多內容以后有時間再做探討。
后臺權限管理系統
相關:
spring boot + mybatis + layui + shiro后臺權限管理系統
springboot + shiro之登錄人數限制、登錄判斷重定向、session時間設置
springboot + shiro 動態更新用戶信息
基于前篇,新增功能:
源碼已集成到項目中:
github源碼: https://github.com/wyait/manage.git
碼云:https://gitee.com/wyait/manage.git
github對應項目源碼目錄:wyait-manage-1.2.0
碼云對應項目源碼目錄:wyait-manage-1.2.0
shiro權限注解
Shiro 提供了相應的注解用于權限控制,如果使用這些注解就需要使用AOP 的功能來進行判斷,如Spring AOP;Shiro 提供了Spring AOP 集成用于權限注解的解析和驗證。
@RequiresAuthentication 表示當前Subject已經通過login 進行了身份驗證;即Subject.isAuthenticated()返回true。 @RequiresUser 表示當前Subject已經身份驗證或者通過記住我登錄的。 @RequiresGuest 表示當前Subject沒有身份驗證或通過記住我登錄過,即是游客身份。 @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND) @RequiresRoles(value={“admin”}) @RequiresRoles({“admin“}) 表示當前Subject需要角色admin 和user。 @RequiresPermissions (value={“user:a”, “user:b”}, logical=Logical.OR) 表示當前Subject需要權限user:a或user:b。
Shiro的認證注解處理是有內定的處理順序的,如果有多個注解的話,前面的通過了會繼續檢查后面的,若不通過則直接返回,處理順序依次為(與實際聲明順序無關):
RequiresRoles RequiresPermissions RequiresAuthentication RequiresUser RequiresGuest
以上注解既可以用在controller中,也可以用在service中使用;
建議將shiro注解放在controller中,因為如果service層使用了spring的事物注解,那么shiro注解將無效。
shiro權限注解要生效,必須配置springAOP通過設置shiro的SecurityManager進行權限驗證。
/** * * @描述:開啟Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類,并在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可實現此功能 * </br>Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor(保證實現了Shiro內部lifecycle函數的bean執行) has run * </br>不使用注解的話,可以注釋掉這兩個配置 * @創建人:wyait * @創建時間: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; }
場景:當用戶正常訪問網站時,因為某種原因后端出現exception的時候,直接暴露異常信息或頁面顯示給用戶;
這種操作體驗不是我們想要的。所以要對異常進行統一管理,能提高用戶體驗的同時,后臺能詳細定位到異常的問題點。
springboot異常概況
Spring Boot提供了默認的統一錯誤頁面,這是Spring MVC沒有提供的。在理解了Spring Boot提供的錯誤處理相關內容之后,我們可以方便的定義自己的錯誤返回的格式和內容。
編寫by zero異常
在home頁面,手動創建兩個異常:普通異常和異步異常!
<p> 普通請求異常: <a href="/error/getError">點擊</a> </p> <p> ajax異步請求異常: <a href="javascript:void(0)" onclick="ajaxError()">點擊</a> </p> ... //js代碼 function ajaxError(){ $.get("/error/ajaxError",function(data){ layer.alert(data); }); }
/** * * @描述:普通請求異常 * @創建人:wyait * @創建時間:2018年5月24日 下午5:30:50 */ @RequestMapping("getError") public void toError(){ System.out.println(1/0); } /** * * @描述:異步異常 * @創建人:wyait * @創建時間:2018年5月24日 下午5:30:39 */ @RequestMapping("ajaxError") @ResponseBody public String ajaxError(){ System.out.println(1/0); return "異步請求成功!"; }
異常效果
console錯誤信息:
[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' ...
通過日志可知,springboot返回的錯誤頁面,是通過: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 ...
通過日志可知,springboot返回的錯誤信息,是通過:org.springframework.boot.autoconfigure.web.BasicErrorController.error處理返回ResponseEntity<String,Object>。
springboot異常處理解析
查看org.springframework.boot.autoconfigure.web包下面的類,跟蹤springboot對error異常處理機制。自動配置通過一個MVC error控制器處理錯誤
通過spring-boot-autoconfigure引入
查看springboot 處理error的類
springboot的自動配置,在web中處理error相關的自動配置類:ErrorMvcAutoConfiguration。查看與處理error相關的類:
ErrorAutoConfiguration類源碼//TODO
ErrorAutoConfiguration注冊的bean
//4個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過程中有異常拋出時:
一. 先由HandlerExceptionResolver.resolveException解析異常并保存在request中;
二. 再DefaultErrorAttributes.getErrorAttributes處理;DefaultErrorAttributes在處理過程中,從request中獲取錯誤信息,將錯誤信息保存到RequestAttributes中;
三. 最后在獲取錯誤信息getError(RequestAttributes)時,從RequestAttributes中取到錯誤信息。
@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/“錯誤狀態碼”;的資源):
如果不是異常請求,會執行resolveErrorView方法;該方法會先在默認或配置的靜態資源路徑下查找error/HttpStatus(錯誤狀態碼)的資源文件,如果沒有;使用默認的error頁面。
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered { ... @Override public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) { //status:異常錯誤狀態碼 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) { //視圖名稱,默認是error/+“status”錯誤狀態碼;比如: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等頁面 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根據Accept頭的內容,輸出不同格式的錯誤響應。比如針對瀏覽器的請求生成html頁面,針對其它請求生成json格式的返回。
可以通過配置error/HttpStatus頁面實現自定義錯誤頁面。
/** * {@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; } }
將錯誤頁面注冊到內嵌的tomcat的servlet容器中。
ErrorAutoConfiguration內的兩個配置
//2個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在處理業務的過程中拋出異常,會被 Servlet 容器捕捉到,Servlet 容器再將請求轉發給注冊好的異常處理映射 /error 做響應處理。
springboot配置文件默認error相關配置
springboot配置文件application.properties中關于error默認配置:
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 自定義異常處理
通過跟蹤springboot對異常處理得源碼跟蹤,根據業務需要,可以細分前端響應的錯誤頁面,也可以統一使用/error頁面+錯誤提示信息進行處理。
根據自己的需求自定義異常處理機制;具體可實施的操作如下:
1和2的方法可單獨使用,也可以結合使用。
自定義異常頁面
可以根據不同的錯誤狀態碼,在前端細分不同的響應界面給用戶進行提示;資源路徑必須是:靜態資源路徑下/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。
統一異常處理
普通請求,前端使用error頁面+自定義錯誤響應信息;
其他請求(異步),統一自定義錯誤響應信息,規范處理異步響應的錯誤判斷和處理。
使用springMVC注解ControllerAdvice
/** * * @項目名稱:wyait-manage * @類名稱:GlobalExceptionHandler * @類描述:統一異常處理,包括【普通調用和ajax調用】 * </br>ControllerAdvice來做controller內部的全局異常處理,但對于未進入controller前的異常,該處理方法是無法進行捕獲處理的,SpringBoot提供了ErrorController的處理類來處理所有的異常(TODO)。 * </br>1.當普通調用時,跳轉到自定義的錯誤頁面;2.當ajax調用時,可返回約定的json數據對象,方便頁面統一處理。 * @創建人:wyait * @創建時間: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"; /** * * @描述:針對普通請求和ajax異步請求的異常進行處理 * @創建人:wyait * @創建時間: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】統一異常處理:request="+request); ModelAndView mv=new ModelAndView(); logger.info(getClass().getName() + ".errorHandler】統一異常處理:"+e.getMessage()); //1 獲取錯誤狀態碼 HttpStatus httpStatus=getStatus(request); logger.info(getClass().getName() + ".errorHandler】統一異常處理!錯誤狀態碼httpStatus:"+httpStatus); //2 返回錯誤提示 ExceptionEnum ee=getMessage(httpStatus); //3 將錯誤信息放入mv中 mv.addObject("type", ee.getType()); mv.addObject("code", ee.getCode()); mv.addObject("msg", ee.getMsg()); if(!ShiroFilterUtils.isAjax(request)){ //不是異步請求 mv.setViewName(DEFAULT_ERROR_VIEW); logger.debug(getClass().getName() + ".errorHandler】統一異常處理:普通請求。"); } logger.debug(getClass().getName() + ".errorHandler】統一異常處理響應結果:MV="+mv); return mv; } ... }
運行測試:先走GlobalExceptionHandler(使用注解@ControllerAdvice)類里面的方法,而后又執行了BasicErrorController方法;被springboot自帶的BasicErrorController覆蓋。
實現springboot的AbstractErrorController
自定義實現AbstractErrorController,添加響應的錯誤提示信息。
@RequestMapping(produces="text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { ModelAndView mv=new ModelAndView(ERROR_PATH); /** model對象包含了異常信息 */ Map<String, Object> model=getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)); // 1 獲取錯誤狀態碼(也可以根據異常對象返回對應的錯誤信息) HttpStatus httpStatus=getStatus(request); // 2 返回錯誤提示 ExceptionEnum ee=getMessage(httpStatus); Result<String> result=new Result<String>( String.valueOf(ee.getType()), ee.getCode(), ee.getMsg()); // 3 將錯誤信息放入mv中 mv.addObject("result", result); logger.info("統一異常處理【" + getClass().getName() + ".errorHtml】統一異常處理!錯誤信息mv:" + mv); return mv; } @RequestMapping @ResponseBody //設置響應狀態碼為:200,結合前端約定的規范處理。也可不設置狀態碼,前端ajax調用使用error函數進行控制處理 @ResponseStatus(value=HttpStatus.OK) public Result<String> error(HttpServletRequest request, Exception e) { /** model對象包含了異常信息 */ Map<String, Object> model=getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)); // 1 獲取錯誤狀態碼(也可以根據異常對象返回對應的錯誤信息) HttpStatus httpStatus=getStatus(request); // 2 返回錯誤提示 ExceptionEnum ee=getMessage(httpStatus); Result<String> result=new Result<String>( String.valueOf(ee.getType()), ee.getCode(), ee.getMsg()); // 3 將錯誤信息返回 // ResponseEntity logger.info("統一異常處理【" + getClass().getName() + ".error】統一異常處理!錯誤信息result:" + result); return result; }
針對異步請求,統一指定響應狀態碼:200;也可以不指定,前端在處理異步請求的時候,可以通過ajax的error函數進行控制。
這里是繼承的AbstractErrorController類,自定義實現統一異常處理,也可以直接實現ErrorController接口。
前端ajax異步統一處理:
通過約定,前端ajax異步請求,進行統一的錯誤處理。
/** * 針對不同的錯誤可結合業務自定義處理方式 * @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(){ //跳轉到未授權界面 window.location.href="/403"; }); } } return flag;//返回true }
使用方式:
... success:function(data){ //異常過濾處理 if(isError(data)){ alert(data); } }, ...
error.html頁面:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head th:include="layout :: htmlhead" th:with="title='wyait后臺管理'"> <meta charset="UTF-8"></meta> <title th:text="${result.status}"></title> </head> <body> <h1>出錯了</h1> <p><span th:text="${result.message}"></span>(<span th:text="${result.data}"></span>)</p> </body> </html>
測試效果
普通請求:
異步請求:
線上get請求亂碼
問題描述
前臺通過html頁面,發送請求到后臺查詢數據,在日志中打印的sql語句顯示傳入的參數亂碼:
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開發環境測試沒有亂碼問題;
請求信息
前端頁面發送get請求,瀏覽器默認對get請求路徑進行URL編碼處理。
后臺Controller打印的日志
分頁查詢用戶列表!搜索條件:userSearch:UserSearchDTO{page=1, limit=10, uname='??????', umobile='', insertTimeStart='', insertTimeEnd=''},page:1,每頁記錄數量limit:10,請求編碼:UTF-8
Controller層在接收到這個uname參數時,已經是亂碼,ISO-8859-1解碼后的結果。
請求參數編碼流程
具體編碼細節:TODO
解決方案
項目編碼配置【可以不配置】
開發前,默認必須統一編碼環境;正常都是設置為utf-8。
spring boot 與spring mvc不同,在web應用中,spring boot默認的編碼格式為UTF-8,而spring mvc的默認編碼格式為iso-8859-1。
spring boot項目中如果沒有特殊需求,該編碼不需要修改。如果要強制其他編碼格式,spring boot提供了設置方式:
# 默認utf-8配置 spring.http.encoding.force=true spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true server.tomcat.uri-encoding=UTF-8
此時攔截器中返回的中文已經不亂碼了,但是controller中返回的數據可能會依舊亂碼。
@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"
這種方法的弊端是限定了數據類型。
亂碼解決方案
表單采用get方式提交,中文亂碼解決方案:
param=new String(param.getBytes("iso8859-1"), "utf-8");
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
在這里添加一個屬性:URIEncoding,將該屬性值設置為UTF-8,即可讓Tomcat(默認ISO-8859-1編碼)以UTF-8的編碼處理get請求。
修改完成后:
<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);
后臺代碼:
//兩次解碼 URLDecoder.decode(URLDecoder.decode(param,"utf-8"),"utf-8");
以上四種解決方案,可結合具體情況進行使用。
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【偶爾出現】:
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 [...]
原因
結合項目配置,分析問題原因:
1,用戶退出后,瀏覽器中的SHIROSESSIONID依然存在;
2,再次發送請求時,攜帶SHIROSESSIONID,會在shiro的DefaultWebSecurityManager.getSessionKey(context)中,逐層跟蹤對應在sessionManager中session值,沒有的話,最終在AbstractSessionDAO.readSession(sessionID)中拋出異常。
解決方案
//刪除cookie Cookie co=new Cookie("username", ""); co.setMaxAge(0);// 設置立即過期 co.setPath("/");// 根目錄,整個網站有效 servletResponse.addCookie(co);
@Bean public SimpleCookie sessionIdCookie() { //DefaultSecurityManager SimpleCookie simpleCookie=new SimpleCookie(); //如果在Cookie中設置了"HttpOnly"屬性,那么通過程序(JS腳本、Applet等)將無法讀取到Cookie信息,這樣能防止XSS×××。 simpleCookie.setHttpOnly(true); simpleCookie.setName("SHRIOSESSIONID"); simpleCookie.setMaxAge(86400000*3); return simpleCookie; }
源碼
源碼已集成到項目中:
github源碼: https://github.com/wyait/manage.git
碼云:https://gitee.com/wyait/manage.git
github對應項目源碼目錄:wyait-manage-1.2.0
碼云對應項目源碼目錄:wyait-manage-1.2.0
轉載:https://blog.51cto.com/wyait/2125708
作者:wyait
.基本信息。
[查看開源項目](https://gitee.com/yadong.zhang/DBlog)
| 工具 | 版本或描述 |
| ----- | -------------------- |
| OS | Windows 10 |
| JDK | 1.8+ |
| IDE | eclipse|
| Maven | 3.3.1 |
| MySQL | 5.6.4 |
#### 模塊劃分
| 模塊 | 釋義 |
| ---------- | ----------------------- |
| shiro-core | 核心業務類模塊,提供基本的數據操作、工具處理等 |
| shiro-admin | 后臺管理模塊 |
使用說明
1. 使用IDE導入本項目
2. 新建數據庫`CREATE DATABASE shiro;`
3. 導入數據庫`docs/db/shiro.sql`
4. 修改(`resources/application.yml`)配置文件
1. 數據庫鏈接屬性(可搜索`datasource`或定位到L.19)
2. redis配置(可搜索`redis`或定位到L.69)
5. 運行項目(三種方式)
1. 項目根目錄下執行`mvn -X clean package -Dmaven.test.skip=true`編譯打包,然后執行`java -jar shiro-admin/target/shiro-admin.jar`
2. 項目根目錄下執行`mvn springboot:run`
3. 直接運行`ShiroAdminApplication.java`
6. 瀏覽器訪問`http://127.0.0.1:8080`
**用戶密碼**
_超級管理員_: 賬號:root 密碼:123456
_普通管理員_: 賬號:admin 密碼:123456
**Druid監控**
_鏈接_: `http://127.0.0.1:8080/druid/index.html`
用戶名:zyd-druid 密碼:zyd-druid
2.安裝數據表:shiro.sql;
'
2.sys_role
.
3.sys_role_resources
.
4.sys_user
.
5.sys_user_role
.
6.eclipse導入成功后頁面
/
7.類對象,mapper對象,xml關系如圖
8.打開application.yml,注意。redis,mysql,啟動服務。注意端口;
。
……
……
。
搭建完成?。。。?/p>
*請認真填寫需求信息,我們會在24小時內與您取得聯系。