简介

Apache Shiro 是一个功能强劲且易于使用的Java安全框架,可执行身份验证、授权、加密和会话管理。有了Shiro易于理解的API,您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的web和企业应用程序。
特色
Shiro的目标是Shiro开发团队所称的“应用程序安全的四大基石”——认证、授权、会话管理和加密:

- Authentication: 有时也被称为“登录”,这是一种证明用户是谁的行为。
- Authorization: 访问控制的过程,即确定“谁”可以访问“什么”。
- Session Management: 管理特定于用户的会话,甚至是非web或EJB应用程序
- Cryptography: 使用加密算法确保数据安全,同时依旧易于使用。
在不同的应用程序环境中,还提供了一些额外的特性来支持和加强这些关注点,特别是:
- Web Support: Shiro的web支持api有助于轻松地保护web应用程序。
- Caching: 在Apache Shiro的API中,缓存是第一层的,以确保安全操作保持快速和高效。
- Concurrency: Apache Shiro通过其并发特性支持多线程应用程序。
- Testing: 测试支持可以协助您编写单元测试和集成测试,并确保您的代码如预期的那样安全。
- “Run As”:允许用户使用另一个用户身份的特性(如果允许的话),有时在管理场景中很有用。
- “Remember Me”:记住用户跨会话的身份,这样他们只需要在强制时登录。
系统架构
从外部来看Shiro ,即从应用程序角度的来观察如何使用 Shiro 完成
Apache Shiro的设计目标是通过直观和易于使用来简化应用程序的安全性。Shiro的核心设计模拟了大多数人对应用程序安全性的见解——在某人(或某物)与应用程序交互的上下文中。
软件应用程序一般是基于用户故事设计的。也就是说,您一般会根据用户将如何(或应该如何)与软件交互来设计用户界面或服务api。例如,您可能会说:“如果与我的应用程序交互的用户已经登录,我将向他们显示一个按钮,他们可以单击该按钮来查看自己的帐户信息。如果他们没有登录,我会显示一个注册按钮。”
这个示例语句表明,编写应用程序主要是为了满足用户的需求。即使“用户”是另一个软件系统而不是人,您依旧需要编写代码来反映当前与您的软件交互的人(或物)的行为。
Shiro在自己的设计中反映了这些概念。通过匹配软件开发人员已经具备的直觉性,Apache Shiro在实际应用程序中依旧保持直觉性,易于使用。
在最高的概念层次上,Shiro的架构有3个主要概念:主题(Subject)、安全管理器(SecurityManager)和领域(realm)。下面的图表是这些组件如何交互的高级概述,我们将在下面讨论每个概念:

- Subject: 正如我们在教程中提到的,Subject本质上是当前执行用户的一个安全特定的“视图”。虽然“用户”一词一般意味着人,但Subject可以是一个人,但它也可以代表第三方服务、守护帐户、cron作业或任何类似的东西——基本上是当前与软件交互的任何东西。
- Subject实例都绑定到(并需要)SecurityManager。当您与Subject交互时,这些交互转换为与SecurityManager的特定于主题的交互
- SecurityManager: SecurityManager是Shiro架构的核心,充当一种“保护伞对象”,协调构成对象图的内部安全组件。不过,一旦SecurityManager和它的内部对象图为应用程序配置好后,一般就不会再去管它了,应用程序开发人员将几乎所有的时间都花在Subject API上。我们将在稍后详细讨论SecurityManager,但重大的是要认识到,当您与Subject交互时,实际上是SecurityManager在幕后为任何Subject安全操作执行所有繁重的工作。这反映在上面的基本流程图中。
- Realms: Realms充当Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当需要与用户账户等安全相关数据进行实际交互以执行认证(登录)和授权(访问控制)时,Shiro会从一个或多个应用配置的Realms中查找这些信息。
- 从这个意义上说,Realm本质上是一个特定于安全的DAO:它封装了数据源的连接细节,并使Shiro可以根据需要使用相关的数据。在配置Shiro时,必须至少指定一个Realm用于身份验证和/或授权。SecurityManager可以配置多个realm,但至少需要一个。
核心架构
• Subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外
API 核心就是 Subject。Subject 代表了当前“用户”,与 Subject 的所有交互都会委托给 SecurityManager;
• SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager 交互;且其管理着所有 Subject;可以看出它是 Shiro的核心,
它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 中DispatcherServlet 的角色
• Realm:Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;
也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;
可以把 Realm看成DataSource

- Subject(org.apache.shiro.subject.Subject) 当前与软件交互的实体(用户、第三方服务、cron作业等)的安全特定的“视图”。
- SecurityManager (org.apache.shiro.mgt.SecurityManager) 如上所述,SecurityManager是Shiro架构的核心。它主要是一个“保护伞”对象,协调其托管组件,以确保它们顺利地一起工作。它还管理Shiro对每个应用程序用户的视图,因此它知道如何对每个用户执行安全操作。
- Authenticator (org.apache.shiro.authc.Authenticator) Authenticator是负责执行和响应用户的身份验证(登录)尝试的组件。当用户尝试登录时,该逻辑由Authenticator执行。Authenticator知道如何与一个或多个存储相关用户/账户信息的Realms进行协调。从这些领域获得的数据用于验证用户的身份,以确保用户的确 是他们所说的那个人。Authentication Strategy (org.apache.shiro.authc.pam.AuthenticationStrategy)如果配置了多个Realm,AuthenticationStrategy将协调Realm以确定认证尝试成功或失败的条件(例如,如果一个Realm成功但其他领域失败,尝试是否成功?所有的realms都必须成功吗?只有第一个?)
- Authorizer (org.apache.shiro.authz.Authorizer) 授权者是负责确定用户在应用程序中的访问控制的组件。这是一种最终决定用户是否可以做某事的机制。与Authenticator一样,Authorizer也知道如何与多个后端数据源协调以访问角色和权限信息。授权方使用此信息来确定是否允许用户执行给定的操作
- SessionManager (org.apache.shiro.session.mgt.SessionManager)SessionManager知道如何创建和管理用户会话生命周期,为所有环境中的用户提供健壮的会话体验。这是安全框架世界中的一个独特特性——Shiro能够在任何环境中本地管理用户会话,即使没有可用的Web/Servlet或EJB容器。默认情况下,Shiro将使用现有的会话机制(如Servlet容器),但如果没有,列如在独立应用程序或非web环境中,它将使用其内置的企业会话管理来提供一样的进程SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO) SessionDAO代表SessionManager执行会话持久性(CRUD)操作。这允许将任何数据存储插入到会话管理基础设施中。
- CacheManager (org.apache.shiro.cache.CacheManager) CacheManager创建和管理其他Shiro组件使用的缓存实例生命周期。由于Shiro可以访问许多后端数据源来进行身份验证、授权和会话管理,缓存一直是框架中的一流架构特性,可以在使用这些数据源时提高性能。任何现代开源和/或企业缓存产品都可以插入到Shiro中,以提供快速高效的用户体验。
- Cryptography (org.apache.shiro.crypto.*) Cryptography 是对企业安全框架的自然补充。Shiro的crypto包包含了易于使用和理解的密码学密码学的表明,哈希(又名摘要)和不同的编解码器实现。这个包中的所有类都经过精心设计,易于使用和理解。任何使用过Java本地密码学支持的人都知道,驯服它是一件很有挑战性的事情。Shiro的加密api简化了复杂的Java机制,使加密技术易于普通人类使用。Realms (org.apache.shiro.realm.Realm) 如上所述,Realms充当Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当需要与用户账户等安全相关数据进行实际交互以执行认证(登录)和授权(访问控制)时,Shiro会从一个或多个应用配置的Realms中查找这些信息。你可以配置任意数量的Realms(一般每个数据源一个),Shiro会根据身份验证和授权的需要对它们进行协调。
身份验证流程
我们已经从架构一章中获得了前面的架构图,只突出显示了与身份验证相关的组件。每个数字表明身份验证尝试期间的一个步骤:

- 应用程序代码调用Subject。登录方法,传入构造的代表最终用户主体和凭证的AuthenticationToken实例。
- Subject实例,一般是一个DelegatingSubject(或子类),通过调用SecurityManager.login(token)委托给应用程序的SecurityManager,这是实际身份验证工作开始的地方。
- 作为一个基本的“保护伞”组件,SecurityManager接收令牌,并通过调用Authenticator .authenticate(token)简单地将其委托给内部的Authenticator实例。这几乎总是一个ModularRealmAuthenticator实例,它支持在身份验证期间协调一个或多个Realm实例。ModularRealmAuthenticator本质上为Apache Shiro提供了一个PAM风格的范例(在PAM术语中,每个Realm都是一个“模块”)
- 如果为应用程序配置了多个Realm, ModularRealmAuthenticator实例将利用其配置的AuthenticationStrategy启动一个多领域身份验证尝试。在Realm被调用进行身份验证之前、期间和之后,AuthenticationStrategy将被调用,以允许它对每个Realm的结果做出反应。我们将很快讨论认证策略。
- 每个配置的Realm都会被咨询,看看它是否支持提交的AuthenticationToken。如果是这样,支持Realm的getAuthenticationInfo方法将被提交的令牌调用。getAuthenticationInfo方法有效地表明了针对特定领域的一次身份验证尝试。我们将很快讨论Realm身份验证行为。
权限注解
• @RequiresAuthentication:表明当前Subject已经通过login 进行了身份验证;即 Subject. isAuthenticated() 返回 true
• @RequiresUser:表明当前 Subject 已经身份验证或者通过记住我登录的。
•@RequiresGuest:表明当前Subject没有身份验证或通过记住我登录过,即是游客身份。
• @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND):表明当前 Subject 需要角色 admin 和user
• @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR):表明当前 Subject 需要权限 user:a 或
user:b。
过滤器拦截权限
|
类别 |
class |
说明 |
|
anon |
org.apache.shiro.web.filter.authc.AnonymousFilter |
允许立即访问路径而不执行任何类型的安全检查的过滤器。 |
|
authc |
org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
要求请求用户通过身份验证才能继续请求,如果没有,则通过将用户重定向到您配置的loginUrl强制用户登录。 |
|
authcBasic |
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
需要对请求用户进行身份验证才能继续请求,如果没有,则需要用户通过HTTP Basic协议特定的挑战登录。成功登录后,允许他们继续访问请求的资源/url。 |
|
authcBearer |
org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter |
要求请求用户通过身份验证才能继续请求,如果没有,则要求用户通过特定于HTTP承载协议的挑战登录。成功登录后,允许他们继续访问请求的资源/url。 |
|
invalidRequest |
org.apache.shiro.web.filter.InvalidRequestFilter |
拦截恶意请求的请求过滤器。无效的请求将以400响应代码进行响应。 |
|
logout |
org.apache.shiro.web.filter.authc.LogoutFilter |
简单的过滤器,当接收到请求时,将立即注销当前执行的主题,然后将它们重定向到一个配置的redirectUrl。 |
|
noSessionCreation |
org.apache.shiro.web.filter.session.NoSessionCreationFilter |
一个PathMatchingFilter,它将禁用在请求期间创建新的会话。这是一个超级有用的过滤器,可以放在任何过滤器链的前面,这些过滤器链可能导致REST、SOAP或其他不打算参与会话的服务调用。 |
|
perms |
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
筛选器,如果当前用户具有映射值指定的权限,则允许访问;如果用户没有指定的所有权限,则拒绝访问。 |
|
port |
org.apache.shiro.web.filter.authz.PortFilter |
要求请求在特定端口上的过滤器,如果不是,则重定向到该端口上的一样URL。 |
|
rest |
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
过滤器将HTTP请求的方法(如GET、POST等)转换为相应的动作(动词),并使用该动词构造一个将被检查以确定访问的权限。 |
|
roles |
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
如果当前用户具有映射值指定的角色,则允许访问;如果用户没有指定的所有角色,则拒绝访问。 |
|
ssl |
org.apache.shiro.web.filter.authz.SslFilter |
要求请求通过SSL的过滤器。如果通过配置的服务器端口和request. issecure()接收到请求,则允许访问。如果任何一个条件为假,则过滤器链将不再继续。 |
|
user |
org.apache.shiro.web.filter.authc.UserFilter |
如果访问器是已知用户(定义为具有已知主体),则允许访问资源的筛选器。这意味着任何通过“记住我”功能被验证或记住的用户将被允许从这个过滤器访问。 |
SpringBoot整合Shiro
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<optional>true</optional>
</dependency>
自定义Realm
package com.dw.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: daWang
* @Date: 2022/4/22 16:24
*/
@Slf4j
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
log.info("-----授权------");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//这个对象对应 SimpleAuthenticationInfo 中的
Object primaryPrincipal = principals.getPrimaryPrincipal();
log.info("授权对象内容: {}",primaryPrincipal);
//查询数据库,把用户的角色和权限查询出来
//用户表users users_role 用户角色表 roles表 menu表 roles_menu表
//查询 users_role 和 roles表 查询用户的角色
//然后根据角色编号查询menu表 和 roles_menu表 权限查询出来
//角色
List<String> list = new ArrayList<String>();
//权限
List<String> perms = new ArrayList<String>();
perms.add("sys:user:*");//
perms.add("sys:menu:select");//
perms.add("sys:role:select");//
simpleAuthorizationInfo.addStringPermissions(perms);//Collection<String>
simpleAuthorizationInfo.addRoles(list);//Collection<String>
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("-----认证------");
// Subject subject = SecurityUtils.getSubject();
// subject.login(token);
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//先得到用户输入的用户名和密码
String username = token.getUsername();
//public char[] getPassword() {}
String password = new String(token.getPassword());
//从数据库查询
//假数据模拟
String u = "admin";
String p = "df655ad8d3229f3269fad2a8bab59b6c";
int status = 1;//正常 0 冻结
if (!username.equals(u)){
throw new UnknownAccountException("账户不存在!");
}
if (!password.equals(p)){
throw new IncorrectCredentialsException("密码错误!");
}
if (status==0){
throw new LockedAccountException("账户被冻结!");
}
//public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName) {
/**
* 参数一:一般是用户对象
* 参数二:密码
* 参数三:自定义realm名称
*/
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,password,this.getName());
return info;
}
}
配置类
package com.dw.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.spring.config.ShiroAnnotationProcessorConfiguration;
import org.apache.shiro.spring.config.ShiroBeanConfiguration;
import org.apache.shiro.spring.config.ShiroConfiguration;
import org.apache.shiro.spring.config.web.autoconfigure.ShiroWebFilterConfiguration;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroRequestMappingConfig;
import org.apache.shiro.spring.web.config.ShiroWebConfiguration;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import sun.security.krb5.RealmException;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
/**
* @Author: daWang
* @Date: 2022/4/22 13:45
*/
@Configuration
public class ShiroConfig {
// 配置自定义Realm
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
//userRealm.setCredentialsMatcher(credentialsMatcher()); //配置使用哈希密码匹配
return userRealm;
}
//权限管理器
@Bean(name = "securityManager")
public DefaultWebSecurityManager getSecurityManager(UserRealm userRealm){
DefaultWebSecurityManager manager =new DefaultWebSecurityManager();
manager.setRealm(userRealm);
return manager;
}
//创建过滤器工厂
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager, ShiroFilterChainDefinition filterChainDefinition){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
bean.setLoginUrl("/not/login");
bean.setFilterChainDefinitionMap(filterChainDefinition.getFilterChainMap());
return bean;
}
// 配置url过滤器
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
chainDefinition.addPathDefinition("/logout","anon");
chainDefinition.addPathDefinition("/not/login","anon");
// all other paths require a logged in user
chainDefinition.addPathDefinition("/login","anon");
chainDefinition.addPathDefinition("/**", "authc");
return chainDefinition;
}
}
Controller
package com.dw.controller;
import com.fasterxml.jackson.databind.util.JSONPObject;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: daWang
* @Date: 2022/4/24 9:21
*/
@Controller
public class UserController {
@RequiresRoles(value = {"admin"},logical = Logical.OR)
@GetMapping("/query")
@ResponseBody
public Map query(){
HashMap<String, Object> map = new HashMap<>();
map.put("code",200);
map.put("msg","详细");
return map;
}
@GetMapping("/not/login")
@ResponseBody
public Map notLogin(){
HashMap<String, Object> map = new HashMap<>();
map.put("code",201);
map.put("msg","您还未登陆");
return map;
}
@GetMapping("/login")
@ResponseBody
public Map login(String userName, String passWord ) {
//shiro认证
UsernamePasswordToken token = new UsernamePasswordToken(userName, passWord);
//得到Subject对象
Subject subject = SecurityUtils.getSubject();
HashMap<String, Object> map = new HashMap<>();
try {
subject.login(token);
map.put("code", 200);
map.put("msg", "login success");
return map;
} catch (UnknownAccountException e) {
map.put("code", 201);
map.put("msg", e.getMessage());
return map;
} catch (IncorrectCredentialsException e) {
map.put("code", 201);
map.put("msg", e.getMessage());
return map;
} catch (LockedAccountException e) {
map.put("code", 201);
map.put("msg", e.getMessage());
return map;
}
}
}
ControllerAdvice
package com.dw.controller;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: daWang
* @Date: 2022/4/25 11:19
*/
@Slf4j
@ResponseBody
@ControllerAdvice
public class ExceptionController {
private Map createMap(){
return new HashMap();
}
@ExceptionHandler(Exception.class)
public Map handler(Exception e){
log.error("{}",e.getMessage());
Map map = createMap();
map.put("code",202);
map.put("msg","操作错误");
return map;
}
@ExceptionHandler(AuthorizationException.class)
public Map handler(AuthorizationException e){
log.error("{}",e.getMessage());
Map map = createMap();
map.put("code",201);
map.put("msg","您没有操作权限");
return map;
}
}


