shiro的简单使用

shiro的话,比spring security轻量一点,与平台无关。当然功能没有security那么强大,但足以应付一般应用的安全框架需求。下面整理一下shiro在springboot中的用法。

0、基本概念

0-1.关于shiro

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。

——取自百度百科

如图,主要分为三个核心组件:Subject, SecurityManager 和 Realms.

Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。

Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。

SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

0-2.关于RBAC

RBAC是Role-BasedAccess Control的英文缩写,意思是基于角色的访问控制。 深层的理论概念东西咱也看不懂,也不想看,来个图直接一点:

详细的话,可以参考这篇文章

1、准备工作

1-1.引入依赖

创建springboot项目,本次用到的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>

<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>

<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

</dependencies>
1-2.创建表

共5个:用户表、角色表、权限表、用户角色表、角色权限表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'id',
`url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '地址',
`describe` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES (1, '/user', 'user:user', '2021-08-09 18:00:43', '2021-08-09 18:00:43');
INSERT INTO `permission` VALUES (2, '/user/add', 'user:add', '2021-08-09 18:00:43', '2021-08-09 18:00:43');
INSERT INTO `permission` VALUES (3, '/user/delete', 'user:delete', '2021-08-09 18:00:44', '2021-08-09 18:00:44');

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '名称',
`describe` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'admin', '超级管理员', '2021-08-09 18:00:05', '2021-08-09 18:00:05');
INSERT INTO `role` VALUES (2, 'test', '测试账户', '2021-08-09 18:00:13', '2021-08-09 18:00:13');

-- ----------------------------
-- Table structure for role_permission
-- ----------------------------
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`role_id` int(0) NULL DEFAULT NULL COMMENT '角色id',
`permission_id` int(0) NULL DEFAULT NULL COMMENT '权限id',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role_permission
-- ----------------------------
INSERT INTO `role_permission` VALUES (1, 2, '2021-08-09 17:53:06', '2021-08-09 17:53:06');
INSERT INTO `role_permission` VALUES (1, 3, '2021-08-09 17:53:20', '2021-08-09 17:53:20');
INSERT INTO `role_permission` VALUES (2, 1, '2021-08-09 17:53:20', '2021-08-09 17:53:20');
INSERT INTO `role_permission` VALUES (1, 1, '2021-08-09 17:53:20', '2021-08-09 17:53:20');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(0) NOT NULL AUTO_INCREMENT COMMENT 'id',
`username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'test', 'e3453f2ed6c27a4a73d0382632360779', 1, '2021-08-09 17:43:06', '2021-08-09 17:43:06');
INSERT INTO `user` VALUES (2, 'lizxing', '642bfab367e65630f679a0ce9e4696cb', 1, '2021-08-09 17:43:07', '2021-08-09 17:43:07');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` int(0) NULL DEFAULT NULL COMMENT '用户id',
`role_id` int(0) NULL DEFAULT NULL COMMENT '角色id',
`create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, '2021-08-09 17:49:39', '2021-08-09 17:49:39');
INSERT INTO `user_role` VALUES (2, 2, '2021-08-09 17:49:47', '2021-08-09 17:49:47');

SET FOREIGN_KEY_CHECKS = 1;

密码加密类在util目录下。

1-3.生成代码

mybatis-plus的CodeGenerator生成对应entity和mapper,懒人必备不再细说。

2、shiro认证

2-1.创建shiro配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录的url
shiroFilterFactoryBean.setLoginUrl("/login");
// 成功跳转
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/403");

LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();

// 不拦截
filterChainDefinitionMap.put("/", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/fonts/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/druid/**", "anon");
// logout后跳转到"/logout"这个地址
filterChainDefinitionMap.put("/logout", "logout");
// 拦截
filterChainDefinitionMap.put("/**", "authc");

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}

@Bean
public SecurityManager securityManager(){
// 配置SecurityManager,并注入shiroRealm
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
return securityManager;
}

@Bean
public ShiroRealm shiroRealm(){
return new ShiroRealm();
}
}
2-2.创建自定义Realm

继承AuthorizingRealm,重写doGetAuthenticationInfo(认证)和doGetAuthorizationInfo(授权)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* 登录认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

String userName = (String) authenticationToken.getPrincipal();
String password = new String((char[]) authenticationToken.getCredentials());

log.info("用户{}正在进行认证", userName);
// 通过用户名到数据库查询用户信息
User user = userMapper.selectByUserName(userName);

if (user == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
if (!password.equals(user.getPassword())) {
throw new IncorrectCredentialsException("用户名或密码错误!");
}
if ("0".equals(user.getStatus())) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());

log.info("用户{}认证成功", userName);
return info;
}
2-3.创建简单的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
public class UserController {

@PostMapping("/login")
@ResponseBody
public String login(String username, String password) {
// 密码MD5加密
password = MD5Utils.encrypt(username, password);
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 获取Subject对象
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return "成功," + SecurityUtils.getSubject().getPrincipal();
} catch (Exception e) {
return "失败," + e.getMessage();
}
}
}
2-4.测试结果

密码错误:

密码正确:

3、shiro授权

3-1.获取用户角色和权限

Realm重写doGetAuthorizationInfo方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 获取用户角色和权限
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
User user = (User) SecurityUtils.getSubject().getPrincipal();
String username = user.getUsername();

log.info("用户{}正在获取权限", username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

// 获取用户角色集
List<Role> roleList = roleMapper.selectByUsername(username);
Set<String> roleSet = new HashSet<String>();
for (Role r : roleList) {
roleSet.add(r.getName());
}
simpleAuthorizationInfo.setRoles(roleSet);

// 获取用户权限集
List<Permission> permissionList = permissionMapper.selectByUsername(username);
Set<String> permissionSet = new HashSet<String>();
for (Permission p : permissionList) {
permissionSet.add(p.getDescribe());
}
simpleAuthorizationInfo.setStringPermissions(permissionSet);

log.info("用户{}获取权限成功", username);
return simpleAuthorizationInfo;
}

对应mapper的查询:

1
2
3
4
5
6
7
8
9
@Select("SELECT " +
" r.id,r.NAME,r.describe " +
"FROM " +
" role r " +
" LEFT JOIN user_role ur ON ( r.id = ur.role_id ) " +
" LEFT JOIN user u ON ( u.id = ur.user_id ) " +
"WHERE " +
" u.username = #{username}")
List<Role> selectByUsername(String username);
1
2
3
4
5
6
7
8
9
10
@Select("SELECT p.id,p.url,p.describe  " +
"FROM " +
" role r " +
" LEFT JOIN user_role ur ON ( r.id = ur.role_id ) " +
" LEFT JOIN user u ON ( u.id = ur.user_id ) " +
" LEFT JOIN role_permission rp ON ( rp.role_id = r.id ) " +
" LEFT JOIN permission p ON ( p.id = rp.permission_id ) " +
"WHERE " +
" u.username = #{username}")
List<Permission> selectByUsername(String username);
3-2.开启权限注解

在ShiroConfig配置类加上:

1
2
3
4
5
6
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}

还需要引入aop依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3-3.权限不足处理

新增全局异常处理:

1
2
3
4
5
6
7
8
9
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(UnauthorizedException.class)
@ResponseBody
public String defaultExceptionHandler(HttpServletRequest req, Exception e){
return "对不起,你没有访问权限!";
}
}
3-4.创建简单的测试接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RequiresPermissions("user:user")
@GetMapping("user")
public String userList() {
return "获取用户列表";
}

@RequiresPermissions("user:add")
@PostMapping("user/add")
public String userAdd(User user) {
return "新增用户" + user;
}

@RequiresPermissions("user:delete")
@PostMapping("user/delete")
public String userDelete(User user) {
return "删除用户" + user;
}
3-5.测试结果

首先登录测试账号,测试账号根据表的配置只有user:user权限。

访问user:

访问user/add:

换上admin账号:

除了根据权限,还能根据角色进行控制:

1
2
3
4
5
// 表示当前Subject需要角色admin和user。  
@RequiresRoles(value={"admin", "user"}, logical= Logical.AND)

// 表示当前Subject需要权限user:a或user:b。
@RequiresPermissions (value={"user:a", "user:b"}, logical= Logical.OR)

4、shiro缓存

我们发现,调用user/add和user/delete时,每次都会去数据库获取角色权限信息:

利用缓存,可以减少不必要的数据库访问。以下使用redis做缓存。

4-1.引入依赖
1
2
3
4
5
6
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>

yml配置:

1
2
3
4
5
6
7
8
9
10
redis:
host: localhost
port: 6379
database: 0
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
timeout: 2000ms
4-2.配置redis

ShiroConfig加入redisManager(),cacheManager()并注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
return redisManager;
}

public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}

@Bean
public SecurityManager securityManager(){
// 配置SecurityManager
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 注入shiroRealm
securityManager.setRealm(shiroRealm());
// 注入cacheManager
securityManager.setCacheManager(cacheManager());
return securityManager;
}
4-3.测试结果

通过控制台或连接池工具,观察到此时只查了一次数据库,在redis里缓存了用户权限信息。

本次demo源码