篇文章我们来学习如何使用 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,启动服务。注意端口;
。
……
……
。
搭建完成!!!!
*请认真填写需求信息,我们会在24小时内与您取得联系。