本篇为《Shiro从入门到精通》系列第二篇,在上篇《还在手写filter进行权限校验?尝试一下Shiro吧》中,我们学习了Shiro的基本功能、架构以及各个组件的概念。本篇文章继续深入,以官方示例为基础,讲解使用Shiro的流程以及认证和授权的原理分析。下面开始正文:
前言
Shiro作为常用的权限框架,可被用于解决认证、授权、加密、会话管理等场景。Shiro对其API进行了友好的封装,如果单纯的使用Shiro框架非常简单。但如果使用了多年Shiro,还依旧停留在基本的使用上,那么这篇文章就值得你学习一下。只有了解Shiro的底层和实现,才能够更好的使用和借鉴,同时也能够避免不必要的坑。
下面以官方提供的实例为基础,讲解分析Shiro的基本使用流程,同时针对认证和授权流程进行更底层的原理讲解,让大家真正了解我们所使用的Shiro框架,底层是怎么运作的。
Shiro组成及框架
在学习Shiro各个功能模块之前,需要先从整体上了解Shiro的整体架构,以及核心组件所处的位置。下面为官方提供的架构图:
上图可以看出Security Manager是Shiro的核心,无论认证、授权、会话管理等都是通过它来进行管理的。在使用和分析原理之前,先来了解后面会用到的组件及其功能:
- Subject:主体,可以是用户或程序,主体可以访问Security Manager以获得认证、授权、会话等服务;
- Security Manager:安全管理器,主体所需的认证、授权功能都是在这里进行的,是Shiro的核心;
- Authenticator:认证器,主体的认证过程通过Authenticator进行;
- Authorizer:授权器,主体的授权过程通过Authorizer进行;
- Session Manager:shiro的会话管理器,与web应用提供的Session管理分隔开;
- Realm:域,可以有一个或多个域,可通过Realm存储授权和认证的逻辑;
上面只列出了部分组件及功能,其他更多组件在后续文章会逐步为大家实践讲解。了解了这些组件和核心功能之后,下面以官方的示例进行讲解。
官方实例分析
Shiro官方示例地址为:http://shiro.apache.org/tutorial.html ,需要留意的是官方示例已经有些老了,在实践中会做一些调整。
我们先在本地将环境搭建起来,运行程序。创建一个基于Maven的Java项目,引入如下依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.29</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.29</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
目前最新版本为1.7.0,可根据需要引入其他版本。运行官方实例比较坑的地方是,还需要引入log4j和slf4j对应的依赖。都是Apache的项目,因此底层默认采用了log4j的日志框架,如果不引入对应的日志依赖,会报错或无法打印日志。
紧接着,在resources目录下创建一个shiro.ini文件,将官网提供内容配置内容复制进去:
# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel *s' film "Spaceballs" :)
# =============================================================================
# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
这个文件可看成是一个Realm,其实就是shiro默认的IniRealm,当然在不同的项目中用户、权限、角色等信息可以以各种形式存储,比如数据库存储、缓存存储等。
上述配置文件格式的语义也比较明确,配置了用户和角色等信息,大家留意看一下注释中对数据格式的解释。root = secret, admin表示用户名root,密码是secret,角色是admin。其中角色可以配置多个,在后面依次用逗号分隔即可。schwartz = lightsaber:*表示角色schwartz拥有权限lightsaber:*。
继续创建一个Tutorial类,将官网提供的代码复制进去,由于采用的是1.7.0版本,官网实例中下面的代码已经没办法正常运行了:
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
原因是IniSecurityManagerFactory类已经被标注废弃了,替代它的是Environment接口及其实现类。因此需将上述获取SecurityManager的方式改为通过shiro提供的Environment来初始化和获取:
Environment environment = new BasicIniEnvironment("classpath:shiro.ini");
SecurityManager securityManager = environment.getSecurityManager();
SecurityUtils.setSecurityManager(securityManager);
改造之后的完整代码如下(其中英文注释已翻译成中文注释):
public class Tutorial {
private static final transient Logger log = LoggerFactory.getLogger(Tutorial.class);
public static void main(String[] args) {
log.info("My First Apache Shiro Application");
// 1.初始化环境,主要是加载shiro.ini配置文件的信息
Environment environment = new BasicIniEnvironment("classpath:shiro.ini");
// 2.获取SecurityManager安全管理器
SecurityManager securityManager = environment.getSecurityManager();
SecurityUtils.setSecurityManager(securityManager);
// 3.获取当前主体(用户)
Subject currentUser = SecurityUtils.getSubject();
// 4.获取当前主体的会话
Session session = currentUser.getSession();
// 5.向会话中存储一些内容(不需要web容器或EJB容器)
session.setAttribute("someKey", "aValue");
// 6.再次从会话中获取存储的内容,并比较与存储值是否一致。
String value = (String) session.getAttribute("someKey");
if ("aValue".equals(value)) {
log.info("Retrieved the correct value! [" + value + "]");
}
// 当前用户进行登录操作,进而可以检验用户的角色和权限。
// 7.判断当前用户是否认证(此时很显然未认证)
if (!currentUser.isAuthenticated()) {
// 8.将账号和密码封装到UsernamePasswordToken中
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
// 9.记住我
token.setRememberMe(true);
try {
// 10.进行登录操作
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... 更多其他异常,包括应用程序异常
catch (AuthenticationException ae) {
// 其他意外异常、error处理
}
}
// 打印当前用户的主体信息
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
// 10.检查是否有指定角色权限(前面已经通过Environment加载了权限和角色信息)
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
// 判断是否有资源操作权限
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
// 更强级别的权限验证
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
// 11.登出
currentUser.logout();
System.exit(0);
}
}
完整项目源码:https://github.com/secbr/shiro
执行程序,打印日志信息如下,可以看到每一步的执行输出:
INFO - My First Apache Shiro Application
INFO - Enabling session validation scheduler...
INFO - Retrieved the correct value! [aValue]
INFO - User [lonestarr] logged in successfully.
INFO - May the Schwartz be with you!
INFO - You may use a lightsaber ring. Use it wisely.
INFO - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun!
上述代码中包含了11个主要的流程:
- 1、初始化环境,这里主要是加载shiro.ini配置文件的信息;
- 2、获取SecurityManager安全管理器;
- 3、获取当前主体(用户);
- 4、获取当前主体的会话;
- 5、向会话中存储一些内容(不需要web容器或EJB容器);
- 6、再次从会话中获取存储的内容,并比较与存储值是否一致;
- 7、判断当前用户是否认证;
- 8、将账号和密码封装到UsernamePasswordToken中;
- 9、开启记住我;
- 10、检查是否有指定角色权限;
- 11、退出登录。
下面我们对几个核心步骤步骤进行分析说明。
初始化环境
源码中通过Environment对象来加载配置文件和初始化SecurityManager,然后通过工具类SecurityUtils对SecurityManager进行设置。在实践中,可根据具体情况进行初始化,比如实例中通过Environment加载文件,也可以直接创建DefaultSecurityManager,在web项目采用DefaultWebSecurityManager等。
// 1.初始化环境,主要是加载shiro.ini配置文件的信息
Environment environment = new BasicIniEnvironment("classpath:shiro.ini");
// 2.获取SecurityManager安全管理器
SecurityManager securityManager = environment.getSecurityManager();
SecurityUtils.setSecurityManager(securityManager);
这里的配置文件相当于一个Realm,部分SecurityManager实现类(比如:DefaultSecurityManager)提供了setRealm方法,用户可通过该方法自定义设置Realm。
总之,无论获取SecurityManager的方式如何,都需要有这么一个SecurityManager用来处理后续的认证、授权等处理,可见SecurityManager的核心地位。
认证流程
在上述实例代码中,先将认证功能相关的核心代码抽离出来,包含以下代码及操作步骤(省略了SecurityManager的创建和设置):
// 获取当前主体(用户)
Subject currentUser = SecurityUtils.getSubject();
// 判断当前用户是否认证
currentUser.isAuthenticated()
// 将账号和密码封装到UsernamePasswordToken中
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
// 记住我
token.setRememberMe(true);
// 登录操作
currentUser.login(token);
从这个代码流程上来看,Shiro的认证过程包括:初始化环境,获取当前用户主体,判断是否认证过,将账号密码进行封装,进行认证,认证完成校验权限。可以通过下图来表示整个流程。
接下来,通过跟踪源码,来看看Shiro的认证流程涉及到哪些组件。
认证原理分析
认证的入口程序是login方法,以此方法为入口,进行跟踪,并忽略掉非核心操作,可得出认证逻辑经过以下代码执行步骤:
//currentUser类型为Subject,在构造了SecurityManager之后,提交认证,token封装了用户信息
currentUser.login(token);
//DelegatingSubject类中,调用SecurityManager执行认证
Subject subject = this.securityManager.login(this, token);
//DefaultSecurityManager类中,SecurityManager委托给Authenticator执行认证逻辑
AuthenticationInfo info = this.authenticate(token);
// AuthenticatingSecurityManager类中,进行认证
this.authenticator.authenticate(token);
//AbstractAuthenticator类中,进行认证
AuthenticationInfo info = this.doAuthenticate(token);
//ModularRealmAuthenticator类中,获取多Realm进行身份认证
Collection<Realm> realms = this.getRealms();
doSingleRealmAuthentication(realm, token);
//ModularRealmAuthenticator类中,针对具体的Realm进行身份认证
AuthenticationInfo info = realm.getAuthenticationInfo(token);
//AuthenticatingRealm类中,调用对应的Realm进行校验,认证成功则返回用户属性
AuthenticationInfo info = realm.doGetAuthenticationInfo(token);
//SimpleAccountRealm类中,根据token获取账户信息
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
SimpleAccount account = getUser(upToken.getUsername());
//AuthenticatingRealm类中,比对传入的token和根据token获取到的账户信息
assertCredentialsMatch(token, info);
->getCredentialsMatcher().doCredentialsMatch(token, info);
//SimpleCredentialsMatcher类中,进行具体对比
byte[] tokenBytes = toBytes(tokenCredentials);
byte[] accountBytes = toBytes(accountCredentials);
MessageDigest.isEqual(tokenBytes, accountBytes);
//或
accountCredentials.equals(tokenCredentials);
上述代码包括了认证过程中一些核心流程,抽离出核心部分,整理成流程图如下:
可以看出,整个认证过程中涉及到了SecurityManager、Subject、Authenticator、Realm等组件,相关组件的功能可参考架构图中的功能说明。
授权原理
实例中授权调用的代码比较少,主要就是以下几个方法:
// 检查是否有相应角色权限
currentUser.hasRole("schwartz")
// 判断是否有资源操作权限
currentUser.isPermitted("lightsaber:wield")
// 判断是否有(更细粒度的)资源操作权限
currentUser.isPermitted("winnebago:drive:eagle5")
下面以hasRole方法为例,进行追踪分析源代码,看看具体的实现原理。
// 检查是否有相应角色权限
currentUser.hasRole("schwartz");
// DelegatingSubject类中,委托给SecurityManager判断角色与既定角色是否匹配
this.securityManager.hasRole(this.getPrincipals(), roleIdentifier);
// AuthorizingSecurityManager类中,SecurityManager委托Authorizer进行角色检验
this.authorizer.hasRole(principals, roleIdentifier);
// ModularRealmAuthorizer类中,获取所有Realm,并遍历检查角色
for (Realm realm : getRealms());
((Authorizer) realm).hasRole(principals, roleIdentifier)
// AuthorizingRealm中,Authorizer判断Realm中的角色/权限是否和传入的匹配
AuthorizationInfo info = getAuthorizationInfo(principal);
// AuthorizingRealm中,执行Realm进行授权操作
AuthorizationInfo info = this.doGetAuthorizationInfo(principals);
// SimpleAccountRealm类中,获得用户SimpleAccount(实现了AuthorizationInfo),
// users类型为Map,以用户名为key,对应shiro.ini中配置的初始化用户信息
return this.users.get(username);
// AuthorizingRealm类中,判断传入的用户和初始化配置的是否匹配
return hasRole(roleIdentifier, info);
// AuthorizingRealm类中,最终的授权判断
return info != null && info.getRoles() != null && info.getRoles().contains(roleIdentifier);
上述代码包括了授权过程中一些核心流程,抽离出核心部分,整理成流程图(isPermitted方法类似,读者可自行追踪),如下:
可以看出,整个认证过程中涉及到了SecurityManager、Subject、Authorizer、Realm等组件,相关组件的功能可参考架构图中的功能说明。
自定义Realm
通过上面认证和授权流程及原理的分析,会发现无论哪个操作都需要通过Realm来定义用户认证时需要的账户信息和授权时的权限信息。但一般情况下不会使用官网示例的基于“ini配置文件”的方式,而是通过自定义Realm组件来实现。
以下面的示例来说,我们可以使用Shiro内置的Realm组件:
public class AuthenticationTest {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before
public void addUser() {
// 在方法开始前添加一个用户
simpleAccountRealm.addAccount("wmyskxz", "123456");
}
@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
// 2.主体提交认证请求
// 设置SecurityManager环境
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 获取当前主体
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
// 登录
subject.login(token);
// subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("isAuthenticated:" + subject.isAuthenticated());
}
}
上述示例中创建了一个SimpleAccountRealm对象,并把初始化的账户信息通过addAccount方法添加进去。
实践中自定义Realm的方法通常是继承AuthorizingRealm类,并实现其doGetAuthorizationInfo方法和doGetAuthenticationInfo方法。在上面的流程梳理过程中,我们已经知道doGetAuthorizationInfo方法为授权功能的实现,而doGetAuthenticationInfo方法为认证的功能实现。关于具体实例,后续会用专门的实例来讲解。
小结
本篇文章从Shiro的整体架构、使用实例,再到认证和授权的源码分析,想必经过这番学习,大多数朋友已经了解了使用多年的Shiro框架到底是怎么运作的了。当了解了这些底层的实现,再回头看,是不是感觉之前有疑惑的地方豁然开朗了?是不是有一种原来如此的感觉?那么,恭喜你,你学到了。