文章目录
资料
概念
基本功能:
以下内容翻译自官网:https://shiro.apache.org/introduction.html
Shiro的目标是Shiro开发团队所说的“应用程序安全的四个基石” - 身份验证,授权,会话管理和密码学:
-
认证:有时称为“登录”,这是证明用户是他们所说的身份的行为。
-
授权:访问控制的过程,即确定“谁”可以访问“什么”。
-
会话管理:管理特定于用户的会话,即使在非 Web 或 EJB 应用程序中也是如此。
-
密码学:使用加密算法确保数据安全,同时仍然易于使用。
在不同的应用程序环境中,还有一些其他功能可以支持和加强这些问题,特别是:
-
Web 支持:Shiro 的 Web 支持 API 有助于轻松保护 Web 应用程序。
-
缓存:缓存是Apache Shiro的API中的一级公民,以确保安全操作保持快速高效。
-
并发:Apache Shiro支持多线程应用程序及其并发功能。
-
测试:测试支持可帮助您编写单元测试和集成测试,并确保代码按预期得到保护。
“运行方式”:允许用户假定其他用户的身份(如果允许)的功能,有时在管理方案中很有用。
“记住我”:跨会话记住用户的身份,以便他们只需要在强制要求时登录。
架构原理
以下内容翻译自官网:https://shiro.apache.org/architecture.html
-
Subject:正如我们在教程中提到的,Subject本质上是当前执行用户的特定于安全的“视图”。“用户”一词通常指的是人,而主体可以是人,但它也可以代表第三方服务、守护帐户、cron作业或任何类似的东西——基本上是当前与软件交互的任何东西。
主题实例都绑定到(并且需要)SecurityManager。当您与Subject交互时,这些交互将转换为与SecurityManager的特定于主题的交互。 -
SecurityManager: SecurityManager是Shiro架构的核心,充当一种“保护伞”对象,协调内部安全组件,共同形成对象图。然而,一旦为应用程序配置了SecurityManager及其内部对象图,它通常就不会受到影响,应用程序开发人员几乎把所有时间都花在Subject API上。
我们将在后面详细讨论SecurityManager,但重要的是要认识到,当您与Subject交互时,实际上是幕后的SecurityManager为任何Subject安全操作做了所有繁重的工作。这反映在上面的基本流程图中。 -
Realms: Realms充当Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当需要实际与安全相关数据(如用户帐户)交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个realm中查找其中的许多内容。
从这个意义上讲,Realm本质上是一个特定于安全的DAO:它封装了数据源的连接细节,并根据需要将相关数据提供给Shiro。配置Shiro时,必须指定至少一个Realm用于身份验证和/或授权。SecurityManager可以配置多个realm,但至少需要一个。
-
Subject:当前与软件交互的实体(用户、第三方服务、cron作业等)的特定于安全的“视图”。
-
SecurityManager:如前所述,SecurityManager是Shiro架构的核心。它主要是一个“伞形”对象,用于协调其托管组件,以确保它们顺畅地协同工作。它还管理Shiro对每个应用程序用户的视图,因此它知道如何对每个用户执行安全操作。
-
Authenticator: Authenticator是负责执行和响应用户的身份验证(登录)尝试的组件。当用户尝试登录时,Authenticator将执行该逻辑。验证者知道如何与一个或多个存储相关用户/帐户信息的域进行协调。从这些领域获得的数据用于验证用户的身份,以确保用户确实是他们所说的那个人。
- Authentication Strategy:如果配置了多个Realm, AuthenticationStrategy将协调各个Realm以确定身份验证尝试成功或失败的条件(例如,如果一个域成功而其他域失败,那么尝试成功吗?所有领域都必须成功吗?只有第一个?)
-
Authorizer :授权器是负责确定应用程序中用户访问控制的组件。它是一种机制,最终决定是否允许用户做某事。与验证者一样,授权者也知道如何与多个后端数据源协调以访问角色和权限信息。授权人使用此信息来确定是否允许用户执行给定的操作。
-
SessionManager: SessionManager知道如何创建和管理用户会话生命周期,为所有环境中的用户提供健壮的会话体验。这是安全框架世界中的一个独特特性——Shiro能够在任何环境中本地管理用户会话,即使没有可用的Web/Servlet或EJB容器。默认情况下,Shiro将使用现有的会话机制(例如Servlet容器),但如果没有,例如在一个独立的应用程序或非web环境中,它将使用其内置的企业会话管理来提供相同的编程体验。SessionDAO的存在是为了允许使用任何数据源持久化会话。
- SessionDAO代表SessionManager执行会话持久性(CRUD)操作。这允许将任何数据存储插入到Session Management基础设施中。
-
CacheManager (org.apache.shiro.cache.CacheManager)创建并管理其他Shiro组件使用的Cache实例生命周期。由于Shiro可以访问许多后端数据源进行身份验证、授权和会话管理,因此缓存一直是该框架中的一流体系结构特性,可以在使用这些数据源时提高性能。任何现代开源和/或企业缓存产品都可以插入到Shiro中,以提供快速高效的用户体验。
-
Cryptography :是企业安全框架的自然补充。Shiro的加密包包含易于使用和理解的加密密码,哈希(又名摘要)和不同的编解码器实现的表示。这个包中的所有类都经过精心设计,非常易于使用和理解。任何使用过Java原生加密支持的人都知道,它是一种难以驯服的动物。Shiro的加密api简化了复杂的Java机制,使加密技术易于为普通人使用。
-
Realms:如上所述,Realms是Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当需要实际与安全相关数据(如用户帐户)交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个realm中查找其中的许多内容。您可以根据需要配置任意数量的realm(通常每个数据源一个),Shiro将根据需要与它们协调进行身份验证和授权。
登录认证
概念
(1)身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供email,用户名/密码来证明。
(2)在shiro中,用户需要提供principals(身份)和credentials (证明
给shiro,从而应用能验证用户身份
(3) principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary’ principals,一般是用户名/邮箱/手机号。
(4) credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
(5)最常见的principals和crqdentials组合就是用户名/密码
流程
角色授权
概念
(1)授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面
编辑数据/页面澡作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)
(2)主体(Subject):访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。
(3)资源(Resource):在应用中用户可以访问的· URL,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
(4)权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD(增查改删)式权权限控制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许。
(5)Shiro支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权艮,即实例级别的)
(6)角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程币等都是角色,不同的角色拥有一组不同的权限
流程
代码
大致流程
shiro配置解读(ShiroConfig
)
1、自定义realm:
- MyRealm是一个自定义类,继承AuthorizingRealm类
2、创建安全管理器
- 这里需要用到刚刚创建的realm
3、ShiroFilter过滤器
- 需要用到第2步的安全管理器
- 下图中map的value可取值:
- anon:无需认证就可以访问
- authc:必须认证了才能让问
- user:必须拥行记住我功能才能用
- perms:圳有对某个资源的权限才能访问;
- role:拥有某个角色权限才能访问
- logout:设置登出的路径
登录/认证:
我们在配置类配置了登录页的地址
当访问登录页,或访问某个需要认证才能访问的资源时,就会跳转到登录页,然后进入登录认证流程,大致流程为:
1、填写登录表单(login.html
)
2、来到controller:(UserController
)
- 在这里,我们需要获取到加密后的密码,并执行subject.login方法
3、来到自定义Realm:(MyRealm
)
- 自定义Realm实现AuthorizingRealm
- 认证对应于doGetAuthenticationInfo方法,另一个是授权用的
- 在这里,我们只需告诉shiro真实的密码应该是数目即可。
- SimpleAuthenticationInfo的参数:
- 第一个,类似于身份标识符,在这存什么,在其他类就能取到什么
- 第二个,用户对应的密码
- 第三个,空串即可
- 如果这里传的密码,和第2步controller里面那传的密码相同就认证成功(shiro来完成)
授权
1、配置了需要某个权限才能访问的资源:(两种方式)
- 在配置类中配置:(
ShiroConfig
)
- 使用注解配置:(
UserController
)
2、当访问对应资源时,将会来到自定义Realm:(MyRealm
)
- 对应的方法是:doGetAuthorizationInfo
- 我们只用告诉shiro该用户有哪些权限即可,其他的由shiro来完成
详细代码:
pom
- (spring-boot我用的2.2.6.RELEASE)
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
提示:用户名任意,密码是1234即可,root用户有user:*权限<br>
username:<input name="username"><br>
username:<input name="password"><br>
<input type="submit">
</form>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<hr>
<a href="/user/add">添加用户</a>|
<a href="/user/delete">删除用户</a>|
<a href="/user/logout">退出登录</a>
</body>
</html>
UserController
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@Slf4j
public class UserController {
/**
* 去填写登录表单
*
* @return
*/
@GetMapping({"/user/login", "/"})
public String login() {
return "redirect:/login.html";
}
/**
* 去首页(需要先登录,具体的请看:ShiroConfig)
*
* @return
*/
@GetMapping("/user/index")
public String index() {
return "redirect:/index.html";
}
/**
* 添加用户(需要user:add权限),现假设有权限就返回添加成功。使用配置类ShiroConfig实现。
* 相关代码:map.put("/user/add","perms[user:add]");
*/
@GetMapping("/user/add")
@ResponseBody
public String add() {
return "添加成功...";
}
/**
* 删除用户(需要user:delete权限),现假设有权限就返回删除成功。使用注解实现。
* 相关注解:@Requires*
*/
@GetMapping("/user/delete")
@ResponseBody
@RequiresPermissions("user:delete")
public String delete() {
return "删除成功...";
}
/**
* 登录(认证),具体逻辑在:MyRealm
*
* @param username
* @param password
* @return
*/
@PostMapping("/user/login")
public String login(String username, String password) {
try {
//获取加密后的密码
password = new Md5Hash(password, "salt", 3).toHex();
//获取主题对象
Subject subject = SecurityUtils.getSubject();
//登录(第三个参数,是否记住我)
subject.login(new UsernamePasswordToken(username, password));
System.out.println(username + ",登录成功!!!");
return "redirect:/index.html";
}
//以下是可能出现的异常...
catch (UnknownAccountException uae) {
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException ice) {
System.out.println("密码不正确");
} catch (LockedAccountException lae) {
System.out.println("帐户锁定异常");
} catch (AuthenticationException ae) {
System.out.println("其他登录异常...");
}
return "redirect:/login.html";
}
}
ShiroConfig
import com.ljy.realm.MyRealm;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* ShiroFilter过滤所有请求
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给ShiroFilter配置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
//配置系统公共资源、受限资源
Map<String, String> map = new HashMap<>();
/**
* anon:无需认证就可以访问
* authc:必须认证了才能让问
* user:必须拥行记住我功能才能用
* perms:圳有对某个资源的权限才能访问;
* role:拥有某个角色权限才能访问
* logout:设置登出的路径
*/
//无需认证
map.put("/user/login","anon");
//需要认证(登录)
map.put("/user/index","authc");
//需要有某个权限
map.put("/user/add","perms[user:add]");
//设置登出的路径
map.put("/user/logout","logout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 设置认证(登录)界面路径
shiroFilterFactoryBean.setLoginUrl("/user/login");
return shiroFilterFactoryBean;
}
/**
* 创建安全管理器
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm,也可以通过setRealms设置多个
securityManager.setRealm(realm);
//设置认证策略示例:
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
//有三种策略:
// AtLeastOneSuccessfulStrategy(至少有一个realm成功)
// FirstSuccessfulStrategy(有一个成功即可,后面的将被忽略)
// AllSuccessfulStrategy(所有的都要成功)
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return securityManager;
}
/**
* 创建自定义Realm
*/
@Bean
public Realm realm() {
return new MyRealm();
}
}
MyRealm
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义Realm
*/
public class MyRealm extends AuthorizingRealm {
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//获取用户名
String username = (String) SecurityUtils.getSubject().getPrincipal();
//根据用户名查权限
List<String> perms = getPermsByUsername(username);
if (perms != null && !perms.isEmpty()){
info.addStringPermissions(perms);
}
return info;
}
/**
* 认证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
//根据用户名获取对应的密码
String password = getPasswordByUsername(username);
//剩下的事,交给shiro
//第一个参数,是一个对象(例如,可以使用Admin、User等),在其它类中可以使用SecurityUtils.getSubject().getPrincipal()获取。
// 这里为了简单,将用户名作为主题
return new SimpleAuthenticationInfo(username, password, "");
}
/**
* 根据用户名查密码。一般通过Service、Dao查询数据库进行实现。
* 这里为了简单,将密码同意设置为1234(md5加密)
*
* @param username
* @return
*/
public String getPasswordByUsername(String username) {
//加盐,盐值是“salt”,迭代三次
return new Md5Hash("1234", "salt", 3).toHex();
}
/**
* 根据用户名查询用户的权限。一般通过Service、Dao查询数据库进行实现。
* 这里为了简单,就给root用户user:*权限
* @param principal
* @return
*/
public List<String> getPermsByUsername(String principal) {
if (principal.equals("root")){
ArrayList<String> list = new ArrayList<>();
list.add("user:*");
return list;
}
return null;
}
}