目录

  1. Shiro 概述
  2. Shiro 入门
    1. shiro 基本功能点
    2. Shiro 核心架构
    3. subject
    4. Security Manager
    5. Authenticator
    6. Authrizer
    7. Realm
    8. SessionManager
    9. SessionDAO
    10. CacheManager
    11. Cryptography
  3. Shiro 认证
    1. shiro 认证的关键对象
    2. 认证流程
    3. 认证实现
  4. Shiro 授权
    1. 权限字符串
    2. 授权流程
    3. 授权实现
  5. 自定义 Realm
  6. Shiro 加密
  7. 附录

Shiro 概述

🤔什么是 shiro ?

Apache Shiro 是一个强大且易用的 Java 安全框架,具有身份验证、授权、密码和会话管理等功能。使用 Shiro 可以快速地开发安全的应用程序

✨Shiro 具有如下的特点

  • 易于使用:易用性是项目的最终目标,使用 Shiro 能够让没有安全开发经验的人员也能开发出安全的应用程序
  • 灵活:Apache Shiro 可以在任何应用程序环境中工作。虽然在网络工作、EJB 和 IoC 环境中可能并不需要它。但 Shiro 的授权也没有任何规范,甚至没有许多依赖关系
  • Web 支持:Apache Shiro 对 web 应用程序支持,允许您基于应用程序的 url 创建灵活的安全策略和网络协议(例如 REST),同时还提供一组 JSP 库控制页面输出
  • 低耦合:Shiro 干净的 API 和设计模式使它容易与许多其他框架和应用程序集成。Shiro 可以无缝地集成 Spring 这样的框架

Shiro 入门

如果想要使用 Shiro 就需要在项目中引入 Shiro 的依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>

shiro 基本功能点

功能名功能
Authentication身份认证/登录,验证用户是不是拥有相应的身份
Authorization授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情
Session Management会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是 Web 环境的
Cryptography加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
Web SupportWeb 支持,可以非常容易的集成到 Web 环境
Caching缓存,用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
ConcurrencyShiro 支持多线程应用程序
Testing提供测试支持
Run As允许一个用户假装为另一个用户的身份进行访问
Remember Me记住我,即一次登录后,下次再次访问就不需要登录

Shiro 核心架构

刚开始接触不必要全部记住,只需要了解到有哪些组件即可

subject

  • 应用代码直接交互的对象是 Subject, Shiro 的对外 API 核心是 Subject,Subject 代表了当前「用户」,这个用户不一定是一个具体的人,只要与当前应用交互的任何东西都是 Subject;
  • 与 Subject 的所有交互都会委托给 Security Manager;
  • Subject 其实是一个门面,SecurityManager 才是实际的执行者

Security Manager

Security Manager 相当于 SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏;所有具体的交互都通过 Security Manager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理、安全管理器,所有与安全有关的操作都会与 Security Manager 交互;

Authenticator

认证器,负责对 Subject 认证,可以自定义实现;其需要认证策略(Authentication Strategy)

Authrizer

授权器,或者访问控制器,用来决定 Subject 是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm

Shiro 从 Realm 获取安全数据(如用户、角色、权限),就是说 Security Manager 要验证用户角色,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 realm 看成 DataSource

可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;

🎶Shiro 不知道用户/权限存储在哪及以何种格式存储;所以一般在应用中都需要实现自己的 Realm

SessionManager

Session 需要人为管理它的生命周期,在 Shiro 中这个组件就是 Session Manager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有,Shiro 就抽象了一个自己的 Session 来管理 Subject 与应用之间交互的数据

SessionDAO

DAO——数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;例如:想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能

CacheManager

缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography

密码模块,Shiro 提供了一些常见的加密组件用于如密码加密/解密的

Shiro 认证

身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。

shiro 认证的关键对象

对象含义
Subject:主体访问系统的用户
Principal:身份信息是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)
credential:凭证信息是只有主体自己知道的安全信息,如密码、证书等

认证流程

  1. 首先调用 Subject.login(token) 进行登录认证,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils. setSecurityManager();

  2. SecurityManager 负责身份验证逻辑;它会委托给 Authenticator 进行身份验证;

  3. Authenticator 是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现

  4. Authenticator 可能会委托给相应的 Authentication Strategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证

  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问

认证实现

shiro.ini
1
2
3
[users]
zhangsan=123456
lisi=123456

📓配置文件的名称随意,主要以 .ini 结尾,放在 resources 目录下即可。在实际的项目开发中并不会使用配置文件的方式,这种方式主要是用来初学者学习使用

TestLoging.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testLogin() throws Exception {
//1.加载 ini 配置文件创建 SecurityManager
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2.获取 securityManager
SecurityManager securityManager = factory.getInstance();
//3.将 securityManager 绑定到当前运行环境
SecurityUtils.setSecurityManager(securityManager);
//4.创建主体(此时的主体还为经过认证)
Subject subject = SecurityUtils.getSubject();
//5.构造主体登录的凭证(即用户名/密码)
UsernamePasswordToken upToken = new UsernamePasswordToken("zhangsan", "123456");
//6.主体登录
subject.login(upToken);
//7.验证是否登录成功
System.out.println("用户登录成功=" + subject.isAuthenticated());
//8.登录成功获取数据
//getPrincipal 获取登录成功的安全数据
System.out.println(subject.getPrincipal());
}

Shiro 授权

Shiro 中的授权,就是对某个用户授予某些权限,如果使用 RBAC 模型,也就是对某些用户授予某些角色

shiro 授权和认证大体过程类似,不过是Authentication认证过程变成了Authorizer授权过程

权限字符串

权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源标识符、操作、资源实例的分割符,权限字符串也可以使用*通配符,下面列举了几个权限字符串的例子

  • 用户创建权限:user:create,或 user:create:*
  • 用户修改实例 001 的权限:user:update:001
  • 用户实例 001 的所有权限:user:*:001

授权流程

  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 表示授权失败

授权实现

shiro.ini
1
2
3
4
5
6
7
[users] #模拟从数据库查询的用户 #数据格式 用户名=密码,角色 1,角色 2..
zhangsan=123456,role1,role2
lisi=654321,role2
[roles] #模拟从数据库查询的角色和权限列表 #数据格式 角色名=权限 1,权限 2
role1=user:save,user:update
role2=user:update,user.delete
role3=user.find
TestPermission.Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testLogin() {
Subject subject = SecurityUtils.getSubject();
String username = "abc";
String password = "123456";
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
subject.login(usernamePasswordToken);
//验证用户是否登陆成功
System.out.println(subject.isAuthenticated());
//获取登陆成功的数据
System.out.println(subject.getPrincipals());
//判断 subject 是否拥有相关角色
System.out.println(subject.hasRole("role1"));
//判断 subject 是否拥有相关权限
System.out.println(subject.isPermitted("user:save"));
}

📓认证与授权的关系

自定义 Realm

自定义 Realm 的作用:放弃使用.ini 文件,使用数据库查询具体用户的权限等信息,上边的程序使用的是 Shiro 自带的 IniRealm。
IniRealm 从 ini 配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义 realm

shiro.ini
1
2
3
[main]
permRealm = cn.pineapple.shiro.PermissionRealm
securityManager.realms=$permRealm
PermissionRealm.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class PermissionRealm extends AuthorizingRealm {
@Override
public void setName(String name) {
System.out.println(name);
super.setName("permissionRealm");
}
/_授权:授权的主要目的对用户增加某些权限或角色_/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = (String) principalCollection.getPrimaryPrincipal();
System.out.println(username);
List<String> permissions = new ArrayList<>();
permissions.add("user:save");
permissions.add("user:update");
List<String> roles = new ArrayList<>();
roles.add("role1");
roles.add("role2");
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissions);
simpleAuthorizationInfo.addRoles(roles);
return simpleAuthorizationInfo;
}
/_ 认证:认证的主要目的,比较用户输入的用户名密码是否和数据库中的一致_/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
String password = String.valueOf(usernamePasswordToken.getPassword());
if (!"123456".equals(password)) {
throw new RuntimeException("username or password error");
} else {
return new SimpleAuthenticationInfo(username, password, this.getName());

}

}
}
Shiro 中 Realm 的继承体系
SimpleAccountRealm 的继承体系

SimpleAccountRealm 的部分源码中有两个方法一个是「认证」一个是「授权」

Shiro 加密

在之前的操作中,在数据库中保存的密码都是明文的,一旦数据库数据泄露,就会造成不可估算的损失,所以通常都会使用非对称加密,等其他安全的手段保护用户的密码。通过加密算法后,就可以将加密的字符串保存在数据库中,等下次用户登陆时将密码通过同样的算法加密后再从数据库中取出这个字符串进行比较,就能够知道密码是否正确了。这样既保留了密码验证的功能又大大增加了安全性。

1
2
3
4
5
public void testMd5() {
String password = "123456";
String encodedPassword = new Md5Hash(password).toString();
System.out.println(encodedPassword);
}

MD5是一种 hash 算法,根据哈希的特性,相同的输入,必然拥有相同的输出,所以就需要让原始密码再加上一个随机数,然后再进行 MD5 哈希,这种随机数也就是通常说的盐(salt),当然需要将盐也保存进数据库,以便后续进行验证。

1
2
3
4
5
6
7
8
String password = "123456";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times = 2; // 加密次数:2
String alogrithmName = "md5"; // 加密算法

String encodePassword = new SimpleHash(alogrithmName, password, salt, times).toString();

System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodePassword);

另外我们可以通过多次加密的方法,即使黑客通过一定的技术手段拿到了我们的密码 md5 值,但它并不知道我们到底加密了多少次,所以这也使得破解工作变得艰难

附录

Shiro 安全框架【快速入门】就这一篇