Shiro
入门概述
Apache Shiro | Simple. Java. Security.
是什么?
ApacheShiro是一个功能强大且易于使用的Java安全(权限)框架。Shiro可以完成:认证、授权、加密、会话管理、与Web集成、缓存等。借助Shiro您可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的Web和企业应用程序。
为什么要使用Shiro
自2003年以来,框架格局发生了相当大的变化,因此今天仍然有很多系统在使用Shiro。这与Shiro 的特性密不可分。
- 易于使用:使用Shiro构建系统安全框架非常简单。就算第一次接触也可以快速掌握。
- 全面: Shiro包含系统安全框架需要的功能,满足安全需求的“一站式服务”。
- 灵活: Shiro·可以在任何应用程序环境中工作。虽然它可以在·Web、EJB和IoC·环境中工作,但不需要依赖它们。Shiro也没有强制要求任何规范,甚至没有很多依赖项。
- 强力支持Web: Shiro具有出色的Web应用程序支持,可以基于应用程序URL和Web协议(例如REST)创建灵活的安全策略,同时还提供一组JSP库来控制页面输出。
- 兼容性强: Shiro 的设计模式使其易于与其他框架和应用程序集成。Shiro与Spring、Grails、Wicket、Tapestry、Mule、Apache·Camel、Vaadin等框架无缝集成。
- 社区支持:Shiro 是 Apache 软件基金会的一个开源项目,有完备的社区支持,文档 支持。如果需要,像 Katasoft 这样的商业公司也会提供专业的支持和服务。
权限的管理
权限管理
- 基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者
安全策略
控制用户可以访问而且只能访问自己被授权的资源。 - 权限管理包括用户
身份认证
和授权
两部分,简称认证授权
。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
什么是身份认证
身份认证
,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的 用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
什么是授权
授权,即访问控制
,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的
shiro的核心架构
Subject
Subject即主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序。
Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
SecurityManager
SecurityManager即安全管理器,对全部的subject进行安全管理,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。
SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口。
Authenticator
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
Authorizer
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Realm
Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
SessionManager
sessionManager即会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO
SessionDAO即会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
CacheManager
CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
Cryptography
Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
Shiro中的认证
认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。
shiro中认证的关键对象
Subject:主体
访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Principal:身份信息
是主体(subject)进行身份认证的标识,标识必须具有唯一性
,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
credential:凭证信息
是只有主体自己知道的安全信息,如密码、证书等
认证流程
demo
引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
shiro 配置文件
.ini 结尾的文件
用来学习shiro时书写系统中的相关数据权限
demo
- 创建安全管理器对象
- 给安全管理器设置realm
- SecurityUtils给全局安全工具类设置安全管理器
- 关键对象 subject 主体
- 创建令牌
package com.bo;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.subject.Subject;
/**
* @author: bo
* @date: 2022/10/14
* @description:
*/
public class TestAuthenticator {
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 subject = SecurityUtils.getSubject();
//5.创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang","123");
try {
System.out.println("认证状态"+subject.isAuthenticated());
subject.login(token);
System.out.println("认证状态"+subject.isAuthenticated());
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("认证失败:用户名不存在");
}catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("认证失败,密码错误");
}
}
}
常见的异常类型
- DisabledAccountException(帐号被禁用)
- LockedAccountException(帐号被锁定)
- ExcessiveAttemptsException(登录失败次数过多)
- ExpiredCredentialsException(凭证过期)等
看看认证的源码
打个断点
做认证的还是securityManager
再进入login
再进入authenticate方法
再进入doAuthenticate
先做一个断言,然后用getRealms拿到域
调用了realm.getAuthenticationInfo
进入
先去缓存中拿了一下认证信息,没有缓存
进入这个方法
拿到用户名,看看加没加锁,密码过期了吗
最终执行用户名比较是在这里
最终密码校验在这里
总结:
AuthenticatingRealm 认证realm doGetAuthenticationInfo
AuthorizingRealm 授权realm doGetAuthorizationInfo
我们要自定义认证用户名就需要继承AuthorizingRealm类
AuthorizingRealm类2个方法都有,因为继承了AuthenticatingRealm类
密码是shiro自动验证的
为什么密码是shiro自己处理的呢
因为密码可能会做加密,自己处理可能影响到shiro的加密的使用,所以密码的校验由shiro完成
自定义Realm的作用:放弃使用.ini文件,使用数据库查询,去继承AuthorizingRealm
上边的程序使用的是Shiro自带的IniRealm,IniRealm从ini配置文件中读取用户的信息,大部分情况下需要从系统的数据库中读取用户信息,所以需要自定义realm。
SimpleAccountRealm的部分源码中有两个方法一个是 认证 一个是 授权
部分源码
public class SimpleAccountRealm extends AuthorizingRealm {
//.......省略
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = getUsername(principals);
USERS_LOCK.readLock().lock();
try {
return this.users.get(username);
} finally {
USERS_LOCK.readLock().unlock();
}
}
}
自定义realm
package com.bo.realm;
import jdk.nashorn.internal.parser.Token;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author: bo
* @date: 2022/10/14
* @description: 自定义realm实现,将认证、授权数据的来源转化为数据库的实现
*/
public class CustomRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//在token中获取用户名
String principal = (String) token.getPrincipal();
System.out.println(principal);
//根据身份信息使用jdbc mybatis查询数据库
if("xiaozhang".equals(principal)){
//数据库查到的参数1:用户名 参数2:密码 参数3
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("xiaozhang", "123",this.getName());
return simpleAuthenticationInfo;
}
return null;
}
}
测试类
package com.bo;
import com.bo.realm.CustomRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
/**
* @author: bo
* @date: 2022/10/14
* @description: 使用自定义realm
*/
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
//创建securityManager
DefaultSecurityManager manager = new DefaultSecurityManager();
//设置自定义realm
manager.setRealm(new CustomRealm());
//将安全工具类设置安全工具类
SecurityUtils.setSecurityManager(manager);
//通过安全工具类获取subject
Subject subject = SecurityUtils.getSubject();
//创建token
UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123456");
try {
subject.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}
MD5+Salt+Hash
注册的时候加盐,把盐和加密后的密码都存数据库
登录的时候,把用户传过来的密码从数据库取出来,加盐处理和数据库加密后的密码比较,一样就登录成功
使用md5
Md5Hash md5Hash =new Md5Hash(“123”);
System.out.println(md5Hash.toHex());
md5+盐
Md5Hash md5Hash1 = new Md5Hash(“123”, “dasdas”);
System.out.println(md5Hash1.toHex());
md5+盐+hash散列第三个参数是次数
Md5Hash md5Hash2 = new Md5Hash(“123”, “dasdas”,1024);
System.out.println(md5Hash2.toHex());
改造Shiro
md5加密
使用时候告诉要用md5加密
package com.bo;
import com.bo.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
/**
* @author: bo
* @date: 2022/10/14
* @description:
*/
public class TestCustomerMD5RealmAuthenicator {
public static void main(String[] args) {
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomerMD5Realm realm = new CustomerMD5Realm();
//设置real使用hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123");
try {
subject.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
}
}
package com.bo.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author: bo
* @date: 2022/10/14
* @description: 自定义realm,加入md5+salt+hash
*/
public class CustomerMD5Realm extends AuthorizingRealm {
public static void main(String[] args) {
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取身份信息
String principal = (String) authenticationToken.getPrincipal();
if ("xiaozhang".equals(principal)) {
return new SimpleAuthenticationInfo("principal", "202cb962ac59075b964b07152d234b70", this.getName());
}
return null;
}
}
md5+盐
md5+盐+散列
授权
授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。
关键对象
授权可简单理解为who对what(which)进行How操作:
Who,即主体(Subject),主体需要访问系统中的资源。
What,即资源(Resource),如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
How,权限/许可(Permission),规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
授权方式
基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制
if(subject.hasRole("admin")){
//操作什么资源
}
基于资源的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制
if(subject.isPermission("user:update:01")){ //资源实例
//对资源01用户具有修改的权限
}
if(subject.isPermission("user:update:*")){ //资源类型
//对 所有的资源 用户具有更新的权限
}
权限字符串
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作,“:”是资源/操作/实例的分割符,权限字符串也可以使用*
通配符。
例子:
用户创建权限:user:create,或user:create:*
用户修改实例001的权限:user:update:001
用户实例001的所有权限:user:*:001
shiro授权实现方式
编程式
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole(“admin”)) {
//有权限
} else {
//无权限
}
注释式
@RequiresRoles("admin")
public void hello() {
//有权限
}
标签式
JSP/GSP 标签:在JSP/GSP 页面通过相应的标签完成:
<shiro:hasRole name="admin">
<!— 有权限—>
</shiro:hasRole>
注意: Thymeleaf 中使用shiro需要额外集成!
在自定义realm中的doGetAuthorizationInfo方法进行授权
测试类
单角色控制
package com.bo;
import com.bo.realm.CustomerMD5Realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
/**
* @author: bo
* @date: 2022/10/14
* @description:
*/
public class TestCustomerMD5RealmAuthenicator {
public static void main(String[] args) {
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
CustomerMD5Realm realm = new CustomerMD5Realm();
//设置real使用hash凭证匹配器
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
defaultSecurityManager.setRealm(realm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("xiaozhang", "123");
try {
subject.login(token);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
//认证用户进行授权
if (subject.isAuthenticated()) {
//1.基于角色权限控制
System.out.println(subject.hasRole("admin"));
}
}
}
package com.bo.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
/**
* @author: bo
* @date: 2022/10/14
* @description: 自定义realm,加入md5+salt+hash
*/
public class CustomerMD5Realm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
System.out.println("身份信息" + primaryPrincipal);
//根据身份信息 用户名 获取当前用户的角色信息,以及权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//将数据库查询的角色信息赋值给权限对象
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取身份信息
String principal = (String) authenticationToken.getPrincipal();
//参数1数据库的用户名,参数2,md5+salt加密的密码,参数3加的盐,参数4realm的名字
if ("xiaozhang".equals(principal)) {
return new SimpleAuthenticationInfo("xiaochen",
"3e89a6098a07842cf0ee50cb975b8829", ByteSource.Util.bytes("dasdas"), this.getName());
}
return null;
}
}
角色权限是user或者admin都可以通过
多角色权限控制
//基于多角色权限控制
System.out.println(subject.hasAllRoles(Arrays.asList("admin", "user")));
是否具有其中一个角色
//是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("admin", "user", "super"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
基于权限字符串的访问控制, 资源标识符:操作:资源类型
Q.E.D.