Shiro
1.权限的管理
1.1权限管理
涉及用户参与,实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括身份认证和授权两部分。
1.2 身份认证
用户登录
1.3 授权
即访问控制,分配权限。
2. Shiro
Shiro是一个功能强大且易于使用的Java安全框架,它执行身份验证、授权、加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序―从最小的移动应用程序到最大的web和企业应用程序.
3. Shiro的核心框架
3.1 Subject
主体,外部应用与subject进行交互,subject记录了当前操作用户,将用户的概念理解为当前操作的主体,可以是一个通过浏览器请求的用户,也可能是一个运行的程序。subject在shiro中是一个接口,接口中定义了很多认证相关的方法,外部程序通过subject进行认证授权,而subject是通过securityManager安全管理器进行认证授权
3.2 SecurityManager
SecurityManager即安全管理器,对全部的subject进行安全管理,是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证。授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorize进行授权,通过SessionManager进行会话管理等。
securityManager是一个接口,继承了Authenticator,Authorize,SessionManager这三个接口。
3.3 Authenticator
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
3.4 Authorizer
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限
3.5 Realm
Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库,那么Realm就需要从数据库获取用户身份信息。
3.6 SessionManager
sessionManager即会话管理,shiro框架定义了一套会话管理,不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使他实现单点登录。
3.7 SessionDAO
sessionDAO即会话DAO,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
3.8 CacheManager
缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
3.9 Cryptography
密码管理,shiro提供了一套加密/解密的组件,方便开发
4. Shiro的认证
4.1 认证
身份认证,就是判断一个用户是否为合法用户的处理过程,最常用的简单身份认证是通过系统核对用户输入的用户名和口令,看其是否与系统中存储的用户的用户名和口令一致,来判断用户身份是否正确。
4.2 关键对象
- subject 主体
- 访问系统的用户,主体可以是用户、程序等,进行认证的都可以称为主体;
- Principal:身份信息
- 是主体进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但必须有一个主身份。
- credential:凭证信息
- 是只有主体自己知道的安全信息,如密码、证书等。
4.3 认证流程
4.4 shiro的认证代码实现
4.4.1 添加依赖
<!-- 引入Shiro的依赖 -->
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
4.4.2 shiro.ini配置文件
在resource下新增shiro.ini配置文件
[users]
hjx=123
zhangsan=123
lisi=456
4.4.3 编码实现
1.前期配置
// 创建安全管理器对象,
/**
* 选中 SecurityManager,ctrl+H 可查看其层次结构
*
* SecurityManager 是一个接口,其实现类为 DefaultSecurityManager
*
* 父类引用指向子类实例,多态理解
*/
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 给安全管理器设置 Realm,realm 的作用是用于数据库的交互
defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 全局安全管理工具类 SecurityUtils , 设置SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 获取 关键对象 主体 subject
Subject subject = SecurityUtils.getSubject();
2.登录认证
// 创建 认证令牌token
/**
* shiro 的认证是通过查验令牌实现的
*
* 令牌的目的是减轻频繁查询数据库的压力
*
* new UsernamePasswordToken("hjx","123");模拟前端传回的用户输入的用户名和密码
*/
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("hjx","123");
try {
System.out.println("认证状态 : "+subject.isAuthenticated());
subject.login(usernamePasswordToken);
System.out.println("认证状态 : "+subject.isAuthenticated());
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}catch (Exception e){
e.printStackTrace();
}
3.运行结果
4.源码分析
-
Debug进入subject.login()函数
-
调用的是DelegatingSubject类里的login函数,
public class DelegatingSubject implements Subject { ... ... public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); ... ... }
-
进入securityManager.login(this, token)函数
public class DefaultSecurityManager extends SessionsSecurityManager { ... ... public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { // 认证函数 info = authenticate(token); } catch (AuthenticationException ae) { try { onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; //propagate } Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn; } }
-
进入authenticate(token)函数
public abstract class AuthenticatingSecurityManager extends RealmSecurityManager { ... ... /** * Delegates to the wrapped {@link org.apache.shiro.authc.Authenticator Authenticator} for authentication. */ public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); } ... ... }
-
进入this.authenticator.authenticate(token)
public abstract class AbstractAuthenticator implements Authenticator, LogoutAware { ... ... public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { if (token == null) { throw new IllegalArgumentException("Method argument (authentication token) cannot be null."); } log.trace("Authentication attempt received for token [{}]", token); AuthenticationInfo info; try { // 认证函数 info = doAuthenticate(token); if (info == null) { String msg = "No account information found for authentication token [" + token + "] by this " + "Authenticator instance. Please check that it is configured correctly."; throw new AuthenticationException(msg); } } catch (Throwable t) { ... ... }
-
进入doAuthenticate(token)函数
public class ModularRealmAuthenticator extends AbstractAuthenticator { ... ... protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { // 判断realm是否配置了 assertRealmsConfigured(); // 获取所有的realm的集合 Collection<Realm> realms = getRealms(); if (realms.size() == 1) { // 由于这里只配置了一个realm,所以进入这一个分支 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } } ... ... protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { // 判断realm是否支持token if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } return info; } ... ... }
-
进入realm.getAuthenticationInfo(token)函数
public abstract class AuthenticatingRealm extends CachingRealm implements Initializable { ... ... public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 首先尝试从缓存中查询数据 AuthenticationInfo info = getCachedAuthenticationInfo(token); // 若缓存中没有对应的数据 if (info == null) { //otherwise not cached, perform the lookup: // 进入这里 info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { // 添加进缓存 cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { // 验证密码是否正确 assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; } ... ... }
-
进入doGetAuthenticationInfo(token);
public class SimpleAccountRealm extends AuthorizingRealm { ... ... protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 获取token UsernamePasswordToken upToken = (UsernamePasswordToken) token; // 根据 token 里的身份信息,查询配置文件 // 将返回的数据封装成 Account的实现类 SimpleAccount // SimpleAccount类里包含 用户的 身份信息和凭证信息,即包含用户名和密码 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 SimpleAccount getUser(String username) { USERS_LOCK.readLock().lock(); try { // users是一个Map集合 this.users = new LinkedHashMap<String, SimpleAccount>(); // 里面以 <用户名,用户身份信息类> 的形式存储了所有的用户信息 return this.users.get(username); } finally { USERS_LOCK.readLock().unlock(); } } ... ... }
-
总结
最终结果调用的是 SimpleAccountRealm 中的 doGetAuthenticationInfo 函数,而密码的校验是在 AuthenticatingRealm类中完成的。
-
从realm继承的结构图中看出,SimpleAccountRealm 继承了 AuthorizingRealm类,AuthorizingRealm类是用来进行授权操作的,
-
AuthorizingRealm类继承了 AuthenticatingRealm类, AuthenticatingRealm类是用来进行认证的,
-
在 SimpleAccountRalm类中
- doGetAuthenticationInfo函数 用来进行用户身份认证,继承自 AuthenticatingRealm,是AuthenticatingRealm抽象类doGetAuthenticationInfo函数的实现
- 还有一个 doGetAuthorizationInfo函数用来进行用户授权的, 继承自AuthorzingRealm, 是AuthorzingRealm抽象类doGetAuthorizationInfo函数的实现
!!!所以,当我们需要使用shiro查询数据库数据的时候,就要自定义Realm,继承AuthorizingRealm类
!!!然后重写它的 doGetAuthenticationInfo 实现认证,
!!!重写doGetAuthorizationInfo 实现授权操作。
4.5 自定义Realm
4.5.1 创建CustomerRealm类
继承AuthorizingRealm, 实现doGetAuthorizationInfo方法和doGetAuthenticationInfo方法
/**
* @Author: Hjx
* @Date: 2021/8/9 15:43
* 自定义的 Realm , 将认证/授权的数据来源转为数据库
*/
public class CustomerRealm extends AuthorizingRealm {
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从 token 中获取用户输入的身份凭证
String userName = (String) token.getPrincipal();
// 模拟从数据库查询出的用户名和密码
String name = "hjx";
String pass = "123";
if (name.equals(userName)){
/*
SimpleAuthenticationInfo 是 AuthenticationInfo 的实现类
参数1:用户名
参数2:密码
参数3:自定义realm的名称
*/
return new SimpleAuthenticationInfo(name, pass, this.getName());
}
return null;
}
}
4.5.2 测试类
/**
* @Author: Hjx
* @Date: 2021/8/9 15:47
*/
public class TestCustomerRealmAuthenticator {
public static void main(String[] args) {
// 创建SecurityManager安全管理器
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 给SecurityManager设置自定义Realm
defaultSecurityManager.setRealm(new CustomerRealm());
// 给安全工具类SecurityUtils 设置 SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 通过安全工具类SecurityUtils获取 主体subject
Subject subject = SecurityUtils.getSubject();
// 创建Token,模拟接收前端用户输入的用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken("hjx", "123");
try {
subject.login(token);
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("用户名错误");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("密码错误");
}catch (Exception e){
e.printStackTrace();
}
}
}
代码及资料地址:hjx: 知道的越多,越发觉自己的无知 (gitee.com)
视频学习参考:【编程不良人】2020最新版Shiro教程,整合SpringBoot项目实战教程_哔哩哔哩_bilibili