Spring Boot 整合 Shiro 实现登录认证与权限控制

Posted by 彭超 on 2019-06-14
Estimated Reading Time 14 Minutes
Words 2.7k In Total
Viewed Times

用户角色权限数据库设计

数据库这里以 MySQL 为例

创建数据库

所需表如下:

  • user:用户表
  • role:角色表
  • perm:权限菜单表
  • user_role:用户与角色关联的中间表
  • role_prem:角色与权限菜单关联的中间表

执行数据库脚本

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
/*
Navicat Premium Data Transfer

Source Server : 127.0.0.1
Source Server Type : MySQL
Source Server Version : 50718
Source Host : 127.0.0.1:3306
Source Schema : shiro

Target Server Type : MySQL
Target Server Version : 50718
File Encoding : 65001
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for perm
-- ----------------------------
DROP TABLE IF EXISTS `perm`;
CREATE TABLE `perm` (
`perm_id` int(32) NOT NULL COMMENT '权限主键',
`perm_url` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限url',
`perm_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '权限描述',
PRIMARY KEY (`perm_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of perm
-- ----------------------------
INSERT INTO `perm` VALUES (1, '/user/*', '拥有对用户的所有操作权限');

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`role_id` int(32) NOT NULL COMMENT '角色主键',
`role_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色名',
`role_description` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, '超级管理员', '超级管理员');

-- ----------------------------
-- Table structure for role_perm
-- ----------------------------
DROP TABLE IF EXISTS `role_perm`;
CREATE TABLE `role_perm` (
`role_id` int(32) NOT NULL COMMENT '角色主键',
`perm_id` int(32) DEFAULT NULL COMMENT '权限主键'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role_perm
-- ----------------------------
INSERT INTO `role_perm` VALUES (1, 1);

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` int(32) NOT NULL COMMENT '用户主键',
`username` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL COMMENT '密码(存储加密后的密码)',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '5dbc683c53b7f317fa45c05bf9499fdd');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` int(32) NOT NULL COMMENT '用户主键',
`role_id` int(32) NOT NULL COMMENT '角色主键'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1);

SET FOREIGN_KEY_CHECKS = 1;

数据库设计完成以后,将相对应的实体类和 mapper 文件加入到项目当中

业务代码

这里我们需要定义一个业务接口查询用户的相关信息(包括用户关联的角色与权限)

这里不阐述具体的 SQL 语句

UserService

1
2
3
4
5
6
7
8
9
public interface UserService {

/**
* 根据用户名查询用户信息(包含角色及权限信息)
* @param username 用户名
* @return User
*/
User selectByUsername(String username);
}

UserServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserMapper userMapper;

@Override
public User selectByUsername(String username) {

return userMapper.selectByUsername(username);
}
}

引入依赖

pox.xml 中添加 org.apache.shiro:shiro-springcom.github.theborakompanioni:thymeleaf-extras-shiro 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<properties>
<thymeleaf-extras-shiro.version>2.0.0</thymeleaf-extras-shiro.version>
<shiro.version>1.4.0</shiro.version>
</properties>

<dependencies>
<!-- Shiro核心依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>

<!-- Thymeleaf对Shiro的支持 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>${thymeleaf-extras-shiro.version}</version>
</dependency>
</dependencies>

自定义认证和授权

创建 MyRealm 类实现认证与授权

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
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;

/**
* 自定义Realm,实现授权与认证
*/
public class MyRealm extends AuthorizingRealm {

@Autowired
private UserService userService;

/**
* 用户认证
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.selectByUsername(token.getUsername());
if (user == null) {
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}

/**
* 用户授权
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();
if (user != null) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<String> roles = new LinkedList<>();
List<String> perms = new LinkedList<>();
for (Role role : user.getRoleList()) {
roles.add(role.getRoleName());
}
for (Perm perm : user.getPermList()) {
perms.add(perm.getPermUrl());
}
simpleAuthorizationInfo.addRoles(roles);
simpleAuthorizationInfo.addStringPermissions(perms);
return simpleAuthorizationInfo;
}
return null;
}
}

Shiro 配置类

创建 ShiroConfig 配置类

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;

@Configuration
public class ShiroConfig {

/**
* 配置密码加密
*/
@Bean("hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {

HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
// 散列算法(加密)
credentialsMatcher.setHashAlgorithmName("MD5");
// 散列次数(加密次数)
credentialsMatcher.setHashIterations(1);
// storedCredentialsHexEncoded 默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}

/**
* 注入自定义的 Realm
*/
@Bean("MyRealm")
public MyRealm MyRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher matcher) {

MyRealm MyRealm = new MyRealm();
MyRealm.setCredentialsMatcher(matcher);
return MyRealm;
}

/**
* 配置自定义权限过滤规则
*/
@Bean
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {

ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setSuccessUrl("/index.html");
bean.setLoginUrl("/login.html");
bean.setUnauthorizedUrl("/unauthorized.html");

/**
* anon:匿名用户可访问
* authc:认证用户可访问
* user:使用rememberMe可访问
* perms:对应权限可访问
* role:对应角色权限可访问
**/
Map<String, String> filterMap = new LinkedHashMap<>();
/**
* 允许匿名访问静态资源
*/
filterMap.put("/image/**", "anon");
filterMap.put("/css/**", "anon");
filterMap.put("/js/**", "anon");
filterMap.put("/plugin/**", "anon");
/**
* 允许匿名访问登录页面和登录操作
*/
filterMap.put("/login.html", "anon");
filterMap.put("/login.do", "anon");
/**
* 其它所有请求需要登录认证后才能访问
*/
filterMap.put("/**", "authc");
bean.setFilterChainDefinitionMap(filterMap);
return bean;
}

/**
* 注入 securityManager
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(HashedCredentialsMatcher hashedCredentialsMatcher, @Qualifier("sessionManager") DefaultWebSessionManager defaultWebSessionManager) {

DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(MyRealm(hashedCredentialsMatcher));
securityManager.setSessionManager(defaultWebSessionManager);
return securityManager;
}

/**
* 开启权限注解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {

AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {

DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}

/**
* 配置异常跳转页面
*/
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {

SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
// 未认证跳转页面(跳转路径为项目里的页面相对路径,并非 URL)
properties.setProperty("org.apache.shiro.authz.UnauthenticatedException", "login");
// 权限不足跳转页面
properties.setProperty("org.apache.shiro.authz.UnauthorizedException", "unauthorized");
resolver.setExceptionMappings(properties);
return resolver;
}

/**
* 会话管理器
*/
@Bean("sessionManager")
public DefaultWebSessionManager defaultWebSessionManager() {

DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
// 设置用户登录信息失效时间为一天(单位:ms)
defaultWebSessionManager.setGlobalSessionTimeout(1000L * 60L * 60L * 24L);
return defaultWebSessionManager;
}

/**
* 重置 ShiroDialect,省略此步将不能在 Thymeleaf 页面使用 Shiro 标签
*/
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
}

Controller

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
100
101
102
@Controller
public class IndexController {

@Autowired
private UserService userService;

@RequestMapping(value = "login.html")
public String loginView() {

// 判断当前用户是否通过认证
if (SecurityUtils.getSubject().isAuthenticated()) {
// 认证通过,重定向到首页
return "redirect:index.html";
} else {
// 未认证或认证失败,转发到登录页
return "login";
}
}

@RequestMapping(value = "login.do")
@ResponseBody
public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
try {
// 执行认证
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
return AppReturn.defeated("账号不存在");
} catch (IncorrectCredentialsException e) {
return AppReturn.defeated("密码错误");
}
return AppReturn.succeed("登录成功");
}

@RequestMapping(value = "index.html")
public String indexView() {
return "index";
}

@RequestMapping(value = "logout.do")
public String logoutDo() {

if (SecurityUtils.getSubject().isAuthenticated()) {
// 退出
SecurityUtils.getSubject().logout();
}
return "redirect:login.html";
}

@RequestMapping(value = "unauthorized.html")
public String unauthorizedView() {

return "unauthorized";
}
}
@Controller
public class IndexController {

@Autowired
private UserService userService;

@RequestMapping(value = "login.html")
public String loginView() {

// 判断当前用户是否通过认证
if (SecurityUtils.getSubject().isAuthenticated()) {
// 认证通过,重定向到首页
return "redirect:index.html";
} else {
// 未认证或认证失败,转发到登录页
return "login";
}
}

@RequestMapping(value = "login.do")
@ResponseBody
public AppReturn loginDo(@RequestParam String username, @RequestParam String password) {
return userService.loginDo(username, password);
}

@RequestMapping(value = "index.html")
public String indexView() {
return "index";
}

@RequestMapping(value = "logout.do")
public String logoutDo() {

if (SecurityUtils.getSubject().isAuthenticated()) {
// 退出
SecurityUtils.getSubject().logout();
}
return "redirect:login.html";
}

@RequestMapping(value = "unauthorized.html")
public String unauthorizedView() {

return "unauthorized";
}
}

Web 页面

引入 jquery.js

login.html

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
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<div>
用户名:<input id="username" name="username" type="text" /><br/>
密码:<input id="password" name="password" type="password"><br/>
<span id="tip" class="tip"></span><br/>
<button onclick="login()">点击登录</button>
</div>
</body>

<script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>
<script type="text/javascript">
function login() {
var username = $('#username').val()
var password = $('#password').val()
$.ajax({
url: '/login.do'
, data: {
username: username
, password: password
}
, type: 'post'
, dataType: 'json'
, success: function(res) {
if (res.code == 200) {
// 登录成功,跳转到 index.html
window.location.href = '/index.html'
} else {
// 登录失败,提示登录错误信息
$("#tip").text(res.msg)
}
}
, error: function() {
$("#tip").text('服务器响应失败')
}
})
}
</script>
</html>

index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
Hello Shiro
<a href="/logout.do">退出</a>
</body>
</html>

unauthorized.html

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>无权访问</title>
</head>
<body>
权限不足
</body>
</html>

Java 中使用 Shiro 权限注解

除了在 ShiroConfig 配置类中自定义权限过滤规则,还可以使用 Shiro 提供的注解实现权限过滤,在 Controller 中的每个请求方法上可以添加以下注解实现权限控制:

@RequiresAuthentication: 只有认证通过的用户才能访问

@RequiresRoles(value = {“root”}, logical = Logical.OR)

  • value:指定拥有 root 角色才能访问,角色可以是多个,以逗号隔开
  • logical:该属性有两个值,Logical.OR(只要拥有其中一个角色就能访问),Logical.AND(需要拥有指定的全部角色才能访问,否则会抛出权限不足异常)

@RequiresPermissions(value = {“/user/delete”}, logical = Logical.OR)

  • value:指定拥有 /user/delete 权限才能访问,权限可以是多个,以逗号隔开
  • logical:有两个值,Logical.OR(只要拥有其中一个权限就访问),Logical.AND(需要拥有指定的全部权限才能访问,否则会抛出权限不足异常)

Thymeleaf 模板中使用 Shiro 权限标签

修改 thymeleaf 模板的 html 标签,加入 xmlns:shiro=”http://www.pollix.at/thymeleaf/shiro 命名空间:

1
2
3
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

常用的 Shiro 标签有以下:

  • <shiro:hasRole=”root”>:需要拥有root角色
  • <shiro:hasAnyRoles="root,guest">:需要拥有root和guest中的任意一个角色
  • <shiro:hasAllRoles ="root,guest">:需要同时拥有root和guest角色
  • <shiro:hasPerm>:原理同上
  • <shiro:hasAnyPerms>:原理同上
  • <shiro:hasAllPerms> :原理同上

登录


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !