本篇为《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 Brooks' 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/tree/main/shiro-demo
执行程序,打印日志信息如下,可以看到每一步的执行输出:
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框架到底是怎么运作的了。当了解了这些底层的实现,再回头看,是不是感觉之前有疑惑的地方豁然开朗了?是不是有一种原来如此的感觉?那么,恭喜你,你学到了。