1. 什么是Shiro
Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序―从最小的移动应用程序到最大的web和企业应用程序。
Shiro官网:http://shiro.apache.org/architecture.html
Shiro在线中文文档学习:https://www.w3cschool.cn/shiro/co4m1if2.html
2. Shiro的核心架构
2.1 Subject
- Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
2.2 SecurityManager
- SecurityManager:安全管理器,相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
2.3 Authenticator
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
2.4 Realm
- Realm:域,可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意:Shiro 不知道你的用户 / 权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm;
2.5 SessionManager
- SessionManager:会话管理,如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所以呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器);
2.6 SessionDAO
- SessionDAO:DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
2.7 CacheManager
- CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能;
2.8 Cryptography
- Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的。
3. Shiro中的认证(身份验证)
3.1 认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
在 shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份。
3.2 shiro中认证的关键对象
-
Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体; -
Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。 -
credential:凭证信息
是只有主体自己知道的安全信息,如密码、证书等。
3.3 认证流程
3.3 认证的开发
3.3.1 创建项目并引入依赖
创建项目
引入依赖
pom.xml
在这里插入代码片
3.3.2 引入shiro配置文件
配置文件:名称随意,以 .ini 结尾,放在 resources 目录下
注意:在实际的项目开发中并不会使用这种方式,这种方法可以用来初学时练手
[users]
zhangsan=123456
lisi=456789
3.3.3 认证代码
public class TestHelloWorld {
public static void main(String[] args) {
//1.创建安全管理器
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//2.通过Realm获取数据
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
//3.SecurityUtils 给安全工具类设置安全管理器
SecurityUtils.setSecurityManager(securityManager);
//4.获取关键对象
Subject subject = SecurityUtils.getSubject();
//5.创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("zhangsans","123456");
try {
//6.用户认证
System.out.println("认证前:"+(subject.isAuthenticated() ? "认证成功":"认证失败"));
subject.login(token);
System.out.println("认证后:"+(subject.isAuthenticated() ? "认证成功":"认证失败"));
}catch (Exception e) {
e.printStackTrace();
}
}
}
流程如下:
- 首先调用
Subject.login(token)
进行登录,其会自动委托给Security Manager
,调用之前必须通过SecurityUtils.setSecurityManager()
设置; SecurityManager
负责真正的身份验证逻辑;它会委托给Authenticator
进行身份验证;Authenticator
才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;Authenticator
可能会委托给相应的AuthenticationStrategy
进行多Realm
身份验证,默认ModularRealmAuthenticator
会调用AuthenticationStrategy
进行多Realm
身份验证;Authenticator
会把相应的token
传入Realm
,从Realm
获取身份验证信息,如果没有返回 / 抛出异常表示身份验证成功了。此处可以配置多个Realm
,将按照相应的顺序及策略进行访问。
3.4 常见的异常类型
DisabledAccountException(帐号被禁用)
LockedAccountException(帐号被锁定)
ExcessiveAttemptsException(登录失败次数过多)
ExpiredCredentialsException(凭证过期)
UnknownAccountException(用户账户不存在)
IncorrectCredentialsException(密码错误)
4. 自定义Realm
通过分析源码可得:
认证:
1.最终执行用户名比较是 在SimpleAccountRealm类 的 doGetAuthenticationInfo 方法中完成用户名校验
2.最终密码校验是在 AuthenticatingRealm类 的 assertCredentialsMatch方法 中
总结:
AuthenticatingRealm 认证realm doGetAuthenticationInf
AuthorizingRealm 授权realm doGetAuthorizationInfo
自定义Realm的作用:放弃使用.ini文件,使用数据库查询
4.1 shiro提供的Realm
4.2 根据认证源码认证使用的是SimpleAccountRealm
SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权,
4.3 自定义realm
/**
* 自定义realm
*/
public class CustomerRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 从token中获取用户名
String principal = (String)authenticationToken.getPrincipal();
System.out.println(principal);
//根据jdbc mybatis 查询相关数据库
if ("zhangsan".equals(principal)) {
//参数1:返回数据库中正确的用户名
//参数2:返回数据库中正确密码
// 参数3:提供当前realm的名字this.getName()
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal,"123456",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
自定义Realm测试
public class TestCustomerRealm {
public static void main(String[] args) {
//1.创建安全管理对象 securityManager
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//2.给安全管理器设置realm(设置为自定义realm获取认证数据)
defaultSecurityManager.setRealm(new CustomerRealm());
//IniRealm realm = new IniRealm("classpath:shiro.ini");
//3.给安装工具类中设置默认安全管理器
SecurityUtils.setSecurityManager(defaultSecurityManager);
//4.获取主体对象subject
Subject subject = SecurityUtils.getSubject();
//5.创建token令牌
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
subject.login(token);
}catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户账户有误!!!");
}catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!!!");
}
}
}
4.4 MD5+Salt+Hash
MD5算法
作用:一般用来加密或者签名(校验和)
特点:MD5算法不可逆如何内容相同无论执行多少次md5生成结果始终是一致
网络上提供的MD5在线解密一般是用穷举的方法
生成结果:始终是一个16进制32位长度字符串
MD5使用:
public class TestShiroMD5 {
public static void main(String[] args) {
//MD5
Md5Hash MD5 = new Md5Hash("123");
System.out.println(MD5.toHex());
//MD5+SAlt
Md5Hash MD52 = new Md5Hash("123","XO*7PS");
System.out.println(MD52.toHex());
//MD5+Salt+Hash(参数代表要散列多少次,一般是 1024或2048)
Md5Hash MD53 = new Md5Hash("123","XO*7PS",1024);
System.out.println(MD53.toHex());
}
}
输出结果:
202cb962ac59075b964b07152d234b70
cede6e1e0bef3ec72f73965b21396139
4e6dc09a5f190a8613d351906a76ffe2
实际应用:将 盐和散列 后的值存在数据库中,自定义realm从数据库取出盐和加密后的值由shiro完成密码校验。
1.自定义md5+salt的realm
public class CustomerMD5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String principal = (String) authenticationToken.getPrincipal();
if ("zhangsan".equals(principal)) {
//参数1:数据库用户名
//参数2:数据库md5+salt之后的密码
//参数3:注册时的随机盐
//参数4:realm的名字
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(principal, "bf974c3cd65aa214d1c0601b3cf87ce7", ByteSource.Util.bytes("XO*7PS"),this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
2.使用md5+salt 认证
public class TestCustomerMD5Realm {
public static void main(String[] args) {
//创建安全管理器
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//注入自定义Realm
CustomerMD5Realm realm = new CustomerMD5Realm();
//创建Hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//指定使用md5算法
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
//将安全管理器注入安全工具类中
defaultSecurityManager.setRealm(realm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取用户信息
Subject subject = SecurityUtils.getSubject();
//创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan","123456");
//用户认证
try {
subject.login(token);
System.out.println("登录成功!!!");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误!!!");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误!!!");
}
}
}
5.Shiro中的授权
5.1 授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
5.2 关键对象
授权可简单理解为who对what(which)进行How操作:
Who,即主体(Subject),主体需要访问系统中的资源。
What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型
和资源实例
,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
5.3 授权流程
5.4 授权方式
5.4.1 基于角色的访问控制
- RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
if(subject.hasRole("admin")){
//操作什么资源
}
5.4.2 基于资源的访问控制
if(subject.isPermission("user:update:01")){ //资源实例
//对资源01用户具有修改的权限
}
if(subject.isPermission("user:update:*")){ //资源类型
//对 所有的资源 用户具有更新的权限
}
- RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
5.5 权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*通配符。
例子:
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001
5.6 shiro中授权编程实现方式
5.6.1 编程式
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
5.6.2 注解式
@RequiresRoles("admin")
public void hello() {
//有权限
}
5.6.3 标签式
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!
5.7 开发授权
1.realm的实现
/**
* 使用自定义realm 加入md5 + salt +hash
* 实现授权操作
*/
public class CustomerMd5Realm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String primaryPrincipal = (String)principals.getPrimaryPrincipal();
System.out.println("身份信息: "+primaryPrincipal); //用户名
//根据身份信息 用户名 获取当前用户的角色信息,以及权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//假设 admin,user 是从数据库查到的 角色信息
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
//假设 ... 是从数据库查到的 权限信息赋值给权限对象
simpleAuthorizationInfo.addStringPermission("user:*:01");
simpleAuthorizationInfo.addStringPermission("prodect:*");//第三个参数为*省略
return simpleAuthorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取 token中的 用户名
String principal = (String) token.getPrincipal();
//假设这是从数据库查询到的信息
String username="zhangsan";
String password="7268f6d32ec8d6f4c305ae92395b00e8";//加密后
//根据用户名查询数据库
if (username.equals(principal)) {
//参数1:数据库用户名
//参数2:数据库md5+salt之后的密码
//参数3:注册时的随机盐
//参数4:realm的名字
return new SimpleAuthenticationInfo(principal,
password,
ByteSource.Util.bytes("@#$*&QU7O0!"),
this.getName());
}
return null;
}
}
2.授权
public class TestCustomerMd5RealmAuthenicator {
public static void main(String[] args) {
//1.创建安全管理器
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//2.注入realm
CustomerMd5Realm realm = new CustomerMd5Realm();
//3.设置realm使用hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//声明:使用的算法
credentialsMatcher.setHashAlgorithmName("md5");
//声明:散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
//4.将安全管理器注入安全工具
SecurityUtils.setSecurityManager(defaultSecurityManager);
//5.通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
//6.认证
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123");
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}
//授权
if (subject.isAuthenticated()){
//基于角色权限控制
System.out.println(subject.hasRole("admin"));
//基于多角色的权限控制
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));//true
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "manager")));//false
//是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "manager"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
System.out.println("====这是一个分隔符====");
//基于权限字符串的访问控制 资源标识符:操作:资源类型
//用户具有的权限 user:*:01 prodect:*
System.out.println("权限:"+subject.isPermitted("user:update:01"));
System.out.println("权限:"+subject.isPermitted("prodect:update:02"));
//分别具有哪些权限
boolean[] permitted = subject.isPermitted("user:*:01", "user:update:02");
for (boolean b : permitted) {
System.out.println(b);
}
//同时具有哪些权限
boolean permittedAll = subject.isPermittedAll("prodect:*:01", "prodect:update:03");
System.out.println(permittedAll);
}
}
}
内容参考:
B站编程不良人:https://www.bilibili.com/video/BV1uz4y197Zm
仅用于学习!