avatar

目录
shiro学习笔记(全)

什么是Shiro

​ Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
​ Apache Shiro 的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架应该尽可能掩盖复杂的地方,露出一个干净而直观的 API,来简化开发人员在使他们的应用程序安全上的努力。以下是你可以用 Apache Shiro 所做的事情:

  • 验证用户来核实他们的身份
  • 对用户执行访问控制,如:
    判断用户是否被分配了一个确定的安全角色
    判断用户是否被允许做某事
  • 在任何环境下使用 Session API,即使没有 Web 或 EJB 容器
  • 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应
  • 聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”
  • 启用单点登录(SSO)功能
  • 为没有关联到登录的用户启用”Remember Me”服务

Shiro与Spring Security的对比

Shiro

Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。

  1. 易于理解的 Java Security API;
  2. 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
  3. 对角色的简单的签权(访问控制),支持细粒度的签权;
  4. 支持一级缓存,以提升应用程序的性能;
  5. 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
  6. 异构客户端会话访问;
  7. 非常简单的加密 API;
  8. 不跟任何的框架或者容器捆绑,可以独立运行

Spring Security

除了不能脱离Spring,shiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手
动实现。Spring Security的权限细粒度更高

Shiro的功能模块

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份

  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。

  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话
    中;会话可以是普通JavaSE环境的,也可以是如Web环境的

  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储

  • Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序

  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率

  • Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。

  • Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全

  • “Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用

  • “Remember Me”:记住我

Shiro内部模块

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实
现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;

SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能;

Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

在应用中使用Shiro

也就是说对于我们而言,最简单的一个Shiro应用:
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判
断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。

Shiro中的认证与授权

SpringBoot中集成Shiro

在Spring Boot中集成Shiro进行用户的认证过程主要可以归纳为以下三点:

1、定义一个ShiroConfig,然后配置SecurityManager Bean,SecurityManager为Shiro的安全管理器,管理着所有Subject;

2、在ShiroConfig中配置ShiroFilterFactoryBean,其为Shiro过滤器工厂类,依赖于SecurityManager;

3、自定义Realm实现,Realm包含doGetAuthorizationInfo()doGetAuthenticationInfo()方法,因为本文只涉及用户认证,所以只实现doGetAuthenticationInfo()方法

用户认证

引入依赖

搭建一个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

<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>

<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>

<!--druid 数据驱动-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>

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

ShiroConfiguration

定义一个Shiro配置类,命名为ShiroConfiguration

java
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
@Configuration
public class ShiroConfiguation {

/**
* 配置自定义realm
* @return
*/
@Bean
public ShiroRealm ihrmRealm(){
return new ShiroRealm();
}

/**
* 配置安全管理器
* @param realm
* @return
*/
@Bean
public SecurityManager securityManager(ShiroRealm realm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//将自定义的realm交给安全管理器统一管理
manager.setRealm(realm);
return manager;
}

/**
* Filter工厂,设置对应的过滤条件和跳转条件
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean ShiroFilter(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilter.setSecurityManager(securityManager);
//登录的url
shiroFilter.setLoginUrl("/login");
//登录成功后跳转的url
shiroFilter.setSuccessUrl("/index");
//未授权跳转的url
shiroFilter.setUnauthorizedUrl("/403");

//设置过滤器集合
Map<String,String> map = new LinkedHashMap<>();
//配置过滤器,静态资源不拦截,即匿名也可以访问
map.put("/css/**", "anon");
map.put("/js/**", "anon");
map.put("/fonts/**", "anon");
map.put("/img/**", "anon");
//druid数据源监控页面不拦截
map.put("/druid/**", "anon");
//配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了
map.put("/logout", "logout");
map.put("/", "anon");
//除上以外所有url都必须认证通过才可以访问,未通过认证自动访问LoginUrl
map.put("/**", "authc");

shiroFilter.setFilterChainDefinitionMap(map);
return shiroFilter;
}

}

需要注意的是filterChain基于短路机制,即最先匹配原则,如:

java
1
2
/user/**=anon
/user/aa=authc 访问user下aa且需要授权的命令则永远不会执行,因为被上一条覆盖了

其中anonauthc等为Shiro为我们实现的过滤器,具体如下表所示:

过滤器

Filter Name Class Description
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例/static/**=anon
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单的拦截器;如/**=authc,如果没有登录会跳到相应的登录页面登录
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP身份验证拦截器
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/),示例/logout=logout
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter 不创建会话拦截器,调用subject.getSession(false)不会有什么问题,但是如果subject.getSession(true)将抛出DisabledSessionException异常
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例/user/**=perms["user:create"]
port org.apache.shiro.web.filter.authz.PortFilter 端口拦截器,主要属性port(80):可以通过的端口;示例/test= port[80],如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter rest风格拦截器,自动根据请求方法构建权限字符串;示例/users=rest[user],会自动拼出user:read,user:create,user:update,user:delete权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;示例/admin/**=roles[admin]
ssl org.apache.shiro.web.filter.authz.SslFilter SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口443;其他和port拦截器一样;
user org.apache.shiro.web.filter.authc.UserFilter 用户拦截器,用户已经身份验证/记住我登录的都可;示例/**=user

配置完ShiroConfig后,接下来对Realm进行实现,然后注入到SecurityManager中。

自定义Realm

自定义Realm实现只需继承AuthorizingRealm类,然后实现setName(),doGetAuthorizationInfo()doGetAuthenticationInfo()方法即可。这后面两个方法名乍看有点像,authorization发音[ˌɔ:θəraɪˈzeɪʃn],为授权,批准的意思,即获取用户的角色和权限等信息;authentication发音[ɔ:ˌθentɪ’keɪʃn],认证,身份验证的意思,即登录时验证用户的合法性,比如验证用户名和密码。

java
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
public class ShiroRealm extends AuthorizingRealm {

@Autowired
private UserMapper userMapper;

@Override
public void setName(String name) {
super.setName("shiroRealm");
}

/**
* 授权:获取用户角色和权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}

/**
* 登录认证
* @param authenticationTokentoken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationTokentoken) throws AuthenticationException {
//获取token
UsernamePasswordToken token = (UsernamePasswordToken) authenticationTokentoken;
//获取用户名和密码
String username = token.getUsername();
String password = new String(token.getPassword());

System.out.println("用户" + username + "认证-----ShiroRealm.doGetAuthenticationInfo");

// 通过用户名到数据库查询用户信息
User user = userMapper.findByUserName(username);

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

SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
}

其中UnknownAccountException等异常为Shiro自带异常,Shiro具有丰富的运行时AuthenticationException层次结构,可以准确指出尝试失败的原因。你可以包装在一个try/catch块,并捕捉任何你希望的异常,并作出相应的反应。例如:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
currentUser.login(token);
} catch ( UnknownAccountException uae ) {
...
} catch ( IncorrectCredentialsException ice ) {
...
} catch ( LockedAccountException lae ) {
...
} catch ( ExcessiveAttemptsException eae ) {
...
} ... catch your own ...
} catch ( AuthenticationException ae ) {
//unexpected error?
}

虽然我们可以准确的获取异常信息,并根据这些信息给用户提示具体错误,但最安全的做法是在登录失败时仅向用户显示通用错误提示信息,例如“用户名或密码错误”。这样可以防止数据库被恶意扫描。

在Realm中UserMapper为Dao层,标准的做法应该还有Service层,但这里为了方便就不再定义Service层了。接下来编写和数据库打交道的Dao层。

数据层

首先创建一张用户表,用于存储用户的基本信息(基于mysql):

sql
1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `pe_user` (
`id` varchar(40) NOT NULL COMMENT 'ID',
`username` varchar(255) NOT NULL COMMENT '用户名称',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
`status` int(11) DEFAULT NULL COMMENT '状态:0失效 1有效',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

----------------------------------------------
INSERT INTO `shiro_db`.`pe_user`(`id`, `username`, `password`, `status`) VALUES ('1', 'zhangsan', '123456', 1);
INSERT INTO `shiro_db`.`pe_user`(`id`, `username`, `password`, `status`) VALUES ('2', 'lisi', '123456', 1);
INSERT INTO `shiro_db`.`pe_user`(`id`, `username`, `password`, `status`) VALUES ('3', 'wangwu', '123456', 0);
配置druid数据源
yaml
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
spring:
datasource:
druid:
# 数据库访问配置,使用druid数据源
db-type: mysql
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shiro_db?serverTimezone=UTC
username: root
password: root
# 连接池配置
initial-size: 5
min-idle: 5
max-active: 20
# 连接等待超时时间
max-wait: 30000
# 配置检测可以关闭的空闲连接间隔时间
time-between-eviction-runs-millis: 60000
# 配置连接在池中的最小生存时间
min-evictable-idle-time-millis: 300000
validation-query: select '1' from dual
test-while-idle: true
test-on-borrow: false
test-on-return: false
# 打开PSCache,并且指定每个连接上PSCache的大小
pool-prepared-statements: true
max-open-prepared-statements: 20
max-pool-prepared-statement-per-connection-size: 20
# 配置监控统计拦截的filters, 去掉后监控界面sql无法统计, 'wall'用于防火墙
filters: stat,wall
# Spring监控AOP切入点,如x.y.z.service.*,配置多个英文逗号分隔
aop-patterns: com.springboot.servie.*
# WebStatFilter配置
web-stat-filter:
enabled: true
# 添加过滤规则
url-pattern: /*
# 忽略过滤的格式
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'

# StatViewServlet配置
stat-view-servlet:
enabled: true
# 访问路径为/druid时,跳转到StatViewServlet
url-pattern: /druid/*
# 是否能够重置数据
reset-enable: false
# 需要账号密码才能访问控制台
login-username: druid
login-password: druid123
# IP白名单
# allow: 127.0.0.1
# IP黑名单(共同存在时,deny优先于allow)
# deny: 192.168.1.218
# 配置StatFilter
filter:
stat:
log-slow-sql: true
User表对应的实体类
java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Data
public class User {
/**
* ID
*/
private String id;
/**
* 用户名称
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 状态:0失效 1有效
*/
private Integer status;
/**
* 该用户所拥有的角色集合
*/
private Set<Role> roles = new HashSet<>();
}

配置application.yml

yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
#druid数据源配置在上方
thymeleaf:
prefix: classpath:/templates/
check-template-location: true
suffix: .html
encoding: utf-8
servlet:
content-type: text/html
mode: LEGACYHTML5
cache: false

server:
port: 8080

mybatis:
# mapper xml实现扫描路径
mapper-locations: classpath:mapper/*.xml
property:
order: BEFORE

页面准备

登录页面login.html

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
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
<!-- <link rel="stylesheet" th:href="@{/css/login.css}" type="text/css">-->
<script th:src="@{/js/jquery-3.4.1.min.js}"></script>
</head>
<body>
<div class="login-page">
<div class="form">
<input type="text" placeholder="用户名" name="username" required="required"/><br/>
<input type="password" placeholder="密码" name="password" required="required"/>
<button onclick="login()">登录</button>
</div>
</div>
</body>
<script>
function login() {
var username = $("input[name='username']").val();
var password = $("input[name='password']").val();
$.ajax({
type: "post",
url: "/login",
data: {"username": username,"password": password},
dataType: "json",
success: function (r) {
if (r.result) {
location.href = '/index';
} else {
alert(r.msg);
}
}
});
}
</script>
</html>

主页index.html

html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<p>你好![[${user.username}]]</p>
<a th:href="@{/logout}">注销</a>
</body>
</html>

Controller

java
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
@Controller
public class LoginController {

@RequestMapping(value = "/login",method = RequestMethod.GET)
public String login(){
return "login";
}

@RequestMapping(value = "/",method = RequestMethod.GET)
public String redirectIndex(){
return "redirect:/index";
}

@ResponseBody
@RequestMapping(value = "/login",method = RequestMethod.POST)
public Result login(@RequestParam("username") String username,
@RequestParam("password") String password){
//将密码MD5加密
// password = new Md5Hash(password, username, 3).toString();
//构建token
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
//获取subject主体
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return new Result(200,"登录成功",true);
}catch (UnknownAccountException e) {
return Result.error(e.getMessage());
} catch (IncorrectCredentialsException e) {
return Result.error(e.getMessage());
} catch (LockedAccountException e) {
return Result.error(e.getMessage());
} catch (AuthenticationException e) {
return Result.error("认证失败");
}
}

@RequestMapping(value = "/index",method = RequestMethod.GET)
public String index(Model model){
//登录成功后即可通过subject获得用户的信息
User user = (User) SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user",user);
return "index";
}
}

登录成功后,根据之前在ShiroConfig中的配置shiroFilterFactoryBean.setSuccessUrl("/index"),页面会自动访问/index路径

测试

项目目录结构如下

启动项目,分别访问

都会被重定向到http://localhost:8080/login页面

当用wangwu账号登录时(王五账号的status字段为0):

点击注销连接,根据ShiroConfig的配置filterChainDefinitionMap.put("/logout", "logout"),Shiro会自动帮我们注销用户信息,并重定向到/路径。

Shiro的权限控制

刚刚,我们通过继承AuthorizingRealm抽象类实现了doGetAuthenticationInfo()方法完成了用户认证操作。接下来继续实现doGetAuthorizationInfo()方法完成Shiro的权限控制功能。

授权也称为访问控制,是管理资源访问的过程。即根据不同用户的权限判断其是否有访问相应资源的权限。在Shiro中,权限控制有三个核心的元素:权限,角色和用户。

库模型设计

在这里,我们使用RBAC(Role-Based Access Control,基于角色的访问控制)模型设计用户,角色和权限间的关系。简单地说,一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。如下图所示:

在RBAC模型中,角色是系统根据管理中相对稳定的职权和责任来划分,每种角色可以完成一定的职能。用户通过
饰演不同的角色获得角色所拥有的权限,一旦某个用户成为某角色的成员,则此用户可以完成该角色所具有的职
能。通过将权限指定给角色而不是用户,在权限分派上提供了极大的灵活性和极细的权限指定粒度。

一个用户拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型
中,用户与角色之间,角色与权限之间,一般者是多对多的关系。

根据这个模型,设计数据库表,并插入一些测试数据:

sql
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
CREATE TABLE `pe_permission` (
`id` varchar(40) NOT NULL COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '权限名称',
`code` varchar(20) DEFAULT NULL,
`description` text COMMENT '权限描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
----------------------------------------------------------
INSERT INTO `shiro_db`.`pe_permission`(`id`, `name`, `code`, `description`) VALUES ('1', '添加用户', 'user-add', NULL);
INSERT INTO `shiro_db`.`pe_permission`(`id`, `name`, `code`, `description`) VALUES ('2', '查询用户', 'user-find', NULL);
INSERT INTO `shiro_db`.`pe_permission`(`id`, `name`, `code`, `description`) VALUES ('3', '更新用户', 'user-update', NULL);
INSERT INTO `shiro_db`.`pe_permission`(`id`, `name`, `code`, `description`) VALUES ('4', '删除用户', 'user-delete', NULL);
----------------------------------------------------------
CREATE TABLE `pe_role` (
`id` varchar(40) NOT NULL COMMENT '主键ID',
`name` varchar(40) DEFAULT NULL COMMENT '权限名称',
`description` varchar(255) DEFAULT NULL COMMENT '说明',
PRIMARY KEY (`id`),
UNIQUE KEY `UK_k3beff7qglfn58qsf2yvbg41i` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
----------------------------------------------------------
INSERT INTO `shiro_db`.`pe_role`(`id`, `name`, `description`) VALUES ('1', '系统管理员', '系统日常维护');
INSERT INTO `shiro_db`.`pe_role`(`id`, `name`, `description`) VALUES ('2', '普通员工', '普通操作权限');
----------------------------------------------------------
CREATE TABLE `pe_role_permission` (
`role_id` varchar(40) NOT NULL COMMENT '角色ID',
`permission_id` varchar(40) NOT NULL COMMENT '权限ID',
PRIMARY KEY (`role_id`,`permission_id`),
KEY `FK74qx7rkbtq2wqms78gljv87a0` (`permission_id`),
KEY `FKee9dk0vg99shvsytflym6egxd` (`role_id`),
CONSTRAINT `fk-p-rid` FOREIGN KEY (`role_id`) REFERENCES `pe_role` (`id`),
CONSTRAINT `fk-pid` FOREIGN KEY (`permission_id`) REFERENCES `pe_permission` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
----------------------------------------------------------
INSERT INTO `shiro_db`.`pe_role_permission`(`role_id`, `permission_id`) VALUES ('1', '1');
INSERT INTO `shiro_db`.`pe_role_permission`(`role_id`, `permission_id`) VALUES ('1', '2');
INSERT INTO `shiro_db`.`pe_role_permission`(`role_id`, `permission_id`) VALUES ('2', '2');
INSERT INTO `shiro_db`.`pe_role_permission`(`role_id`, `permission_id`) VALUES ('1', '3');
INSERT INTO `shiro_db`.`pe_role_permission`(`role_id`, `permission_id`) VALUES ('1', '4');
---------------------------------------------------------
CREATE TABLE `pe_user_role` (
`role_id` varchar(40) NOT NULL COMMENT '角色ID',
`user_id` varchar(40) NOT NULL COMMENT '权限ID',
KEY `FK74qx7rkbtq2wqms78gljv87a1` (`role_id`),
KEY `FKee9dk0vg99shvsytflym6egx1` (`user_id`),
CONSTRAINT `fk-rid` FOREIGN KEY (`role_id`) REFERENCES `pe_role` (`id`),
CONSTRAINT `fk-uid` FOREIGN KEY (`user_id`) REFERENCES `pe_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
---------------------------------------------------------
INSERT INTO `shiro_db`.`pe_user_role`(`role_id`, `user_id`) VALUES ('1', '1');
INSERT INTO `shiro_db`.`pe_user_role`(`role_id`, `user_id`) VALUES ('2', '2');

上面的sql创建了四张表:角色表PE_ROLE、用户角色关联表PE_USER_ROLE、权限表PE_PERMISSION和权限角色关联表PE_ROLE_PERMISSION。用户zhangsan角色为系统管理员,用户lisi角色为普通用户。系统管理员角色拥有用户的所有权限(user:user-add,user:user-find,user:user-delete,user:user-update),而test角色只拥有用户的查看权限(user:user-find)。密码都是123456,经过Shiro提供的MD5加密。(加密暂未使用)

实体类

创建两个实体类,对应用户角色表Role和用户权限表Permission:

Role:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class Role {
/**
* 主键ID
*/
private String id;
/**
* 权限名称
*/
private String name;
/**
* 说明
*/
private String description;
/**
* 该角色所拥有的的权限集合
*/
private Set<Permission> permissions = new HashSet<>();
}

Permission:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data
public class Permission {
/**
* 主键
*/
private String id;
/**
* 权限名称
*/
private String name;

private String code;
/**
* 权限描述
*/
private String description;
}

创建两个dao接口,分别用户查询用户的所有角色和用户的所有权限:

UserRoleMapper:

java
1
2
3
4
@Repository
public interface UserRoleMapper {
List<Permission> findByUserName(String username);
}

RolePermissionMapper:

java
1
2
3
4
@Repository
public interface RolePermissionMapper {
List<Role> findByUserName(String username);
}

其xml实现:

UserRoleMapper.xml:

xml
1
2
3
4
5
6
7
8
9
10
<?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="top.lemcoo.shiro.mapper.UserRoleMapper">
<select id="findByUserName" resultType="top.lemcoo.shiro.domain.Permission">
select r.id,r.name,r.description from pe_role r
left join pe_user_role ur on(r.id = ur.role_id)
left join pe_user u on(u.id = ur.user_id)
where u.username = #{username,jdbcType=VARCHAR}
</select>
</mapper>

RolePermissionMapper.xml

xml
1
2
3
4
5
6
7
8
9
10
11
12
<?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="top.lemcoo.shiro.mapper.RolePermissionMapper">
<select id="findByUserName" resultType="top.lemcoo.shiro.domain.Role">
select p.id,p.name,p.code from pe_role r
left join pe_user_role ur on(r.id = ur.role_id)
left join pe_user u on(u.id = ur.user_id)
left join pe_role_permission rp on(rp.role_id = r.id)
left join pe_permission p on(p.id = rp.permission_id )
where u.username = #{username,jdbcType=VARCHAR}
</select>
</mapper>

数据层准备好后,接下来对Realm进行改造。

Realm

在Shiro中,用户角色和权限的获取是在Realm的doGetAuthorizationInfo()方法也就是授权中实现的,所以接下来手动实现该方法:

java
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
public class ShiroRealm extends AuthorizingRealm {

@Autowired
private UserMapper userMapper;

@Autowired
private UserRoleMapper userRoleMapper;

@Autowired
private RolePermissionMapper rolePermissionMapper;

@Override
public void setName(String name) {
super.setName("shiroRealm");
}

/**
* 授权:获取用户角色和权限
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
String username = user.getUsername();

System.out.println("用户" + username + "获取权限-----ShiroRealm.doGetAuthorizationInfo");

List<Role> roleList = userRoleMapper.findByUserName(username);
//获取用户的角色列表
Set<String> roles = new HashSet<>();
for (Role role : roleList) {
roles.add(role.getName());
}
List<Permission> byUserName = rolePermissionMapper.findByUserName(username);
//获取角色的权限列表
Set<String> permis = new HashSet<>();
for (Permission permission : byUserName) {
permis.add(permission.getCode());
}
//构建授权方法的实现类
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//配置角色列表
info.setRoles(roles);
//配置权限列表
info.setStringPermissions(permis);
return info;
}

/**
* 登录认证
* @param authenticationTokentoken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationTokentoken) throws AuthenticationException {
//此方法在上文中,在此省略
}
}

在上述代码中,我们通过方法userRoleMapper.findByUserName(userName)userPermissionMapper.findByUserName(userName)获取了当前登录用户的角色和权限集,然后保存到SimpleAuthorizationInfo对象中,并返回给Shiro,这样Shiro中就存储了当前用户的角色和权限信息了。除了对Realm进行改造外,我们还需修改ShiroConfig配置。

ShiroConfiguration

Shiro为我们提供了一些和权限相关的注解,如下所示:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 表示当前Subject已经通过login进行了身份验证;即Subject.isAuthenticated()返回true。
@RequiresAuthentication

// 表示当前Subject已经身份验证或者通过记住我登录的。
@RequiresUser

// 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。
@RequiresGuest

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

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

要开启这些注解的使用,需要在ShiroConfig中添加如下配置:

java
1
2
3
4
5
6
7
8
9
10
11
/**
* 配置shiro注解支持
* @param manager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager manager){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(manager);
return advisor;
}

Controller

编写一个UserController,用于处理User类的访问请求,并使用Shiro权限注解控制权限:

java
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
@Controller
@RequestMapping("/user")
public class UserController {

@RequiresPermissions("user-find")
@RequestMapping("/list")
public String userList(Model model) {
model.addAttribute("value", "获取用户信息");
return "user";
}

@RequiresPermissions("user-add")
@RequestMapping("/add")
public String userAdd(Model model) {
model.addAttribute("value", "新增用户");
return "user";
}

@RequiresPermissions("user-delete")
@RequestMapping("/delete")
public String userDelete(Model model) {
model.addAttribute("value", "删除用户");
return "user";
}
}

在LoginController中添加一个/403跳转:

java
1
2
3
4
@RequestMapping(value = "/403",method = RequestMethod.GET)
public String forbid(){
return "403";
}

前端页面

对index.html改造,添加三个用户操作的链接:

html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<p>你好![[${user.username}]]</p>
<h3>权限测试链接</h3>
<div>
<a th:href="@{/user/list}">获取用户信息</a>
<a th:href="@{/user/add}">新增用户</a>
<a th:href="@{/user/delete}">删除用户</a>
</div>
<a th:href="@{/logout}">注销</a>
</body>
</html>

当用户对用户的操作有相应权限的时候,跳转到user.html:

html
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>[[${value}]]</title>
</head>
<body>
<p>[[${value}]]</p>
<a th:href="@{/index}">返回</a>
</body>
</html>

403页面

html
1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>暂无权限</title>
</head>
<body>
<p>您没有权限访问该资源!!</p>
<a th:href="@{/index}">返回</a>
</body>

测试

启动项目,使用zhangsan的账户登录后主页如下图所示:

点击”获取用户信息连接”:

因为zhangsan角色为系统管理员,对着三个链接都由访问权限,所以这里就不演示了。

接着使用lisi用户登录。因为lisi用户角色为普通用户,只拥有(user-find)权限,所以当其点击”新增用户”和”删除用户”的时候:

后台抛出org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method:…异常!!!

这里有点出乎意料,本以为在ShiroConfiguration中配置了shiroFilter.setUnauthorizedUrl("/403");,没有权限的访问会自动重定向到/403,结果证明并不是这样。后来研究发现,该设置只对filterChain起作用,比如在filterChain中设置了map.put("/user/update", "perms[user-update]");,如果用户没有user-update权限,那么当其访问/user/update的时候,页面会被重定向到/403。

所以在使用注解的时候,当权限不足时是会抛出异常的,那么对于这个问题,我们可以自定义一个全局异常捕获类。当因为权限不足抛出异常时,我们让它重定向到403页面即可。

java
1
2
3
4
5
6
7
8
@ControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(value = AuthorizationException.class)
public String handleAuthorizationException() {
return "403";
}
}

启动项目,再次使用tester的账号点击”新增用户”和”删除用户”链接的时候,页面如下所示:

页面已经成功重定向到/403。

Shiro的缓存

在Shiro中加入缓存可以使权限相关操作尽可能快,避免频繁访问数据库获取权限信息,因为对于一个用户来说,其权限在短时间内基本是不会变化的。Shiro提供了Cache的抽象,其并没有直接提供相应的实现,因为这已经超出了一个安全框架的范围。在Shiro中可以集成常用的缓存实现,这里介绍基于Redis和Ehcache缓存的实现。

在上述权限控制中,当用户访问”获取用户信息”、”新增用户”和”删除用户”的时候,后台输出了三次打印信息,如下所示:

java
1
2
3
用户zhangsan获取权限-----ShiroRealm.doGetAuthorizationInfo
用户zhangsan获取权限-----ShiroRealm.doGetAuthorizationInfo
用户zhangsan获取权限-----ShiroRealm.doGetAuthorizationInfo

说明在这三次访问中,Shiro都会从数据库中获取用户的权限信息,这对数据库来说是没必要的消耗。接下来使用缓存来解决这个问题。

引入Redis依赖

xml
1
2
3
4
5
6
<!-- shiro-redis -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>2.4.2.1-RELEASE</version>
</dependency>

配置Redis

我们在application.yml配置文件中加入Redis配置:

yaml
1
2
3
4
spring:
redis:
host: localhost
port: 6379

接着在ShiroConfig中配置Redis:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 配置 shiro RedisManager
* @return
*/
public RedisManager redisManager(){
RedisManager redisManager = new RedisManager();
return redisManager;
}

/**
* 配置 shiro RediscacheManager缓存管理器
* @return
*/
public RedisCacheManager redisCacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}

上面代码配置了RedisManager,并将其注入到了RedisCacheManager中,最后在SecurityManager中加入RedisCacheManager:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 配置安全管理器
* @param realm
* @return
*/
@Bean
public SecurityManager securityManager(ShiroRealm realm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//将自定义的realm交给安全管理器统一管理
manager.setRealm(realm);
//自定义缓存管理
manager.setCacheManager(redisCacheManager());
return manager;
}

配置完毕启动项目,分别访问访问”获取用户信息”、”新增用户”和”删除用户”,可发现后台只打印一次获取权限信息:

java
1
用户zhangsan获取权限-----ShiroRealm.doGetAuthorizationInfo

Shiro的会话管理

在Shiro中我们可以通过org.apache.shiro.session.mgt.eis.SessionDAO对象的getActiveSessions()方法方便的获取到当前所有有效的Session对象。通过这些Session对象,我们可以实现一些比较有趣的功能,比如查看当前系统的在线人数,查看这些在线用户的一些基本信息,强制让某个用户下线等。

为了达到这几个目标,我们在现有的Spring Boot Shiro项目基础上进行一些改造。

更改ShiroConfiguration

为了能够在Spring Boot中使用SessionDao,我们在ShiroConfig中配置该Bean:

java
1
2
3
4
5
6
7
8
9
/**
* RedisSessionDAO shiro sessionDao层实现
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}

在Shiro中,SessionDao通过org.apache.shiro.session.mgt.SessionManager进行管理,所以继续在ShiroConfig中配置SessionManager

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* shiro session管理
* @return
*/
@Bean
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager manager = new DefaultWebSessionManager();
Collection<SessionListener> listeners = new ArrayList<>();
listeners.add(new ShiroSessionListener());
manager.setSessionDAO(redisSessionDAO());
manager.setSessionListeners(listeners);
return manager;
}
}

其中ShiroSessionListenerorg.apache.shiro.session.SessionListener接口的手动实现,所以接下来定义一个该接口的实现:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ShiroSessionListener implements SessionListener{
private final AtomicInteger sessionCount = new AtomicInteger(0);

@Override
public void onStart(Session session) {
sessionCount.incrementAndGet();
}

@Override
public void onStop(Session session) {
sessionCount.decrementAndGet();
}

@Override
public void onExpiration(Session session) {
sessionCount.decrementAndGet();
}
}

其维护着一个原子类型的Integer对象,用于统计在线Session的数量。

定义完SessionManager后,还需将其注入到SecurityManager中:

java
1
2
3
4
5
6
7
8
9
10
11
@Bean
public SecurityManager securityManager(ShiroRealm realm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//将自定义的realm交给安全管理器统一管理
manager.setRealm(realm);
//自定义缓存管理
manager.setCacheManager(redisCacheManager());
//自定义session管理
manager.setSessionManager(sessionManager());
return manager;
}

UserOnline

配置完ShiroConfig后,我们可以创建一个UserOnline实体类,用于描述每个在线用户的基本信息:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class UserOnline implements Serializable{

private static final long serialVersionUID = 3828664348416633856L;
// session id
private String id;
// 用户id
private String userId;
// 用户名称
private String username;
// 用户主机地址
private String host;
// 用户登录时系统IP
private String systemHost;
// 状态
private String status;
// session创建时间
private Date startTimestamp;
// session最后访问时间
private Date lastAccessTime;
// 超时时间
private Long timeout;
// get set略
}

Service

创建一个Service接口,包含查看所有在线用户和根据SessionId踢出用户抽象方法:

java
1
2
3
4
public interface SessionService {
List<UserOnline> list();
boolean forceLogout(String sessionId);
}

具体实现

java
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
@Service
public class SessionServiceImpl implements SessionService {

@Autowired
private SessionDAO sessionDAO;

/**
* 查看所有在线用户
* @return
*/
@Override
public List<UserOnline> list() {
List<UserOnline> list = new ArrayList<>();
Collection<Session> sessions = sessionDAO.getActiveSessions();
for (Session session : sessions) {
UserOnline userOnline = new UserOnline();
User user = new User();
SimplePrincipalCollection collection = new SimplePrincipalCollection();
if (session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null){
continue;
}else {
collection = (SimplePrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
user = (User) collection.getPrimaryPrincipal();
userOnline.setUsername(user.getUsername());
userOnline.setUserId(user.getId());
}
userOnline.setId((String) session.getId());
userOnline.setHost(session.getHost());
userOnline.setStartTimestamp(session.getStartTimestamp());
userOnline.setLastAccessTime(session.getLastAccessTime());
Long timeout = session.getTimeout();
if (timeout == 0L) {
userOnline.setStatus("离线");
} else {
userOnline.setStatus("在线");
}
userOnline.setTimeout(timeout);
list.add(userOnline);
}
return list;
}

/**
* 根据sessionId踢出用户
* @param sessionId
* @return
*/
@Override
public boolean forceLogout(String sessionId) {
Session session = sessionDAO.readSession(sessionId);
session.setTimeout(0);
return true;
}
}

通过SessionDao的getActiveSessions()方法,我们可以获取所有有效的Session,通过该Session,我们还可以获取到当前用户的Principal信息。

值得说明的是,当某个用户被踢出后(Session Time置为0),该Session并不会立刻从ActiveSessions中剔除,所以我们可以通过其timeout信息来判断该用户在线与否。

如果使用的Redis作为缓存实现,那么,forceLogout()方法需要稍作修改

java
1
2
3
4
5
6
7
8
9
10
11
/**
* 根据sessionId踢出用户
* @param sessionId
* @return
*/
@Override
public boolean forceLogout(String sessionId) {
Session session = sessionDAO.readSession(sessionId);
sessionDAO.delete(session);
return true;
}

Controller

定义一个SessionContoller,用于处理Session的相关操作:

java
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
@Controller
@RequestMapping("/online")
public class SessionController {
@Autowired
SessionService sessionService;

@RequestMapping("index")
public String online() {
return "online";
}

@ResponseBody
@RequestMapping("list")
public List<UserOnline> list() {
return sessionService.list();
}

@ResponseBody
@RequestMapping("forceLogout")
public Result forceLogout(String id) {
try {
sessionService.forceLogout(id);
return Result.ok();
} catch (Exception e) {
e.printStackTrace();
return Result.error("踢出用户失败");
}
}
}

页面

我们编写一个online.html页面,用于展示所有在线用户的信息:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>在线用户管理</title>
<script th:src="@{/js/jquery-3.4.1.min.js}"></script>
<script th:src="@{/js/dateFormat.js}"></script>
</head>
<body>
<h3>在线用户数:<span id="onlineCount"></span></h3>
<table>
<tr>
<th>序号</th>
<th>用户名称</th>
<th>登录时间</th>
<th>最后访问时间</th>
<th>主机</th>
<th>状态</th>
<th>操作</th>
</tr>
</table>
<a th:href="@{/index}">返回</a>
</body>
<script th:inline="javascript">
$.get("/online/list", {}, function(r){
var length = r.length;
$("#onlineCount").text(length);
var html = "";
for(var i = 0; i < length; i++){
html += "<tr>"
+ "<td>" + (i+1) + "</td>"
+ "<td>" + r[i].username + "</td>"
+ "<td>" + new Date(r[i].startTimestamp).Format("yyyy-MM-dd hh:mm:ss") + "</td>"
+ "<td>" + new Date(r[i].lastAccessTime).Format("yyyy-MM-dd hh:mm:ss") + "</td>"
+ "<td>" + r[i].host + "</td>"
+ "<td>" + r[i].status + "</td>"
+ "<td><a href='#' onclick='offline(\"" + r[i].id + "\",\"" + r[i].status +"\")'>下线</a></td>"
+ "</tr>";
}
$("table").append(html);
},"json");

function offline(id,status){
if(status == "离线"){
alert("该用户已是离线状态!!");
return;
}
$.get(ctx + "online/forceLogout", {"id": id}, function(r){
if (r.code == 0) {
alert('该用户已强制下线!');
location.href = '/online/index';
} else {
alert(r.msg);
}
},"json");
}
</script>
</html>

在index.html中加入该页面的入口:

html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<p>你好![[${user.username}]]</p>
<p>你的角色为超级管理员</p>

<div>
<a th:href="@{/user/list}">获取用户信息</a>
<a th:href="@{/user/add}">新增用户</a>
<a th:href="@{/user/delete}">删除用户</a>
</div>
<a th:href="@{/online/index}">在线用户管理</a>
<a th:href="@{/logout}">注销</a>
</body>
</html>

测试

启动项目,在google浏览器中使用zhangsan账户访问:

在FireFox浏览器中使用lisi账户访问:

然后在zhangsan主界面点击“在线用户管理”:

显示的信息符合我们的预期,点击lisi的下线按钮,强制将其踢出

回到lisi用户的主界面,点击“查看用户信息”,会发现页面已经被重定向到login页面,因为其Session已经失效!

再次刷新zhangsan的online页面,显示如下:

到此,基本的Shiro功能已大概记录完成。

参考了Mrbird大神的博客!

源码地址:https://gitee.com/worldzwx/Shiro-Demo

文章作者: 十四礼
文章链接: https://lemcoo.top/2020/01/202001301855-shiro/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 观礼塘

评论