spring boot 整合shiro
一、SHIRO的概念
shiro是一个安全框架,主要可以帮助我们解决程序开发中认证和授权的问题。基于拦截器做的权限系统,权限控制的粒度有限,为了方便各种各样的常用的权限管理需求的实现,,我们有必要使用比较好的安全框架,早期spring security 作为一个比较完善的安全框架比较火,但是spring security学习成本比较高,于是就出现了shiro安全框架,学习成本降低了很多,而且基本的功能也比较完善。
1.1、SHIRO提供的功能
1、Authentication:身份认证/登陆,验证用户是不是拥有相对应的身份;
2、Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者粒度的验证某个用户对某个资源是否具有权限;
3、Session Manager:会话管理,即用户登陆后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是Web环境的;
4、Cryptographt:加密,保护数据,如密码加密存储到数据库,而不是明文存储;
5、Web Support:Web支持,可以非常容易的继承到Web环境的;
6、Caching:缓存,比如用户登陆后,其用户信息、拥有的角色/权限不必每次去查,这样提高效率;
7、Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
8、Testing:提供测试支持;
9、Run As:允许一个用户假装另一个用户(如果我们允许)的身份进行访问;
10、Remember Me:记住我,这个是非常常见的功能,即一次登陆后,下次再来的话不用登陆了。
1.2、通俗总结
既然大家都是phper转过来的,所以我想大家还是知道rbac的对吧?那么通俗的点说,shiro就是封装好的rbac的项目。还是不懂么?那么大家做CMS后台的时候,总知道管理员管理,角色管理,角色用户关系~~这些东西的吧?对,没错,这玩意就是搞这个东西的。
二、开干
废话不多说,直接干!!!!!
2.1、基础数据源
注意:sql中的 “e10adc3949ba59abbe56e057f20f883e” 是通过123456进行md5得出来的字符串
-- ----------------------------
-- Table structure for shiro_permission
-- ----------------------------
DROP TABLE IF EXISTS `shiro_permission`;
CREATE TABLE `shiro_permission` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='权限表';
-- ----------------------------
-- Table structure for shiro_role
-- ----------------------------
DROP TABLE IF EXISTS `shiro_role`;
CREATE TABLE `shiro_role` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色表';
-- ----------------------------
-- Table structure for shiro_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `shiro_role_permission`;
CREATE TABLE `shiro_role_permission` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`role_id` varchar(255) DEFAULT NULL,
`permission_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='角色与权限多对多表';
-- ----------------------------
-- Table structure for shiro_user
-- ----------------------------
DROP TABLE IF EXISTS `shiro_user`;
CREATE TABLE `shiro_user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`) USING BTREE COMMENT '用户名不可以重复的唯一索引'
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='用户表';
-- ----------------------------
-- Table structure for shiro_user_role
-- ----------------------------
DROP TABLE IF EXISTS `shiro_user_role`;
CREATE TABLE `shiro_user_role` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` varchar(255) DEFAULT NULL,
`role_id` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户与角色多对多表';
insert into `shiro_user` (`id`, `name`, `password`) values('1','admin','e10adc3949ba59abbe56e057f20f883e');
insert into `shiro_user` (`id`, `name`, `password`) values('2','vip','e10adc3949ba59abbe56e057f20f883e');
insert into `shiro_user` (`id`, `name`, `password`) values('3','svip','e10adc3949ba59abbe56e057f20f883e');
insert into `shiro_role` (`id`, `name`) values('1','user');
insert into `shiro_role` (`id`, `name`) values('2','vip');
insert into `shiro_role` (`id`, `name`) values('3','svip');
insert into `shiro_permission` (`id`, `name`, `url`) values('1','user','user');
insert into `shiro_permission` (`id`, `name`, `url`) values('2','vip','vip');
insert into `shiro_permission` (`id`, `name`, `url`) values('3','svip','svip');
insert into `shiro_user_role` (`id`, `user_id`, `role_id`) values('1','1','1');
insert into `shiro_user_role` (`id`, `user_id`, `role_id`) values('3','2','2');
insert into `shiro_user_role` (`id`, `user_id`, `role_id`) values('6','3','3');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('1','1','1');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('2','2','1');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('3','2','2');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('4','3','1');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('5','3','2');
insert into `shiro_role_permission` (`id`, `role_id`, `permission_id`) values('6','3','3');
三、引入依赖
<!-- 导入shiro和spring继承的jar包 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
四、生成实体类
参考:phper转java记录篇-spring boot 整合easy-code.
4.1 要注意的问题:
1、如果你是一气呵成的做完easycode相关的所有操作,你不会感知到,但是当你如果修改了数据库的字段,你会发现有的时候字段变更,重新生成的文件好像有缓存的感觉,这时候你需要考虑项目根目录下**.idea的文件,进去找到对应的json删掉**
2、生成的文件目录是可以去修改的,比如我的dao和entity是在db目录下的,默认是在项目根目录下的,设置方式如下:
五、实现类
参考文章:springboot整合shiro+mybatis+mysql
springboot与shiro和mybatis和mysql
5.1 ApiShiroRealm类的实现
注:代码中queryAllByUserId,queryByUserName,这些方法可能需要自行补充一下,因为easycode生成不出来。
List<ShiroRole> queryAllByUserId(Integer userId);
ShiroUser queryByUserName(String userName);
具体实现,大家自行实现,不在赘述了
================================================
整理的代码如下:
/**
* 自定义Realm 继承AuthorizingRealm 重写 AuthorizationInfo(授权) 和 AuthenticationInfo(认证)这两个
* @author leoxie
*/
public class ApiShiroRealm extends AuthorizingRealm {
@Resource
private ShiroRolePermissionService shiroRolePermissionService;
@Resource
private ShiroUserRoleService shiroUserRoleService;
@Resource
private ShiroUserService shiroUserService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限配置[doGetAuthorizationInfo]-->"+this.getClass().getName());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
ShiroUser shiroUser = (ShiroUser)principals.getPrimaryPrincipal();
Integer userId = shiroUser.getId();
//用户对应的角色列表
List<ShiroRole> userRoleList = shiroUserRoleService.queryAllByUserId(userId);
for(ShiroRole role:userRoleList){
authorizationInfo.addRole(role.getName());
//获取当前角色对应的节点权限
List<ShiroPermission> userRolePermission = shiroRolePermissionService.queryAllByRoleId(role.getId());
for(ShiroPermission permission:userRolePermission){
authorizationInfo.addStringPermission(permission.getUrl());
}
}
return authorizationInfo;
}
/**
* 主要是用来进行身份认证的,也就是说验证用户输入的账号和密码是否正确
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("权限配置[doGetAuthenticationInfo]-->"+this.getClass().getName());
//获取用户的输入的账号.
String username = (String)token.getPrincipal();
System.out.println("用户名:"+username);
//通过username从数据库中查找 User对象,如果找到,没找到.
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
ShiroUser shiroUser = shiroUserService.queryByUserName(username);
if(shiroUser == null){
return null;
}
System.out.println("用户密码:"+shiroUser.getPassword());
return new SimpleAuthenticationInfo(
//用户信息
shiroUser,
//密码
shiroUser.getPassword(),
//salt=ByteSource.Util.bytes(username) ps:不是固定的,自己搞一个能针对这个用户的盐就好了,当然也可以统一一模一样的盐,为了方便,我们先用空字符串,这样算法就完全是md5(xxx),不用考虑盐的作用
ByteSource.Util.bytes(""),
//realm name
getName()
);
}
}
5.2 ShiroConfig类的实现
看代码注释就好了,不需要过多的说明
/**
* @author leoxie
*/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器.
//必须是LinkedHashMap,因为要保证有序
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/submitLogin","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;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* )
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列的次数,相当于 md5("");
hashedCredentialsMatcher.setHashIterations();
return hashedCredentialsMatcher;
}
@Bean
public ApiShiroRealm myShiroRealm(){
ApiShiroRealm apiShiroRealm = new ApiShiroRealm();
apiShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return apiShiroRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
以上大家应该还是不是很理解2个问题:
1、authc/anon这两个以外还有哪些值
filterChainDefinitionMap.put("/**", "authc");
Filter Name | Class |
---|---|
anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | org.apache.shiro.web.filter.authz.PortFilter |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
ssl | org.apache.shiro.web.filter.authz.SslFilter |
user | org.apache.shiro.web.filter.authc.UserFilter |
当然要特别的设置某个action的话,就需要通过 RequiresPermissions/RequiresRoles 这两个注解来完成
如果注解无效:springboot 项目 shiro注解不生效
六、验证可用性
6.1 访问静态资源
针对上面config的配置来说,static的字段是可以不需要权限验证就可以访问了,所以我们访问一下
http://127.0.0.1:81/static/abc.txt
的确可以直接访问
遇到404了怎么办,在yml中添加如下代码
spring:
mvc:
static-path-pattern: /static/**
6.2 访问不存在的控制器/存在控制器
访问 “http://127.0.0.1:81/axkxk” 被302转发到“http://127.0.0.1:81/login”了
6.3 完善login界面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>用户登录</h1>
<hr>
<form id="from" action="/submitLogin" method="post">
<table>
<tr>
<td>用户名</td>
<td>
<input type="text" name="username" placeholder="请输入账户名" value="" th:value="${userName }"/>
</td>
</tr>
<tr>
<td>密码</td>
<td>
<input type="password" name="password" placeholder="请输入密码"/>
</td>
</tr>
<tr>
<td colspan="2">
<span style="color: red;">[[${msg }]]</span>
</td>
</tr>
<tr>
<td colspan="2">
<input type="submit" value="登录"/>
<input type="reset" value="重置"/>
</td>
</tr>
</table>
</form>
</body>
</html>
注意: 这里因为是页面,所以需要注意注解是 @Controller不要写成了 @RestController 否则页面没办法输出页面
@Controller
public class ShiroController extends Base {
@GetMapping(value="/login")
public String Login(){
//文件名(login.html直接放在/resources/templates/下面即可)
return "login";
}
}
另外记得引入 spring-boot-starter-thymeleaf 依赖,当然这块内容我们后续有需要的话,再进行研究。现在都是前后端分离了,很少说代码框架里面去使用页面,所以我们只要保证基本的可用性即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
默认的路径就是/resources/templates/下面
如果你发现了Cannot resolve MVC View,或者你发现你的html不能访问到的时候,不妨看看你的ShiroController是否使用了 @Controller注解,另外你的鼠标放在【return “login”;】这句话上面,是否出现了Cannot resolve MVC View。
Cannot resolve MVC View解决方案: IDEA开发springboot项目遇到的问题:Cannot resolve MVC View ‘XXX’
6.4 完善submitLogin方法
简单实现一下,不完善的地方,麻烦自行补充一下。
@Controller
public class ShiroController extends Base {
@GetMapping(value="/login")
public String Login(){
return "login";
}
@GetMapping(value="/index")
public String Index(){
return "index";
}
@GetMapping(value="/about")
public String About(){
return "about";
}
@PostMapping("/submitLogin")
public String login(HttpServletRequest request, HttpServletResponse response){
String userName = request.getParameter("username");
String password = request.getParameter("password");
// 等于null说明用户没有登录,只是拦截所有请求到这里,那就直接让用户去登录页面,就不认证了。
// 如果这里不处理,那个会返回用户名不存在,逻辑上不合理,用户还没登录怎么就用户名不存在?
if(null == userName || null == password) {
return "login";
}
// 1.获取Subject
Subject subject = SecurityUtils.getSubject();
// 2.封装用户数据
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
// 3.执行登录方法
request.setAttribute("userName",userName);
request.setAttribute("password",password);
try{
subject.login(token);
return "redirect:/index";
} catch (UnknownAccountException e){
// 这里是捕获自定义Realm的用户名不存在异常
request.setAttribute("msg","用户名不存在!");
} catch (IncorrectCredentialsException e){
request.setAttribute("msg","密码错误!");
} catch (AuthenticationException e) {
// 这里是捕获自定义Realm的认证失败异常
request.setAttribute("msg","认证失败!");
}
return "login";
}
}
小结: 到这里你会发现登录已经可以实现了,另外就是可能出了login页面,你要写一个index的页面和一个about页面做个试验,这里贴一下代码
about.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>关于我们</title>
</head>
<body>
<h1>欢迎来到</h1>
<hr>
关于我们
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页-001</title>
</head>
<body>
<a href="/about">关于页面</a>
</body>
</html>
但是与此同时我发现一个问题。ApiShiroRealm类里面的doGetAuthorizationInfo方法还没有用到呢?另外就是我们还有表没用到,就是角色和permission这一系列的,这时候就需要 RequiresPermissions/RequiresRoles 这两个注解,demo
/**
* 这里的vip,就是对应权限实体类Permission实体类的字段url,自定义Realm类ApiShiroRealm里是用这个字段
*/
@GetMapping(value="/about")
@RequiresPermissions(value = "vip")
public String About(){
return "about";
}
同时要配上拦截器,没有拦截器,异常抛不出来
@ControllerAdvice
public class NoPermissionException {
@ResponseBody
@ExceptionHandler(UnauthorizedException.class)
public ResultJSON handleShiroException(Exception ex) {
//自定义的记录日志方法
SlfUtils.getInst(this).error(0, "访问了无权限目录");
//自定义的返回对象
return ResultJSON.error("无权限操作");
}
@ResponseBody
@ExceptionHandler(AuthorizationException.class)
public String AuthorizationException(Exception ex) {
return "权限认证失败";
}
}
注: 一定要注意拦截器方法上面一定有2个注解:@ExceptionHandler & @ResponseBody ,我因为漏了调试了快一小时
有问题可以加个微信,共同学习成功,Leo.xie博客