Springboot集成Shiro权限管理框架
最近入职了第一份Java后端开发的工作,在正式干活之前,部门老大首先给我派了个小任务,给部门的员工进行信息登记。要求每个员工首先注册自己信息,而员工的任何删改查操作都需要登录,并且只能查询或者修改自己的信息,部门老大需要所有权限。
这个小需求我用Springboot、Mybatis、Thymeleaf、Shiro、MySql做了个小网站,连接池用的Druid,分页插件使用Pagehelper,日志使用的默认日志。
为了记录一下整个项目的过程,并且巩固一下自己的知识,在完成之后,重新建立了一个小demo,重点重现了该Web环境(Mybatis及逆向工程、Shiro权限管理框架集成)的搭建过程。
完成之后,项目的目录结构如下:
一 Springboot的web项目创建
这个就没有什么好讲的,我们用IDEA直接创建一个Springboot的Web环境即可。
application.yml文件如下:
server:
port: 80
spring:
datasource:
name: demo
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 123456
url: jdbc:mysql://localhost:3306/shiroexample?useUnicode=true&charactorEncoding=utf-8&serverTimeZone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath*:/mybatis/mapper/*Mapper.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
root: info
com.hebin.shiroexample.mapper: debug
file: log/log.log
一般Springboot的版本无需使用高版本(本项目使用2.1.14),版本太高会有插件或者工具因为未更新导致未知原因错误。插件选择图中几个,也可以一个不选在最后POM中手动添加依赖坐标,因为本文不展示Thymeleaf的使用,所以无需勾选Thymeleaf。其中Lombok插件用于简化JavaBean的开发,省略Getter/Setter/toString等方法代码,Spring Web是必须使用的,Mybatis和Mysql用于持久层。
POM文件,dependencies如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
二 Apache Shiro
关于Shiro的介绍和使用请看王诗林的这篇文章springboot整合shiro(完整版),因为我使用的也不是很熟练,我这里定性的讲一下Shiro权限验证和授权的大概原理。
首先,在一个项目中,会有用户,而用户除了用户名、密码等常规属性之外,还具有各种角色。比如论坛项目,角色有:管理员,版主,普通注册用户,游客等角色,同时应该注意到,一个用户可以同时拥有多个角色,是个一对多的关系。而一个角色会有各种权限,也是一对多的关系。例如管理员有封禁、修改用户角色、发帖、删帖等权限,而游客只有浏览权限。
因此,在Shiro框架下,至少有三个实体类:User,Role,Permission,对应有三张数据库表,同时也应该建立两张关联表,将用户与角色的关系User_Role,角色与权限Role_Permission的关系联系起来。
2.1 创建Shiro的实体类
-
在数据库中建立五张表
建表语句如下:
create table t_user ( id int auto_increment comment '用户主键ID' primary key, username varchar(10) not null comment '用户名,不能重复', nickname varchar(10) not null comment '昵称', password varchar(100) not null comment '密码', description varchar(256) null comment '用户描述', constraint t_user_username_uindex unique (username) ); create table t_role ( id int auto_increment comment '角色主键ID' primary key, role_name varchar(20) not null comment '角色名称', description varchar(50) null comment '角色描述', constraint t_role_role_name_uindex unique (role_name) ); create table t_permission ( id int auto_increment comment '权限主键ID' primary key, permission_name varchar(20) not null comment '权限名称', description varchar(30) null comment '权限描述', constraint t_permission_permission_name_uindex unique (permission_name) ); create table t_user_role ( id int auto_increment comment '用户和角色关联表主键ID' primary key, user_id int not null comment '用户ID', role_id int not null comment '角色ID' ); create table t_role_permission ( id int auto_increment comment '角色和权限关联表主键ID' primary key, role_id int not null comment '角色ID', permission_id int not null comment '权限ID' );
-
利用逆向工程生成User、Role、Permission单表的Bean和Mapper接口以及Mapper映射文件
逆向工程可以查看这篇文章:Springboot项目Web环境引入Mybatis逆向工程MBG。
在User、Role的类中添加字段,最终如下。
@Data
注解为Lombok插件注解,也可不用,但需要将后加属性添加getter/setter方法,以及toString方法。@Data public class User { private Integer id; private String username; private String nickname; private String password; private String description; // 用户对应有各种角色,后加。 private Set<Role> roles; } @Data public class Role { private Integer id; private String roleName; private String description; // 每个角色都有一些权限,后加。 private Set<Permission> permissions; } @Data public class Permission { private Integer id; private String permissionName; private String description; }
2.2 Shiro的Realm和Config
因为Shiro对外接口是Subject,外界与Shiro的交互首先交给Subject,其在内部交给SecurityManager,SecurityManager再根据相关的域(Realm)做出授权或者认证管理。
而这个域,需要进行自己的编写。
MyRealm:
package com.hebin.shiroexample.shiro;
import com.hebin.shiroexample.bean.Permission;
import com.hebin.shiroexample.bean.Role;
import com.hebin.shiroexample.bean.User;
import com.hebin.shiroexample.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import java.util.Set;
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 授权的方法,首先从系统中获取系统使用主体,从数据库中查出该主体拥有的角色信息和权限信息,并赋予给AuthorizationInfo,从而完成授权。
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1.根据主体获取PrimaryPrincipal(不一定非得是用户名,也可以是手机号,邮箱地址等能够唯一确定用户的信息)
String username = (String) principalCollection.getPrimaryPrincipal();
// 这一步先判断是否存在被授权用户。
if (principalCollection == null || StringUtils.isEmpty(username)) {
return null;
}
// 2.根据PrimaryPrincipal获取用户。
User user = userService.getUserWithRolesAndPermissionsByUsername(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
// 3.将用户具有的角色和权限信息分别添加到AuthorizationInfo中
Set<Role> roles = user.getRoles();
for (Role r : roles) {
// 添加角色
simpleAuthorizationInfo.addRole(r.getRoleName());
Set<Permission> permissions = r.getPermissions();
// 添加权限
for (Permission permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission.getPermissionName());
}
}
// 将该主体应该有的角色和权限全部添加到Shiro中返回,Controller接收到返回值后判断是否有授权放行。
return simpleAuthorizationInfo;
}
/**
* 认证的方法
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 从token中获取用户名、密码等信息
String username = (String) authenticationToken.getPrincipal();
// 如果获取不到,认证失败
if (authenticationToken == null || StringUtils.isEmpty(username)) {
return null;
}
// 根据获取到的信息,从数据库查询相关实体
User user = userService.getUserWithRolesAndPermissionsByUsername(username);
// 如果实体不存在,认证失败
if (user == null) {
return null;
}
// 将查询到的实体的相关信息交给Shiro进行判断是否通过认证。
return new SimpleAuthenticationInfo(username,user.getPassword(),getName());
}
}
上文中的service接口及其实现类
package com.hebin.shiroexample.service;
import com.hebin.shiroexample.bean.User;
public interface UserService {
User getUserWithRolesAndPermissionsByUsername(String username);
}
package com.hebin.shiroexample.service.impl;
import com.hebin.shiroexample.bean.Role;
import com.hebin.shiroexample.bean.User;
import com.hebin.shiroexample.mapper.RoleExtendMapper;
import com.hebin.shiroexample.mapper.UserExtendMapper;
import com.hebin.shiroexample.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserExtendMapper userExtendMapper;
@Autowired
private RoleExtendMapper roleExtendMapper;
@Override
public User getUserWithRolesAndPermissionsByUsername(String username) {
// 根据用户名查出用户,其中用户的Set<Role> Roles属性中不包含Role的Set<Permission> permissions属性。
User user = userExtendMapper.selectUserWithRolesByUsername(username);
// 取出用户的Roles Set,根据每个role查询出对应的Set<Permission>,将role缺失的该权限属性集合用setter方法赋予。
Set<Role> roles = user.getRoles();
// 1.循环取出role
for (Role r : roles) {
// 2.每个role取出其id
Integer roleId = r.getId();
// 3.根据roleId取出对应的permissions完好的Role
Role role = roleExtendMapper.selectRoleWithPermissionsByRoleId(roleId);
// 4.调用role的setter方法赋值permissions。
r.setPermissions(role.getPermissions());
}
// 返回User对象,该对象是完整对象,内包含全部的role和permission。
return user;
}
}
上述代码中用到的Mapper接口及其Sql映射文件:
UserExtendMapper:
package com.hebin.shiroexample.mapper;
import com.hebin.shiroexample.bean.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserExtendMapper extends UserMapper {
User selectUserWithRolesByUsername(String username);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.hebin.shiroexample.mapper.UserExtendMapper">
<resultMap id="BaseResultMap" type="com.hebin.shiroexample.bean.User">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="nickname" property="nickname" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="description" property="description" jdbcType="VARCHAR"/>
<collection property="roles" resultMap="RoleSetMap"/>
</resultMap>
<resultMap id="RoleSetMap" type="com.hebin.shiroexample.bean.Role">
<id column="rid" property="id" jdbcType="INTEGER"/>
<result column="role_name" property="roleName" jdbcType="VARCHAR"/>
<result column="rdesc" property="description" jdbcType="VARCHAR"/>
</resultMap>
<select id="selectUserWithRolesByUsername" resultMap="BaseResultMap">
select u.*,r.id rid,r.role_name,r.description rdesc
from t_user u
left join t_user_role ur on u.id=ur.user_id
left join t_role r on ur.role_id=r.id
where u.username=#{username}
</select>
</mapper>
RoleExtendMapper:
package com.hebin.shiroexample.mapper;
import com.hebin.shiroexample.bean.Role;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleExtendMapper extends RoleMapper {
Role selectRoleWithPermissionsByRoleId(Integer id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.hebin.shiroexample.mapper.RoleExtendMapper">
<resultMap id="BaseResultMap" type="com.hebin.shiroexample.bean.Role">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="role_name" property="roleName" jdbcType="VARCHAR"/>
<result column="description" property="description" jdbcType="VARCHAR"/>
<collection property="permissions" resultMap="PermissionSetMap"/>
</resultMap>
<resultMap id="PermissionSetMap" type="com.hebin.shiroexample.bean.Permission">
<id column="pid" property="id" jdbcType="INTEGER"/>
<result column="permission_name" property="permissionName" jdbcType="VARCHAR"/>
<result column="pdesc" property="description" jdbcType="VARCHAR"/>
</resultMap>
<select id="selectRoleWithPermissionsByRoleId" resultMap="BaseResultMap">
select r.*,p.id pid,p.permission_name,p.description pdesc
from t_role r
left join t_role_permission rp on r.id=rp.role_id
left join t_permission p on rp.permission_id=p.id
where r.id=#{id}
</select>
</mapper>
当完成上面的代码后,需要进行Shiro的配置了:
package com.hebin.shiroexample.config;
import com.hebin.shiroexample.shiro.MyRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class ShiroConfig {
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator creator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
// 将自定义的Realm加入到Spring容器
@Bean
public MyRealm myShiroRealm() {
return new MyRealm();
}
// 权限管理,特别需要注意的是,SecurityManager是个接口,一定要使用org.apache.shiro.mgt.SecurityManager;
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(myShiroRealm());
return defaultWebSecurityManager;
}
// filter设置过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
filterFactoryBean.setSecurityManager(securityManager);
HashMap<String, String> map = new HashMap<>();
map.put("/logout", "logout");
// authc表示需要认证才能访问,即拦截认证;anon表示无需认证也能访问,即不拦截
map.put("/admin/**", "authc");
map.put("/user/**", "authc");
map.put("/leader/**", "authc");
map.put("/visitor/**", "anon");
filterFactoryBean.setLoginUrl("/login");
filterFactoryBean.setUnauthorizedUrl("/error");
filterFactoryBean.setFilterChainDefinitionMap(map);
return filterFactoryBean;
}
// 开启Shiro的注解
@Bean
public AuthorizationAttributeSourceAdvisor advisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
三 测试
首先我们建立一个异常拦截类拦截授权或者认证异常
package com.hebin.shiroexample.exceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.naming.AuthenticationException;
@RestControllerAdvice
@Slf4j
public class MyExceptionHandler {
@ExceptionHandler(AuthorizationException.class)
public String authorizationExceptionHandler(AuthorizationException e) {
log.error("没有通过权限验证", e);
return "没有通过权限验证!";
}
@ExceptionHandler(AuthenticationException.class)
public String authenticationExceptionHandler(AuthenticationException e) {
log.error("授权异常", e);
return "授权异常!";
}
}
我们建立一些假数据:
User:
Role:
Permission:
User_Role:
Role_Permission:
建立一个Controller测试权限情况
@RestController
public class IndexController {
@RequestMapping("/index")
public String index() {
return "success!";
}
@RequiresRoles("admin")
@RequestMapping("/admin/list")
public String getUserList() {
Subject subject = SecurityUtils.getSubject();
Object principal = subject.getPrincipal();
System.out.println(principal);
return "userList";
}
@RequiresPermissions("delete")
@RequestMapping("/admin/delete")
public String deleteUser() {
return "userDelete";
}
@GetMapping("/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
return "账号和密码错误!";
} catch (AuthorizationException e) {
e.printStackTrace();
return "没有权限!";
}
return "登陆成功!";
}
}
3.1 浏览器访问测试
-
访问:http://localhost/index,无需任何权限,成功!
返回Success!
-
测试登录:
-
浏览器访问:http://localhost/login?username=aa&password=123
控制台提示认证错误。
org.apache.shiro.authc.AuthenticationException: Authentication failed for token submission [org.apache.shiro.authc.UsernamePasswordToken - aa, rememberMe=false].
浏览器访问:http://localhost/login?username=a&password=123
-
-
a用户为管理员,包含所有权限,访问:http://localhost/admin/list,成功
-
a用户为管理员,包含所有权限,访问:http://localhost/admin/delete,成功
-
我们访问:http://localhost/logout登出,对b用户登录:http://localhost/login?username=b&password=123
-
b为leader、user用户,访问:http://localhost/admin/list,失败
-
b用户不包含delete权限,访问:http://localhost/admin/delete,失败