Shiro 基础

2022-02-10  本文已影响0人  hemiao3000

简介

Shiro 是 apache 基金会旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用 shiro 就可以非常快速的完成认证、授权等功能的开发,降低系统成本。

<el-divider></el-divider>

权限管理实现对用户访问系统的控制,按照安全规则或者安全策略『控制』用户可以访问而且只能访问自己被『授权』的资源。

权限管理包括用户『身份认证』和『授权』两部分,简称『认证授权』。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

如何管控某个用户对某个资源的访问,有两种思路:『基于角色的访问控制』和『基于资源访问控制』两种。

shiro_1

通常企业开发中将资源和权限表合并为一张权限表。

基本概念

使用 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 中检测角色和权限的方法

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 会用到数据库中的三张表:

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 ,一切按照我们自己写的代码来。

上一篇下一篇

猜你喜欢

热点阅读