# demo-shiro **Repository Path**: caplike-demo/demo-shiro ## Basic Information - **Project Name**: demo-shiro - **Description**: No description available - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-10-15 - **Last Updated**: 2023-03-10 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Introduction to Apache Shiro > 博文地址: https://blog.csdn.net/caplike/article/details/117987485 > 关于 Apache Shiro 的系列 Demo. > > 版本: 1.7.1 > > Reference: https://shiro.apache.org/introduction.html#introduction-to-apache-shiro Shiro 着眼于其开发团队认为的现代应用程序安全的 4 块基石: 认证 (**Authentication**), 授权 (**Authorization**), 会话管理 (**Session Manager**) 和 加密 (**Cryptography**): - **Authentication**: 身份认证 / 登录. 验证用户是不是拥有相应的身份; - **Authorization**: 访问控制, 授权, 即权限验证. 验证某个已认证的用户是否拥有某个权限; 即判断用户是否能做事情, 常见的如; 验证某个用户是否拥有某个角色. 或者细粒度的验证某个用户对某个资源是否具有某个权限; - **Session Manager**: 会话管理, 即用户登录后就是一次会话, 在没有退出之前, 它的所有信息都在会话中; 会话可以是普通 JavaSE 环境的, 也可以是如 Web 环境的; - **Cryptography**: 加密, 保护数据的安全性, 如密码加密存储到数据库, 而不是明文存储; ![shiro-features](./material/shiro-features.png) 一些其他特性: - Web Support: Web 支持, 可以非常容易的集成到 Web 环境; - Caching: 缓存, 比如用户登录后, 其用户信息, 拥有的角色 / 权限不必每次去查, 这样可以提高效率; - Concurrency: Shiro 支持多线程应用的并发验证, 即如在一个线程中开启另一个线程, 能把权限自动传播过去; - Testing: 提供测试支持; - Run As: 允许一个用户假装为另一个用户 (如果他们允许) 的身份进行访问; - Remember Me: 记住我, 这个是非常常见的功能, 即一次登录后, 下次再来的话不用登录了; # Architecture 总的来说, Shiro 有 3 个最核心的概念: Subject, SecurityManager 和 Realm. 下图展示了他们之前如何交互以及用户代码如何接入: ![shiro-basic-architecture](./material/shiro-basic-architecture.png) 可以看到: 应用代码直接交互的对象是 Subject, 也就是说 Shiro 的对外 API 核心就是 Subject; 其每个 API 的含义: - **Subject**: 主体,代表了当前 “用户”, 这个用户不一定是一个具体的人, 与当前应用交互的任何东西都是 Subject, 如网络爬虫, 机器人等, 即一个抽象概念; 所有 Subject 都绑定到 SecurityManager, 与 Subject 的所有交互都会委托给 SecurityManager; 可以把 Subject 认为是一个门面; SecurityManager 才是实际的执行者; - **SecurityManager**:安全管理器; 即所有与安全有关的操作都会与 SecurityManager 交互; 它管理着所有 Subject; 是 Shiro 的核心, 负责与后边介绍的其他组件进行交互; - **Realm**:充当 Shiro 和 应用程序的私密数据间的 "桥梁" 的角色. 域. Shiro 从 Realm 获取安全数据 (如用户, 角色, 权限), 就是说 SecurityManager 要验证用户身份, 那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法; 也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作; 可以把 Realm 看成 DataSource, 即安全数据源; 对我们而言, 一个最简单的 Shiro 应用: 1. 应用代码通过 Subject 来进行认证和授权, 而 Subject 又委托给 SecurityManager; 2. 我们需要给 Shiro 的 SecurityManager 注入 Realm, 从而让 SecurityManager 能得到合法的用户及其权限进行判断. 从以上也可以看出, Shiro 不提供维护用户 / 权限, 而是通过 Realm 让开发人员自己注入. # Detailed Architecture > Reference: https://shiro.apache.org/architecture.html#detailed-architecture ![shiro-architecture](./material/shiro-architecture.png) - **Subject** ([`org.apache.shiro.subject.Subject`](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/subject/Subject.html)): 主体, 可以看到主体可以是任何可以与应用交互的 "用户"; - **SecurityManager** ([org.apache.shiro.mgt.SecurityManager](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/mgt/SecurityManager.html)): 是 Shiro 的核心组件; **所有具体的交互都通过 SecurityManager 进行控制; 它管理着所有 Subject, 且负责进行认证和授权, 及会话, 缓存的管理**; - **Authenticator** ([org.apache.shiro.authc.Authenticator](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/Authenticator.html)): 认证器, 负责执行和响应用户的认证请求: 当一个用户尝试登陆, 这个逻辑就是 `Authenticator` 执行, `Authenticator` 知道如何协调一个或者多个与用户/账户信息相关的 `Realm`, 从这些 `Realm` 中获取的数据被用于验证用户的身份. - **Authentication Strategy** ([org.apache.shiro.authc.pam.AuthenticationStrategy](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/pam/AuthenticationStrategy.html)) 如果多个 `Realm` 被配置, `AuthenticationStrategy` 就会协调 Realms 并决定当前认证是否成功: 例如一个 Realm 成功了, 其他的失败了, 这种情况下一次尝试是否成功? 是否必须所有 Realms 成功还是仅仅是第一个? - **Authorizer** ([org.apache.shiro.authz.Authorizer](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/authz/Authorizer.html)): 授权器, 或者访问控制器, 用来决定用户是否有权限进行相应的操作; 即控制着用户能访问应用中的哪些功能; 和 `Authenticator` 一样, `Authorizer` 同样知道如何协调多种后端数据源用于决定当前用户是否被允许执行目标操作. - **SessionManager** ([org.apache.shiro.session.mgt.SessionManager](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/session/mgt/SessionManager.html)): `SessionManager` 知道如何创建和管理用户 `Session`, Shiro 原生就能在所有环境下管理用户会话 (甚至当前没有可用的 Servlet 或是 EJB 容器). 默认情况下, Shiro 会使用已存在的会话管理机制 (如 Servlet 容器), 但是如果当前上下文不存在这样的容器, 如独立应用或是非网络应用场景, Shiro 会使用内置的企业级会话管理机制以提供统一的编程体验. *如果写过 Servlet 就应该知道 Session 的概念, Session 呢需要有人去管理它的生命周期, 这个组件就是 SessionManager; 而 Shiro 并不仅仅可以用在 Web 环境, 也可以用在如普通的 JavaSE 环境, EJB 等环境; Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据; 这样的话, 比如我们在 Web 环境用, 刚开始是一台 Web 服务器; 接着又上了台 EJB 服务器; 这时想把两台服务器的会话数据放到一个地方, 这个时候就可以实现自己的分布式会话 (如把数据放到 Memcached 服务器)* - **SessionDAO** ([org.apache.shiro.session.mgt.eis.SessionDAO](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/session/mgt/eis/SessionDAO.html)) 它的存在允许使用任意数据源来持久化会话, 依托 `SessionManager` 以 CRUD 的形式执行缓存的持久化操作. *DAO 大家都用过, 数据访问对象, 用于会话的 CRUD, 比如我们想把 Session 保存到数据库, 那么可以实现自己的 SessionDAO, 通过如 JDBC 写到数据库; 比如想把 Session 放到 Memcached 中, 可以实现自己的 Memcached SessionDAO; 另外 SessionDAO 中可以使用 Cache 进行缓存, 以提高性能* - **CacheManager** ([org.apache.shiro.cache.CacheManager](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/cache/CacheManager.html)) 用于创建和管理 `Cache` 实例. 正因为 Shiro 能够访问许多用于认证, 授权, 以及会话管理的后端数据源, 能够提升访问性能和效率的缓存通常是首要需要考虑的. - **Cryptography** ([org.apache.shiro.crypto.*](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/crypto/package-summary.html)) Shiro 的加密包在原有 Java 提供的加密机制上再次封装, 提供了更加易用和易于理解的 API, - **Realms** ([org.apache.shiro.realm.Realm](https://shiro.apache.org/static/current/apidocs/org/apache/shiro/realm/Realm.html)) 正如之前提到的, Realms 是 Shiro 和应用程序的安全数据之间的桥梁. 当需要和安全相关的数据 (如用户账户) 交互来进行 (登录) 认证和 (访问) 授权的时候, Shiro 会从配置的一个或者多个 `Realm` 中获取信息. *可以有 1 个或多个 Realm, 可以认为是安全实体数据源, 即用于获取安全实体的; 可以是 JDBC 实现, 也可以是 LDAP 实现, 或者内存实现等等; 由用户提供; 注意: Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;* # Realm `Realm`: 域, Shiro 从 Realm 获取安全数据 (如用户, 角色, 权限), 就是说 `SecurityManager` 要验证用户身份, 那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法; 也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作; 可以把 Realm 看成 DataSource. Shiro 提供的 Realm 的层级结构 ↓ ![shiro-realm-hierarchy](material/shiro-realm-hierarchy.png) # Authentication 身份验证, 即在应用中谁能证明他就是他本人. 一般提供如他们的身份 ID 一些标识信息来表明他就是他本人, 如提供身份证, 用户名 / 密码来证明. 在 Shiro 中, 用户需要提供 principals (身份)和 credentials (证明) 给框架, 从而应用能验证用户身份: - principals: 身份, 即主体的标识属性, 可以是任何东西: 如用户名, 邮箱等, 唯一即可. 一个主体可以有多个 principals, 但只有一个 Primary principals, 一般是用户名 / 密码 / 手机号. - credentials: 证明 / 凭证, 即只有主体知道的安全值, 如密码 / 数字证书等. 最常见的 principals 和 credentials 组合就是用户名 / 密码了. 接下来先进行一个基本的身份认证. 另外两个相关的概念是之前提到的 `Subject` 及 `Realm`, 分别是主体及验证主体的数据源. ![shiro-authentication-flow](material/shiro-authentication-flow.jpg) 流程如下: 1. 首先调用 Subject.login(token) 进行登录, 其会自动委托给 `SecurityManager`; 2. `SecurityManager` 负责真正的身份验证逻辑; 它会委托给 `Authenticator` 进行身份验证; 3. `Authenticator` 才是真正的身份验证者, Shiro API 中核心的身份认证入口点, 此处可以自定义插入自己的实现; 4. `Authenticator` 可能会委托给相应的 `AuthenticationStrategy` 进行多 Realm 身份验证, 默认实现是 `ModularRealmAuthenticator` 会调用 `AuthenticationStrategy` 进行多 Realm 身份验证; 5. `Authenticator` 会把相应的 token 传入 Realm, 从 Realm 获取身份验证信息, 如果没有返回 / 抛出异常表示身份验证失败了. 此处可以配置多个 `Realm`, 将按照相应的顺序及策略进行访问. ## Authenticator & AuthenticationStrategy `Authenticator` 的职责是验证用户帐号, 是 Shiro API 中身份验证核心的入口点. 如果验证成功, 将返回 `AuthenticationInfo` 验证信息; 此信息中包含了身份及凭证; 如果验证失败将抛出相应的 `AuthenticationException`. `SecurityManager` 接口继承了 `Authenticator `(默认 `ModularRealmAuthenticator` 实现), 其委托给多个 Realm 进行验证, 验证规则通过 `AuthenticationStrategy` 接口指定. 默认提供的实现: - **`FirstSuccessfulStrategy`**: 第一个 Realm 验证成功即可, 只返回第一个 Realm 身份验证成功的认证信息, 其他的忽略; - **`AtLeastOneSuccessfulStrategy`**: 有一个 Realm 验证成功即可, 和 `FirstSuccessfulStrategy` 不同, 返回所有 Realm 身份验证成功的认证信息; # Authorization 授权, 也叫访问控制. 即在应用中控制谁能访问哪些资源 (如访问页面 / 编辑数据 / 页面操作等). 在授权中需了解的几个关键对象: - **主体** (Subject): 即访问应用的用户, 在 Shiro 中使用 Subject 代表该用户. 用户只有授权后才允许访问相应的资源 - **资源** (Resource): 在应用中用户可以访问的任何东西, 比如访问 JSP 页面, 查看 / 编辑某些数据, 访问某个业务方法. 打印文本等等都是资源. 用户只要授权后才能访问 - **权限** (Permission): 安全策略中的原子授权单位, 通过权限我们可以表示在应用中用户有没有操作某个资源的权力. 即权限表示在应用中用户能不能访问某个资源. 如: 访问用户列表页面查看 / 新增 / 修改 / 删除用户数据 (即很多时候都是 CRUD (增查改删) 式权限控制) 打印文档等等. - 权限代表了用户有没有操作某个资源的权利, 即反映在某个资源上的操作允不允许, 不反映谁去执行这个操作. 所以后续还需要把权限赋予给用户, 即定义哪个用户允许在某个资源上做什么操作 (权限), Shiro 不会去做这件事情, 而是由实现人员提供. - 字符串通配符权限, 规则: "**资源标识符:操作:对象实例 ID**” 即对哪个资源的哪个实例可以进行什么操作. 其默认支持通配符权限字符串: `subject.checkPermissions("system:user:update", "system:user:delete")` - ":" 表示资源/操作/实例的分割; "," 表示操作的分割; "*" 表示任意资源/操作/实例; - "user:view" 等价于 "user:view:\*"; 而 "organization" 等价于 "organization:\*" 或者 `organization:*:*`. 可以这么理解: 这种方式实现了前缀匹配. "user:\*" 可以匹配 "user:delete"; "user:delete" 可以匹配 "user:delete:1"; "user:\*:1" 可以匹配 "user:view:1"; "user" 可以匹配 "user:view" 或 "user:view:1" 等: **即 \* 可以匹配所有, 不加 \* 可以进行前缀匹配**; 但是 "\*:view" 不能匹配 "system:user:view", 需要使用 `:*:view`: **即后缀匹配必须指定前缀 (多个冒号就需要多个 \* 来匹配)** - **角色** (Role): 代表了操作集合, 可以理解为权限的集合. 一般情况下我们会赋予用户角色而不是权限, 即这样用户可以拥有一组权限, 赋予权限时比较方便. 典型的如: 项目经理, 技术总监, CTO, 开发工程师等都是角色, 不同的角色拥有一组不同的权限; ![shiro-authorization-flow](material/shiro-authorization-flow.jpg) 流程如下: 1. 首先调用 Subject.isPermitted\* / hasRole\*接口, 其会委托给 `SecurityManager`, 而 `SecurityManager` 接着会委托给 `Authorizer`; 2. `Authorizer` 是真正的授权者, 如果我们调用如 isPermitted(“user:view”), 其首先会通过 `PermissionResolver` 把字符串转换成相应的`Permission` 实例; 3. 在进行授权之前, 其会调用相应的 `Realm` 获取 `Subject` 相应的角色 / 权限用于匹配传入的角色 / 权限; 4. `Authorizer` 会判断 `Realm` 的角色 / 权限是否和传入的匹配. 如果有多个 `Realm`, 会委托给 `ModularRealmAuthorizer` 进行循环判断. 如果匹配如 isPermitted\* / hasRole\*会返回 `true`, 否则返回 `false` 表示授权失败. `ModularRealmAuthorizer` 进行多 Realm 匹配流程: 1. 首先检查相应的 `Realm` 是否实现了实现了 `Authorizer`; 2. 如果实现了 `Authorizer`: 那么接着调用其相应的 isPermitted\* / hasRole\* 接口进行匹配; 3. 如果有一个 `Realm` 匹配那么将返回 `true`; 如果 Realm 进行授权的话, 应该继承 `AuthorizingRealm`, 其流程是: 1. 如果调用hasRole\*, 则直接获取 AuthorizationInfo.getRoles() 与传入的角色比较即可; 2. 如果调用如 isPermitted(“user:view”), 则通过 `PermissionResolver` 将权限字符串转换成相应的 `Permission` 实例 (默认实现为 `WildcardPermissionResolver`), 即转换为通配符的 `WildcardPermission`; 3. 通过 AuthorizationInfo.getObjectPermissions() 得到 `Permission` 实例集合; 通过 AuthorizationInfo.getStringPermissions() 得到字符串集合并通过 `PermissionResolver` 解析为 `Permission` 实例; 然后获取用户的角色, 并通过 `RolePermissionResolver` 解析角色对应的权限集合 (默认没有实现, 可以自己提供); 4. 接着调用 Permission.implies(Permission p) 逐个与传入的权限比较, 如果有匹配的则返回 `true` 授权方式有三种: 1. 编程式, 即在代码中硬编码: ```java Subject subject = SecurityUtils.getSubject(); if (subject.hasRole(“admin”)) { // ~ 有权限 } else { // ~ 无权限 } ``` 2. 注解方式, 通过在方法加上相应的注解实现: ```java @RequiresRoles("admin") public void hello() { // ~ 有权限 }; ``` 3. 在服务端渲染的模板语言中 (如 JSP, FreeMarker etc.) ```html ``` # Changelog 2020-10-10 11:26 - The Very First Version. 2021-04-06 10:27 - Init. 2021-07-11 23:50:50 - Add chapter.Realm, chapter.Authentication and chapter.Authorization.