目录
0. 版本
- 使用的版本为:
1.5.3
2020-5-3 号发布。
1. 权限管理
1.1 什么是权限管理系统
按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
1.2 什么是身份认证
身份认证:判断一个用户是否为系统的合法用户。
1.3 什么是授权
授权:对于身份认证通过的用户,判断用户对某资源是否有访问的权限。
2. Shiro 架构
-
Apache Shiro
是一个安全框架。 - 支持:认证,授权,session 管理(即使是非web也可以使用),加解密,web集成,缓存等。
- 从外部来看
Shiro
,即从应用的角度来观察如何使用shiro
:
应用代码使用 subject
来认证授权等操作。Realm
是一个和持久层打交道的接口,代表认证授权信息的来源,比如 ini
配置文件,MySQL
数据库等。
- 从
Shiro
的内部来看:
2.1 Subject
主体,外部应用与Subject
实例进行交互,Subject
当中记录了当前操作用户,将用户的概念理解成当前操作的主体、可能是一个通过浏览器访问本系统的用户、也可能是一个运行的程序。Subject
在 Shiro
中是一个接口,接口中定义了很多认证授权相关的方法,外部程序通过Subject
进行认证授权,Subject
进一步通过SecurityManager
安进行认证和授权。
2.2 SecurityManager
安全管理器:对所有的Subject
进行管理,SecurityManager
通过调用Authenticator
认证管理 和 Authorizer
进行授权管理,通过 SessionManager
进行会话管理。
2.3 Authenticator
认证器:验证身份,Shiro
默认提供 ModularRealmAuthenticator
实现类,基本满足大部分需求,也可以自定义认证器件。
2.4 Authorizer
授权器:认证用户访问某个资源的时候是否具有权限。
3.1 Shiro 认证 (简单Demo)
3.1 第一个认证程序
3.1.1 认证中的关键对象
-
Subject
: 主体,访问系统的用户的抽象。 -
Pricipal
:身份信息,主体进行身份认证的标识,具有唯一性,如用户名、手机号、邮箱地址等。一个主体可以拥有多个身份信息,但是必有只有一个主身份。 -
Credential
:只有主体自己知道的安全信息,如密码,凭证等。
3.1.2 认证流程
-
Subject
将身份信息和凭证信息封装成一个令牌,通过SecurityManager
去认证。 -
SecurityManager
调用Authenticator
、Authenticator
调用Relam
去获得原始数据进行比较。 - 比较成功则认证成功, 否则抛出异常认证失败。
3.1.3 引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
3.1.4 编写 ini
Resources/shiro.ini
[users]
zch=123
wangnan=456
3.1.5 编写Java代码
public class TestAuthenticator {
public static void main(String[] args) {
// 1. 创建 SecurityManager
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2. 给安全管理器设置Relm
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 3. SecurityUtils 全局的安全工具类,提供认证和退出功能, 其依然调用的是 securityManager
SecurityUtils.setSecurityManager(securityManager);
// 4. 获取当前用户的主体
Subject subject = SecurityUtils.getSubject();
// 5. 创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("zch", "123");
// 6. 身份认证
try {
subject.login(token);
System.out.println("认证成功" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
System.out.println("用户不存在");
} catch (IncorrectCredentialsException e) {
System.out.println("用户名密码错误");
}
}
}
3.2 自定义认证Realm
目的:将认证 / 授权 参考的数据来源转换为数据库或其他数据源。
3.2.1 编写 Realm
- 继承
AuthorizingRealm
,这个接口当中的两个方法用于编写认证和验证的逻辑。
public class CustomerRealm extends AuthorizingRealm {
// 授权,回 null 会触发调用者抛出 授权异常
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证,返回 null 会触发调用者抛出 UnknownAccountException
// 在此方法当中就可以从数据库当中查询用户的信息了。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1. 在 token 当中获取用户名
String principal = (String) authenticationToken.getPrincipal();
if("zch".equals(principal)){
// 2. 放入根据用户名查询出来的正确密码,让调用者进行检验, 检验失败抛出 IncorrectCredentialsException
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("zch", "123", this.getName());
return simpleAuthenticationInfo;
} else {
// 返回 null 会触发调用者抛出 UnknownAccountException
return null;
}
}
}
3.2.2 使用自定义 Realm
public class TestCustomerRealm {
public static void main(String[] args) {
// 1. 创建 SecurityManager
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2. 给安全管理器设置Relm,设置自定义的Realm
securityManager.setRealm(new CustomerRealm());
// 3. SecurityUtils 全局的安全工具类,提供认证和退出功能, 其依然调用的是 securityManager
SecurityUtils.setSecurityManager(securityManager);
// 4. 获取当前用户的主体
Subject subject = SecurityUtils.getSubject();
// 5. 创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("zch", "1234");
// 6. 身份认证
try {
subject.login(token);
System.out.println("认证成功" + subject.isAuthenticated());
} catch (UnknownAccountException e) {
System.out.println("用户不存在");
} catch (IncorrectCredentialsException e) {
System.out.println("用户密码错误");
}
}
}
3.3 md5 + salt + hash
3.3.1 md5 算法
作用:一般用来加密 或者 签名(校验和,验证文件是否完整且正确)。
特点:不可逆,相同的内容不管 md5
加密多少次生成结果始终一致,且生成的结果始终是一个16
进制的32
位的字符串。
3.3.2 Salt
明文密码 + 随机盐 生成 最终的密文,避免明文过于简单,可以使用穷举网站直接破解了。
3.3.3 shiro 中的 md5 + salt 的实现
// 单独使用MD5
Md5Hash md5Hash = new Md5Hash("123");
System.out.println(md5Hash.toHex()); // 202cb962ac59075b964b07152d234b70
// 使用MD5 + Salt 处理 , Salt 使用随机字符生成
Md5Hash md5Hash1 = new Md5Hash("123", "X0*7ps");
System.out.println(md5Hash1.toHex()); // 8a83592a02263bfe6752b2b5b03a4799
// 使用 MD5 + salt + hash 散列(第三个参数为hash的次数)
Md5Hash md5Hash2 = new Md5Hash("123", "X0*7ps", 1024);
System.out.println(md5Hash2.toHex()); // e4f9bf3e0c58f045e62c23c533fcf633
3.3.4 Realm 中使用 md5
自定义Realm中替换CredentialsMatcher
public class CustomerMd5Realm extends AuthorizingRealm {
public CustomerMd5Realm() {
// 改写 CredentialsMatcher, 加密方式为 md5.
this.setCredentialsMatcher(new HashedCredentialsMatcher("md5"));
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1. 获取身份信息
String principal = (String) authenticationToken.getPrincipal();
// 2. 根据用户名查询数据库,获得加密后的密码
if("zch".equals(principal)) {
return new SimpleAuthenticationInfo("zch", "202cb962ac59075b964b07152d234b70", this.getName());
}
return null;
}
}
##### 3.3.5 Realm 中使用 md5 + salt `SimpleAuthenticationInfo` 多提供一个盐的参数即可。
public class CustomerMd5Realm extends AuthorizingRealm {
public CustomerMd5Realm() {
// 改写 CredentialsMatcher, 加密方式为 md5.
this.setCredentialsMatcher(new HashedCredentialsMatcher("md5"));
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1. 获取身份信息
String principal = (String) authenticationToken.getPrincipal();
// 2. 根据用户名查询数据库,获得加密后的密码
// 参数2:明文 + 随机盐 生成的密文
// 参数3:随即盐
if("zch".equals(principal)) {
return new SimpleAuthenticationInfo("zch",
"8a83592a02263bfe6752b2b5b03a4799",
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
3.3.6 Realm 中使用 md5 + salt + Hash
不指定散列,则默认使用的是一次。在CredentialsMatcher
中指定的 hash
的次数即可。
public class CustomerMd5Realm extends AuthorizingRealm {
public CustomerMd5Realm() {
// 改写 CredentialsMatcher, 加密方式为 md5,并指定 hash 的次数
HashedCredentialsMatcher credentials = new HashedCredentialsMatcher("md5");
credentials.setHashIterations(1024);
this.setCredentialsMatcher(credentials);
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1. 获取身份信息
String principal = (String) authenticationToken.getPrincipal();
// 3. 根据用户名查询数据库,获得加密后的密码
// 参数2:明文 + 随机盐 + hash 生成的密文
// 参数3:随即盐
if("zch".equals(principal)) {
return new SimpleAuthenticationInfo("zch",
"e4f9bf3e0c58f045e62c23c533fcf633",
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
6. Shiro 授权(简单Demo)
6.1 认证中的关键对象
-
Who
:即主体,需要访问系统中的资源。 -
what
:即资源(系统上一切都可以看作系统当中的资源),如系统菜单,页面,按钮,类方法,系统商品等信息等、资源包括资源类型(一类资源)和资源实例(一类当中的一个),比如商品信息为资源类型,类型为101
的商品为资源实例,编号为001
的商品信息信息也属于资源实例。 -
How
:即权限,规定了主体对资源的操作许可,权限离开资源没有意义,如用户权限、用户添加权限、某个方法的调用权限、编号为001
用户的修改权限等,通过权限可知主体对那些资源有那些操作许可。
6.3 授权流程
6.4 授权方式
-
RBAC(Role-Based Access Control)基于角色的访问控制
:基于角色为中心,给用户分配角色,而将权限分配给角色。
if(subject.hashRole("admin")) {
// ...TODO
}
-
RBAC(Resource-Based Access Control)基于资源的访问控制
:是以资源为中心进行访问控制,用户和权限直接建立关联。
//
if(subject.isPermistion("user:create:*")) {
// ...TODO
}
6.5 权限字符串
```java
资源类型:操作:资源实例
user:*:001 // 对 001 实例的用户资源具有所有操作。
user:update:* // 对 所有实例的用户资源具有更新权限。 或 user:update
6.6 Shiro 授权编程的实现方式
6.6.1 编程式
if(subject.hashRole("admin")) {
//...TOFO
}
6.6.2 注解式
@RequiresRoles("admin");
public void Hello() {
// ... TODO
}
6.6.3 标签式
<!-- JSP/GSP 页面通过相应的标签完成 -->
<shiro:hasRole name="admin">
<!-- ... 渲染 -->
</shiro>
6.7 编写授权程序
6.7.1 编写Realm
public class CustomerMd5Realm extends AuthorizingRealm {
public CustomerMd5Realm() {
// 改写 CredentialsMatcher, 加密方式为 md5,并指定 hash 的次数
HashedCredentialsMatcher credentials = new HashedCredentialsMatcher("md5");
credentials.setHashIterations(1024);
this.setCredentialsMatcher(credentials);
}
// 关键代码
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal(); // 用户名
// 根据身份信息获取当前 用户的角色信息,以及权限信息
// ...TODO
// 讲查询出来的权限信息赋值给 权限对象
// 添加角色
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
// 添加权限字符串
simpleAuthorizationInfo.addStringPermission("user:*:*");
return simpleAuthorizationInfo;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1. 获取身份信息
String principal = (String) authenticationToken.getPrincipal();
// 2. 根据用户名查询数据库,获得加密后的密码
// 参数2:明文 + 随机盐 + hash 生成的密文
// 参数3:随即盐
if("zch".equals(principal)) {
return new SimpleAuthenticationInfo("zch",
"e4f9bf3e0c58f045e62c23c533fcf633",
ByteSource.Util.bytes("X0*7ps"),
this.getName());
}
return null;
}
}
6.7.2 使用 Realm
public class TestCustomerMd5Realm {
public static void main(String[] args) {
// 1. 定义 SecurityMamager
DefaultSecurityManager securityManager = new DefaultSecurityManager();
// 2. 注入Realm
securityManager.setRealm(new CustomerMd5Realm());
// 2. 工具类设置 SecurityManager
SecurityUtils.setSecurityManager(securityManager);
// 4. 获取主体
Subject subject = SecurityUtils.getSubject();
// 5. 设置令牌
UsernamePasswordToken token = new UsernamePasswordToken("zch", "123");
// 5. 认证
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
System.out.println("密码错误");
}
// 6. 认证完毕, 进行授权
if(subject.isAuthenticated()) {
// 6.1 基于角色权限控制
// 是否具有某个角色
System.out.println(subject.hasRole("admin")); // true
// 是否具有指定的全部角色
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user"))); // true
// 是否具有其中一个角色
System.out.println(subject.hasRoles(Arrays.asList("admin", "user"))); // true
// 6.2 基于权限字符串的访问控制
// 是否具有某个的单个权限
System.out.println(subject.isPermitted("user:*:*")); // true
System.out.println(subject.isPermitted("user:update:001")); // true
// 是否具有全部的权限
System.out.println(subject.isPermittedAll("user:update:*", "user:delete:001")); // true
}
}
}