Shiro 基础
简介
Shiro 是 apache 基金会旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用 shiro 就可以非常快速的完成认证、授权等功能的开发,降低系统成本。
-
整体架构
image- 上面 Subject 是操作用户
- 下面 Security Manager 是 Shiro 的核心
- Authenticator 认证器,管理登录和登出
- Authorizer 授权器赋予主体 Subject 有哪些权限
- Session Manager 是 session 管理器
- Session DAO 提供对 Session 的 crud 操作
- Cache Manager 是缓存管理器
- Realms 是 shiro 和数据库之间的桥梁,shiro 获取认证信息和权限数据就是通过 realms 获取
- Cryptography 是用于加密
-
Shiro 认证过程
image先创建 SecurityManager 对象,然后主体会提交认证请求到 SecurityManager,SecurityManager 使用 Authenticator 做认证,认证时要通过 Realm 获取认证数据做最终的认证。
-
Shiro 授权过程
image先创建 SecurityManager 对象,然后主体会提交认证请求到 SecurityManager,SecurityManager 使用 Authorizer 做授权,认证时要通过 Realm 获取认证数据做最终的认证。
<el-divider></el-divider>
权限管理实现对用户访问系统的控制,按照安全规则或者安全策略『控制』用户可以访问而且只能访问自己被『授权』的资源。
权限管理包括用户『身份认证』和『授权』两部分,简称『认证授权』。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
如何管控某个用户对某个资源的访问,有两种思路:『基于角色的访问控制』和『基于资源访问控制』两种。
shiro_1通常企业开发中将资源和权限表合并为一张权限表。
基本概念
-
pom.xml
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.0</version> </dependency>
使用 Shiro 的核心组件在于:SecurityManager 。
::: warning
它是 org.apache.shiro.mgt.SecurityManager ,而非 java.lang.SecurityManager ,import 的时候不要弄错了。
如果发现项目代码中的 SecurityManager 无缘无故报错,确认前面是否 import 了正确的包 。
:::
在每一个使用到 Shiro 的项目中,必须存在 SecurityManager 对象。创建、配置 SecurityManager 对象,是启用 Shiro 的第一步。
Shiro 提供了多种内置的方式来创建、配置 Shiro 对象<small>(而且还提供了灵活的自定义创建-配置方式)</small>。其中最简便<small>(也是最不可能在实际中使用)</small>的方法是『硬编码』方式。
public class AuthenticationTest {
private Logger log = LoggerFactory.getLogger(AuthenticationTest.class);
// 定义一个 realm
private SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before
public void addUser() {
simpleAccountRealm.addAccount("tommy", "123", "admin", "user");
}
@Test
public void testAuthentication() {
/** 1. 构建 SecurityManager 环境 **/
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 『告知』Shiro 在 simpelAccoutnRealm 中找『标准答案』
defaultSecurityManager.setRealm(simpleAccountRealm);
// 启用 Shiro
SecurityUtils.setSecurityManager(defaultSecurityManager);
/** 2. 主体提交认证请求 **/
// 导入 shiro 的 org.apache.shiro.subject.Subject;
Subject subject = SecurityUtils.getSubject();
// token 是用户要认证的用户数据
UsernamePasswordToken token = new UsernamePasswordToken("tommy", "123");
// shiro 提供是否认证的方法
log.info("{}", subject.isAuthenticated() ? "登陆过" : "未登录");
// 登入
subject.login(token);
log.info("{}", subject.isAuthenticated() ? "登陆过" : "未登录");
log.info("isAuthenticated: [{}]", subject.isAuthenticated());
// 退出
subject.logout();
log.info("isAuthenticated: [{}]", subject.isAuthenticated());
}
@Test
public void testAuthorization() {
// 1.构建 SecurityManager 环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
// 2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 导入shiro的org.apache.shiro.subject.Subject;
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("tommy", "123", "admin", "user");
log.info("{}", subject.isAuthenticated() ? "已登陆" : "未登录");
subject.login(token);
log.info("{}", subject.isAuthenticated() ? "已登陆" : "未登录");
log.info("{} 'admin' 角色", subject.hasRole("admin") ? "有" : "没有");
log.info("{} 'user' 角色", subject.hasRole("user") ? "有" : "没有");
}
}
Shiro 权限认证 API
Shiro 中的权限认证<small>(Authorization)</small>也称授权,它解决了『用户能干什么』的问题。
权限认证,也就是访问控制,即在应用中控制谁能访问哪些资源。在权限认证中,最核心的三个要素是:权限,角色 和 用户 :
shiro_2角色(Role)
角色是权限的集合,而一个用户可以拥有多种角色。
RBAC(Role Base Access Control)
-
逻辑上,『角色』是『权限』的集合。
-
一个人能干什么事情,本质上是因为某个角色能干这个事情,而这个人拥有、充当、扮演了这个角色,所以他才能干这个事情。
Shiro 中检测角色和权限的方法
-
Subject 用于判断角色的方法有:
方法 作用 hasRole(String roleName) 判断是否有该角色访问权,
返回 boolenhasRoles(List<String> roleNames) 判断是否有这些这些角色访问权,
返回 boolean[]hasAllRoles(Collection<String> roleNames) 判断是否有这些这些角色访问权,
返回 booleancheckRole(String roleName) 如果判断失败抛出 AuthorizationException 异常 checkRoles(String... roleNames) 如果判断失败抛出 AuthorizationException 异常 checkRoles(Collection<String> roleNames) 如果判断失败抛出 AuthorizationException 异常
-
Subject 用于判断权限的方法有:
方法 作用 isPermitted(String perm) 判断是否有该权限,返回 boolen >isPermitted(List<String> perms) 判断是否有这些这些权限,返回 boolean[] isPermittedAll(Collection<String> perms) 判断是否有这些这些权限,返回 boolean checkPermission(String perm) 如果判断失败抛出 AuthorizationException 异常 checkPermissions(String... perms) 如果判断失败抛出 AuthorizationException 异常 checkPermissionsAll(Collection<String> perms) 如果判断失败抛出 AuthorizationException 异常
Shiro 内置的 IniRealm(了解、自学)
IniRealm 是 Shiro 内置的一个 Realm,这种模式中,将用户名、密码、角色、权限编写在一个 .ini
文件中,Shiro 查找该文件并从中读取相关信息用以校验当前登录用户的身份和权限。
当然,.ini
文件有固定的格式上的语法规则。
在 resources 目录下创建 user.ini
文件。一个典型的简单 ini 配置文件内容类似如下:
# 登录用户名,密码,及其角色
[users]
tommy=123,admin,user
jerry=123,user
# 角色所具有的的权限
[roles]
admin=user:insert,user:delete,user:update
user=user:query
结合配置文件,创建-配置 SecurityManager 的典型代码如下:
private IniRealm realm;
@BeforeEach
public void before() {
Ini ini = new Ini();
ini.loadFromPath("classpath:user.ini");
realm = new IniRealm(ini);
}
@Test
public void test() {
// 和 SimpleAccoutRealm 中的测试代码一样
}
::: warning
.ini
文件中还可以有更多方面的相关配置,但是由于我们项目中并非使用 IniRealm,所以此处不作更多介绍和验证。
:::
Shiro 内置的 JdbcRealm
很显然将用户信息(特别是密码)存在 .ini
这样的文本文件中,也并非合适的做法。更常见也更合理的做法是将相关信息存在数据库中。
Shiro 内置的 JdbcRealm 就是自动从数据库中读取相关用户信息,并用以校验当前登录用户的密码、身份和权限。
从 JdbcRealm 的源码中可以看到其默认的数据库的表和表结构:
String DEFAULT_AUTHENTICATION_QUERY =
"SELECT password FROM users WHERE username = ?";
String DEFAULT_SALTED_AUTHENTICATION_QUERY =
"SELECT password, password_salt FROM users WHERE username = ?"
String DEFAULT_USER_ROLES_QUERY =
"SELECT role_name FROM user_roles WHERE username = ?"
String DEFAULT_PERMISSIONS_QUERY =
"SELECT permission FROM roles_permissions WHERE role_name = ?"
从其默认的 SQL 查询语句来看,默认的表格的结构非常简单,有可能无法满足你的业务逻辑需求。
jdbcRealm 会用到数据库中的三张表:
-
用户表 users,形如:
shiro-jdbcRealm-01
-
角色表 user_roles,形如:
shiro-jdbcRealm-02
-
角色权限表 roles_permissions,形如:
shiro-jdbcRealm-03
SET FOREIGN_KEY_CHECKS = off;
-- 用户表
CREATE TABLE `users`
(
`id` bigint(20) PRIMARY KEY AUTO_INCREMENT,
`username` varchar(100) UNIQUE DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
`password_salt` varchar(100) DEFAULT NULL
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `users` VALUE (null, 'tommy', '123', null);
INSERT INTO `users` VALUE (null, 'jerry', '123', null);
-- 角色表
DROP TABLE IF EXISTS `user_roles`;
CREATE TABLE `user_roles`
(
`id` bigint(20) PRIMARY KEY AUTO_INCREMENT,
`username` varchar(100) DEFAULT NULL,
`role_name` varchar(100) DEFAULT NULL,
UNIQUE KEY (`username`, `role_name`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `user_roles` VALUE (NULL, 'tommy', 'admin');
INSERT INTO `user_roles` VALUE (NULL, 'tommy', 'user');
INSERT INTO `user_roles` VALUE (NULL, 'jerry', 'user');
-- 权限表
DROP TABLE IF EXISTS `roles_permissions`;
CREATE TABLE `roles_permissions`
(
`id` bigint(20) PRIMARY KEY AUTO_INCREMENT,
`role_name` varchar(100) DEFAULT NULL,
`permission` varchar(100) DEFAULT NULL,
UNIQUE KEY (`role_name`, `permission`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;
INSERT INTO `roles_permissions` VALUE (NULL, 'admin', 'user:insert');
INSERT INTO `roles_permissions` VALUE (NULL, 'admin', 'user:delete');
INSERT INTO `roles_permissions` VALUE (NULL, 'admin', 'user:update');
INSERT INTO `roles_permissions` VALUE (NULL, 'user', 'user:query');
SET FOREIGN_KEY_CHECKS = on;
private static DruidDataSource dataSource;
private JdbcRealm realm;
static {
dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro?useUnicode=true&characterEncoding=utf-8&useSSL=false");
dataSource.setUsername("root");
dataSource.setPassword("123456");
}
@Before
public void before() {
realm = new JdbcRealm();
realm.setDataSource(dataSource);
realm.setPermissionsLookupEnabled(true); // 注意此处。设为 ture,不然会验证失败。
}
@Test
public void test() {
// 和 SimpleAccountRealm 以及 IniRealm 中的测试代码一样
}
上面的测试代码中有一个小细节: jdbcRealm.setPermissionsLookupEnabled(true);
这是一个检测权限的开关,jdbcRealm 默认是关闭 check permission 功能的。如果需要调用 subject.checkPermission()
方法检测用户的权限的话,需要将这个功能开关打开。
自定义 SQL 语句<small>(了解)</small>
在上面的测试代码中,我们一直使用的是 JdbcRealm 中自带的默认的 SQL 语句查询用户的密码、身份和权限。如果你的数据库中的表因为某些原因无法满足 Shiro 的默认的要求,而是自建的,从而需要手动指定查询 SQL 语句时,让 Shiro 使用你所指定的 SQL 语句,而不是默认的 SQL 语句去查询用户相关的密码、橘色、权限。
这时需要为 jdbcRealm 手动指定查询语句:
// 使用自定义的 sql 验证表中的用户
String sql = "select `password` from `test_users` where `user_name` = ?";
realm.setAuthenticationQuery(sql);
这里有个小矛盾:此时,可能就不会手动指定 SQL 语句,而是干脆直接自定 Realm ,一切按照我们自己写的代码来。