1. Shiro的介绍
它是一个简单易用的java安全框架,可以运行在非Web环境
1.主要提供的功能
-
身份认证
- 大多时称为登录,这是证明用户身份的行为
-
授权管理
- 访问控制的过程,即决定"谁"可以做"什么"
-
会话管理
- 管理特定于用户的会话,在非Web环境也是这样
-
密码管理
- 对于密码的加密,保证密码的数据安全性
2. 支持的其他功能
- Web支持
- 可以很容器的嵌入Web环境进行安全防护
- 缓存支持
- 并发支持
- RememberMe支持
- 记住用户跨会话的身份,只需要登录一次则记住身份,下次自动登录
2. Shiro的架构
1. 抽象概述
- Shiro的体系结构有3个主要概念
1. Subject
-
概念
- 主题的本质是当前执行用户的安全特定视图,而用户通常代表着一个人,而主题可以是一个人,也可以代表一个第三方服务,Cron作业,可以是与当前于软件交互的任何东西
- Subject实例都绑定到(并需要)一个 SecurityManager。当我们与一个Subject进行交互时,这些交互会转换为与 SecurityManager 相关的特定Subject交互。
-
对于Subject,代表着用户,但是为什么不定义为User呢?
- 最初想定义为用户,但是大多数应用都存在自己的User类,可能会产生冲突,并且在安全领域,Subject是公认的术语
- Subject可以做什么
- 身份认证
- 授权(访问控制)
- 会话访问
- 注销
- 使用Subject
- 特殊场景下自定义Subject
- 使用Buider模式
SecurityManager securityManager = 通过ini文件或者创建实例对象 Subject subject = new Subject.Builder(securityManager).buildSubject();// 根据具体的Session对象的Id关联Subject Subject subject = new Subject.Builder().sessionId(sessionId).buildSubject();// 创建指定身份的Subject对象 Object userIdentity = "luck"; String realmName = "myRealm"; PrincipalCollection principals = new SimplePrincipalCollection(userIdentity, realmName); Subject subject = new Subject.Builder().principals(principals).buildSubject();
-
以上自定义的Subject不会自动与当前线程进行关联,也就是调用
SecurityUtils.getSubject()
并不会返回Subject对象,但是有三种情况可以让创建的Subject与当前线程关联-
执行Subject实例的execute方法
-
该方式使用与临时与线程关联
Subject subject; subject.execute( new Runnable() {public void run() {} });.// 对于框架也适用 Subject.Builder builder = new Subject.Builder(); Subject subject = builder.buildSubject(); return subject.execute(new Callable() {public Object call() throws Exception {return invoke(invocation, targetObject);} });
-
-
手动关联
Subject subject = new Subject.Builder()... ThreadState threadState = new SubjectThreadState(subject); threadState.bind(); try {} finally {threadState.clear(); }
-
不同线程需要共用Subject,执行Subject实例的associateWith,将返回值给另一个线程执行
Subject subject = new Subject.Builder() Runnable work = null; work = subject.associateWith(work); ExecutorService executor = java.util.concurrent.Executors.newCachedThreadPool(); executor.execute(work);
-
2. SecurityManager
-
是Shiro的核心架构,充当一种保护伞对象,协调器内部安全组件,共同形成一个对象图,一旦为应用程序配置了SecurityManager及其内部的对象图,我们就不需要管它了,我们只需要关注Subject的Api上就行
-
当我们与Subject交互的时候,实际上幕后的SecurityManner为Subject安全操作完成了所有繁重的工作
-
它能完成什么功能
- 认证
- 授权
- 会话管理
- 缓存管理
- Realm协调
- 事件传播
- “记住我”服务
- Subject创建
- 注销
- 其他
-
它包含的组件
- 身份验证器 (org.apache.shiro.authc.Authenticator)
- 授权器 (org.apache.shiro.authz.Authorizer)
- 会话管理器 (org.apache.shiro.session.mgt.SessionManager)
- 缓存管理器 (org.apache.shiro.cache.CacheManager)
- 记住我的管理器(org.apache.shiro.mgt.RememberMeManager)
- Subject工厂(org.apache.shiro.mgt.SubjectFactory)
-
创建SecurityManager对象(两种方式,编程式和配置式)
-
虽然可以直接实例化SecurityManager,但是这样非常的繁琐Spring例外,Spring就是创建SecurityManager对象,而不依赖INI配置
- 编程配置SecurityManager 对象
Realm realm = Realm实例对象 SecurityManager securityManager = new DefaultSecurityManager(realm); SecurityUtils.setSecurityManager(securityManager); // 配置自定义的会话管理 SessionDAO sessionDAO = new CustomSessionDAO(); ((DefaultSessionManager)securityManager.getSessionManager()).setSessionDAO(sessionDAO);
-
Shiro提供了一种基于文本的配置格式来配置SecurityManager会容易很多
-
默认提供的文件后缀为"*.ini",它容易阅读,使用简单,依赖少,通过INI文件可以配置出SecurityManager实例
-
Shiro的SecurityManager的实现与所有的JavaBean兼容,所以这使得Shiro可以使用任何类型的配置文件进行配置,例如XML(Spring),JSON,YML,Groovy等等
-
INI只是Shiro通用的文件格式,允许在任何环境中配置,以防止其他配置文件不可用
-
src/main/resources/shiro.ini INI文件解析
-
静态配置数据源
# ----------------------------------------------------------------------------- # 用户名 = 密码,角色1,角色N 来配置用户信息 # username = password, role1, role2, ..., roleN # ----------------------------------------------------------------------------- [users] root = secret, admin guest = guest, guest presidentskroob = 12345, president darkhelmet = ludicrousspeed, darklord, schwartz lonestarr = vespa, goodguy, schwartz# ----------------------------------------------------------------------------- # 角色权限配置 # 角色名称(用户属性中配置的角色) = 权限1,权限N # roleName = perm1, perm2, ..., permN # ----------------------------------------------------------------------------- [roles] admin = * schwartz = lightsaber:* goodguy = winnebago:drive:eagle5
public static void main(String[] args) {Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");SecurityManager securityManager = factory.getInstance();SecurityUtils.setSecurityManager(securityManager);Ini ini = new Ini();Factory<SecurityManager> factory = new IniSecurityManagerFactory(ini);SecurityManager securityManager = factory.getInstance();SecurityUtils.setSecurityManager(securityManager);// 方式二,底层原理就是上面的Environment environment = new BasicIniEnvironment("classpath:shiro.ini");SecurityManager securityManager = environment.getSecurityManager();SecurityUtils.setSecurityManager(securityManager); }
-
-
-
3. Realms Realm授权认证配置
- realm充当Shiro和应用程序安全数据之间的“桥接器”或“连接器”
- 当需要与安全相关的数据(如用户帐户)进行实际交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个realm中查找其中的许多内容
- 您可以根据需要配置尽可能多的realm(通常每个数据源一个),Shiro将根据身份验证和授权的需要与它们进行协调
- 从这个意义来讲,Realm本质上是一个特定与安全性的DAO,它封装数据源连接的细节,并根据需要讲相关的数据提供给Shiro,在配置Shiro时,必须指定至少一个用于身份验证或授权的Realm,SecurityManager可以配置多个Realm,但是至少需要一个
2. 详细的结构
- Subject (org.apache.shiro.subject.Subject) 主题(用户)
- 当前与软件交互的实体(用户、第三方服务、cron作业等)的特定于安全的“视图”。
- SecurityManager (org.apache.shiro.mgt.SecurityManager) 核心安全管理器
- SecurityManager是Shiro体系结构的核心。它主要是一个“保护伞”对象,用于协调其托管组件以确保它们顺利地协同工作,它还管理每个应用程序用户的Shiro视图,因此它知道如何对每个用户执行安全操作
- Authenticator (org.apache.shiro.authc.Authenticator) 身份认证器
- Authenticator是负责执行和响应用户的身份验证(登录)的组件
- 当用户尝试登录时,该逻辑由Authenticator执行
- Authenticator知道如何与存储相关用户/帐户信息的一个或多个realm协调。从这些realm中获得的数据用于验证用户的身份,以确保用户确实是需要验证的这个人
- 如果配置了多个Realm, AuthenticationStrategy将协调这些Realm,是一个决定最终结果的策略,来确定验证成功或失败的条件(例如,如果一个Realm成功,而其他Realm失败,是全部成功还是不成功)
- Authorizer (org.apache.shiro.authz.Authorizer) 授权器
- Authorizer是负责确定应用程序中用户访问控制(授权)的组件
- 它是一种最终决定用户是否被允许做某事的机制
- 与Authenticator一样,Authorizer也知道如何与多个后端数据源协调以访问角色和权限信息,授权方使用这些信息来确定是否允许用户执行给定的操作
- SessionManager (org.apache.shiro.session.mgt.SessionManager) 会话管理
- SessionManager知道如何创建和管理用户会话生命周期,为所有环境中的用户提供健壮的会话体验。这是安全框架Shiro领域的一个独特特性
- Shiro能够在任何环境中本地管理用户会话,即使没有可用的Web/Servlet或EJB容器
- 默认情况下,Shiro将使用现有的会话机制(例如Servlet容器)
- 但如果没有,例如在独立应用程序或非web环境中,它将使用其内置的企业会话管理来提供相同的编程体验
- SessionDAO (org.apache.shiro.session.mgt.eis.SessionDAO)
- SessionDAO接口就是为了允许使用任何数据源类型来持久化Session会话信息
- 例如缓存Session,内存缓存Session,企业级缓存等等
- SessionDAO代表SessionManage来r执行会话持久化(CRUD)操作,这允许将任何数据存储插入到会话管理基础结构中,换句话说,会话是由SessionManage管理,而会话的CRUD操作是交给SessionDAO来完成
- SessionDAO接口就是为了允许使用任何数据源类型来持久化Session会话信息
- CacheManager (org.apache.shiro.cache.CacheManager) 缓存管理
- 创建和管理其他Shiro组件使用的Cache实例生命周期
- 由于Shiro可以访问许多后端数据源以进行身份验证、授权和会话管理,因此缓存一直是框架中用于在使用这些数据源时提高性能的一流体系结构特性
- 任何现代开源和/或企业缓存产品都可以插入到Shiro中,以提供快速高效的用户体验。
- Cryptography (org.apache.shiro.crypto.*) 密码匹配器
- 密码学密码学是企业安全框架的补充
- Shiro的加密包包含易于使用和理解的加密密码、哈希(又名摘要)和不同编解码器实现的表示
- Shiro的加密api简化了复杂的Java机制,并使加密技术易于普通人使用。
- Realms (org.apache.luck.apache.shiro.realm.Realm) Realm流程配置
- realm充当Shiro和应用程序安全数据之间的“桥接器”或“连接器”
- 当需要与安全相关的数据(如用户帐户)进行实际交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个realm中查找其中的许多内容
- 您可以根据需要配置尽可能多的realm(通常每个数据源一个),Shiro将根据身份验证和授权的需要与它们进行协调
- 从这个意义来讲,Realm本质上是一个特定与安全性的DAO,它封装数据源连接的细节,并根据需要讲相关的数据提供给Shiro,在配置Shiro时,必须指定至少一个用于身份验证或授权的Realm,SecurityManager可以配置多个Realm,但是至少需要一个
3. INI文件解析
1. [main]
-
配置应用程序实例及其任何依赖项(例如 Realms)的地方
-
配置对象实例(如SecurityManager或其任何依赖项)听起来像是一件困难的事情,因为 INI 只能使用名称/值对但是通过对对象图的一点约定和理解,你会发现你可以做很多事情
-
Shiro 使用这些假设来实现简单但相当简洁的配置机制我们经常喜欢将这种方法称为“简单版”的依赖注入,虽然不如成熟的 Spring/Guice/JBoss XML强大,但你会发现它完成了相当多的工作,没有太多的复杂性。当然,其他配置机制也可用,但它们不需要使用 Shiro
-
如果需要定义对象,则考虑使用以下代码
- 默认使用Apache Commons BeanUtils完成该工作
- $引用之前引用的实例
- 对象的创建顺序与文件的书写顺序一样,要注意
[main] # 创建对象 sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher # 创建对象 myRealm = com.company.security.shiro.DatabaseRealm # 属性赋值 myRealm.connectionTimeout = 30000 myRealm.username = jsmith myRealm.password = secret # 属性引用赋值 myRealm.credentialsMatcher = $sha256Matcher# 给securityManager赋值,它是一个特殊的实例,不需要我们配置 # 多层属性赋值 securityManager.sessionManager.globalSessionTimeout = 1800000 securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==# 集合属性赋值 sessionListener1 = com.company.my.SessionListenerImplementation sessionListener2 = com.company.my.other.SessionListenerImplementation securityManager.sessionManager.sessionListeners = $sessionListener1, $sessionListener2# Map object1 = com.company.some.Class object2 = com.company.another.Class anObject = some.class.with.a.Map.property anObject.mapProperty = key1:$object1, key2:$object2anObject.map = $objectKey1:$objectValue1, $objectKey2:$objectValue2# 变量插入值 对于常量 ${const:com.example.YourClass.CONSTANT_NAME} 对于环境变量 ${ENV_VARIABLE_NAME} 系统属性 ${system.property}# 变量默认值 ${const:com.example.YourClass.CONSTANT_NAME:-default_value}${VARIABLE_NAME:-default_value}
2. [users]
-
部分允许您定义一组静态用户帐户。这在用户帐户数量非常少或不需要在运行时动态创建用户帐户的环境中非常有用
-
自动 IniRealm
- 只需定义非空的 [users] 或 [roles] 部分,就会自动触发 org.apache.luck.apache.shiro.realm.text.IniRealm 实例的创建,并使其在名称下的 [main] 部分中可用。 您可以像配置任何其他对象一样对其进行配置
-
行数据格式
username
=password
、roleName1、roleName2、…、roleNameN- 等号左侧的值是用户名
- 等号右侧的第一个值是用户的密码。密码是必需的。
- 密码后面的任何逗号分隔值都是分配给该用户的角色的名称。角色名称是可选的。
[users] admin = secret lonestarr = vespa, goodguy, schwartz darkhelmet = ludicrousspeed, badguy, schwartz
-
密码加密
-
从 Shiro 2.0 开始,该部分不能包含纯文本密码
[main] # Shiro2CryptFormat [users] # user1 = sha256-hashed-hex-encoded password, role1, role2, ... user1 = $shiro2$argon2id$v=19$t=1,m=65536,p=4$H5z81Jpr4ntZr3MVtbOUBw$fJDgZCLZjMC6A2HhnSpxULMmvVdW3su+/GCU3YbxfFQ, role1, role2, ...
-
3. [roles]
-
部分允许您将权限与 [users] 部分中定义的角色相关联。同样,这在角色数量较少或不需要在运行时动态创建角色的环境中非常有用
[roles] # admin 具有所有权限 admin = * schwartz = lightsaber:* goodguy = winnebago:drive:eagle5
-
行格式
- 部分中的每一行都必须按以下格式定义角色到权限的键/值映射
rolename
= permissionDefinition1, permissionDefinition2, …, permissionDefinitionN- 其中 permissionDefinition 是任意字符串,但大多数人都希望使用符合 更改为 org.apache.shiro.authz.permission.WildcardPermission 格式,以方便使用和灵活
- 内部逗号
- 请注意,如果单个 permissionDefinition 需要在内部以逗号分隔(例如),则需要用双引号 (“) 将该定义括起来以避免解析错误:'”printer:5thFloor:print,info“'printer:5thFloor:print,info
- 没有权限的角色
如果您的角色不需要权限关联,则无需在 [roles] 部分中列出它们(如果您不想这样做)。只需在 [users] 部分中定义角色名称就足以创建角色(如果该角色尚不存在)
4. [urls]
-
该urls部分允许您执行我们尚未看到的任何 Web 框架中都不存在的功能
-
能够为应用程序中任何匹配的 URL 路径定义临时过滤器链!
-
[urls]这比您通常定义过滤链的方式要灵活、强大和简洁得多:即使您从未使用过 Shiro 提供的任何其他功能,并且只使用过这个功能,仅此一项就值得使用,web.xml
-
URL 路径表达式
- 等号 (=) 左侧的标记是相对于 Web 应用程序的上下文根的 Ant样式路径表达式
/account/** = ssl, authc
- 等号 (=) 右侧的标记是逗号分隔的过滤器列表,用于执行与该路径匹配的请求。它必须与以下格式匹配:
filter1**[optional_config1]**, filter2**[optional_config2]**, ..., filterN**[optional_configN]**
- 等号 (=) 左侧的标记是相对于 Web 应用程序的上下文根的 Ant样式路径表达式
-
行格式
-
URL_Ant_Path_Expression = Path_Specific_Filter_Chain
[urls] /index.html = anon /user/create = anon /user/** = authc /admin/** = authc, roles[administrator] /rest/** = authc, rest /remoting/rpc/** = authc, perms["remote:invoke"]
-
所有的路径都相对于
HttpServletRequest.getContextPath()
-
-
[urls]配合[main]使用过滤器
[main] authc.loginUrl = /login.jsp myFilter = com.company.web.some.FilterImplementation myFilter.property1 = value1[urls] /account/** = authc /some/path/** = myFilter
-
支持的过滤器列表
Filter Name Class anon org.apache.shiro.web.filter.authc.AnonymousFilter authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter authcBearer org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter invalidRequest org.apache.shiro.web.filter.InvalidRequestFilter logout org.apache.shiro.web.filter.authc.LogoutFilter noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter port org.apache.shiro.web.filter.authz.PortFilter rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter ssl org.apache.shiro.web.filter.authz.SslFilter user org.apache.shiro.web.filter.authc.UserFilter
4. Shiro使用和原理
1. Shiro配置
1.1 依赖
<dependencies><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>1.7.21</version><scope>test</scope></dependency><dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>1.7.21</version><scope>test</scope></dependency></dependencies>
1.2 启用Shiro
-
第一件事就是创建SecurityManager实例,并且每个应用程序只需要存在一个SecurityManager就足够了
- 创建SecurityManager或者使用INI配置加载SecurityManager对象
-
获取Subject对象
-
使用Subject开始工作
-
获取当前正在执行的用户
Subject currentUser= SecurityUtils.getSubject()
- 在非WEB环境中,调用可能返回应用程序特定位置的基于用户的数据
- 在WEB应用中,它获取与当前线程或传入请求关联的基于用户的数据
-
获取会话,操作会话Session
- 在用户与应用程序的当前会话期间向用户提供内容
Session session = currentUser.getSession(); session.setAttribute( "someKey", "aValue" );
2. 该Session提供了常用的HttpSession的大部分功能,有一个最大的区别就是,它不需要Web环境,但是在Web环境,它和HttpSession功能一样
-
判断当前用户是否认证
// 未认证 if ( !currentUser.isAuthenticated() ) {UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");// 开启Remember-Me功能token.setRememberMe(true);// 登录currentUser.login(token); }
-
登录认证
try {currentUser.login( token ); } catch ( UnknownAccountException uae ) { } catch ( IncorrectCredentialsException ice ) {} catch ( LockedAccountException lae ) { } } catch ( AuthenticationException ae ) { }
-
获取当前登录用户的信息
currentUser.getPrincipal();
-
是否具有某种角色权限
if ( currentUser.hasRole( "schwartz" ) ) { } else { }if ( currentUser.isPermitted( "lightsaber:wield" ) ) { } else { }
-
注销登录
currentUser.logout();
-
非WEB环境最终的使用案例\
public class Tutorial {public static void main(String[] args) {Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");SecurityManager securityManager = factory.getInstance();SecurityUtils.setSecurityManager(securityManager);Subject currentUser = SecurityUtils.getSubject();Session session = currentUser.getSession();session.setAttribute("someKey", "aValue");String value = (String) session.getAttribute("someKey");if (value.equals("aValue")) {}if (!currentUser.isAuthenticated()) {UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");token.setRememberMe(true);try {currentUser.login(token);} catch (UnknownAccountException uae) {} catch (IncorrectCredentialsException ice) {} catch (LockedAccountException lae) {}catch (AuthenticationException ae) {}}if (currentUser.hasRole("schwartz")) {} else {}//test a typed permission (not instance-level)if (currentUser.isPermitted("lightsaber:wield")) {} else {}if (currentUser.isPermitted("winnebago:drive:eagle5")) {} else {}currentUser.logout();System.exit(0);} }
-
-
2. 身份认证配置
- 有四个必要知道的术语
- Subject
- 与应用程序交互的任何人或事物
- Principals
- Subject的身份属性,例如用户名,用户相关的姓名姓氏等等身份信息
- Credentials
- 用于验证身份的机密数据。密码、生物识别数据
- Realms
- 特定于Security的 DAO、数据访问对象、与后端数据源通信的软件组件
- 您将在每个后端数据源中使用一个Realm,并且 Shiro 将知道如何与这些领域一起协调以完成您必须做的事情
- 也就是它是读取数据库用户身份认证和授权信息的DAO,将这些数据交给Shiro就能完成认证授权
- Subject
- 启用身份验证分为三个步骤
- 收集Subject的Principals主体和Credentials凭据
- 将Principals主体和Credentials凭据提交到身份验证系统
- 允许访问、重试身份验证或阻止访问(访问控制,权限控制)
1. 步骤一(收集用户信息和密码)
UsernamePasswordToken token = new UsernamePasswordToken( username, password );
# 可选,开启Remember-Me
token.setRememberMe(true);
2. 步骤二(将用户凭证提交给Shiro验证系统)
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(token);
3. 步骤三(确定是否认证成功以及权限控制)
try {currentUser.login(token);
} catch ( UnknownAccountException uae ) { ...
} catch ( IncorrectCredentialsException ice ) { ...
} catch ( LockedAccountException lae ) { ...
} catch ( ExcessiveAttemptsException eae ) { ...
}
} catch ( AuthenticationException ae ) {
}
4. 可选步骤
-
Remember-Me的支持
UsernamePasswordToken token = new UsernamePasswordToken( username, password ); # 可选,开启Remember-Me token.setRememberMe(true);
- 在 Shiro 中,Subject 对象支持两种方法:isRemembered() 和 isAuthenticated()
- “被记住的”Subject 具有身份(它不是匿名的),并且其Principals属性(称为Principals主体)会从上一个会话期间的成功身份验证中记住
- 经过身份验证的Subject已经证明了其身份,但是如果一个Subject被记住,并不意味着他们就一定被认证了。因为上次认证成功记住的身份信息可能发生改变,所以不能保证他们已经被认证
- Remember-Me与认证
- 在Shiro中,记住的Subject不能代表他们经过了身份认证的Subject,这一点很重要,认证是一种更严格的检查,因为身份验证是一个证明自己身份的一个过程,可以确定你是谁,当用户被记住,记住的身份会让系统知道该用户
可能
是谁,但无法保证它一定
是谁,一旦对象被认证,那系统就不再将他们视为记住它,而是确定该用户已经被信任和认证 - 虽然应用很多时候任然可能基于记住我的Subject执行特定的逻辑,但是,在一个用户经过认证通过之前,不应该执行高度敏感的操作
- 案例: (当我们开启了记住我)
- 当我们在京东登录,添加书籍到购物车中,时间长了,用户会话已经过期了,但是京东会记住你的身份,还会推荐与之类似的图书,此时Shiro的isRemember返回true,isAuthenticated返回false,但是当你准备修改用户信息或者付款操作的时候,此时会强制你进行身份认证进行认证,否则无法完成操作,登录之后,isAuthenticated返回true ,isRemember返回true
- 在Shiro中,记住的Subject不能代表他们经过了身份认证的Subject,这一点很重要,认证是一种更严格的检查,因为身份验证是一个证明自己身份的一个过程,可以确定你是谁,当用户被记住,记住的身份会让系统知道该用户
-
注销支持
currentUser.logout();
- 当使用应用程序之后,可以进行注销
- 当您注销 Shiro 时,它将关闭用户会话并从Subject实例中删除任何关联的身份
- 如果您在 Web 环境中使用 RememberMe,则默认情况下也会从浏览器中删除 RememberMe cookie
5. Shiro认证原理
-
流程步骤
- 应用程序调用
Subject.login()
方法,传入表示用户信息和凭证的构造实例对象AuthenticationToken
- 通常Subject的实例为DelegatingSubject(或子类),实际的工作入口为:
securityManager.login(token)
- SecurityManager作为基本的伞型组件(内部包含了很多东西的保护伞),它继承了
org.apache.shiro.authc.Authenticator
认证器,最终调用认证器Authenticator.authenticate(token),将AuthenticationToken凭证信息委托给Authenticator
实例来完成认证操作- 这个实例一般都是
ModularRealmAuthenticator
的实例对象,它支持身份验证期间,协调一个或多个Realm实例进行工作
- 这个实例一般都是
- 如果为应用程序配置了多个身份验证Realm实例,则ModularRealmAuthenticator实例将使用其配置的 AuthenticationStrategy 启动多重身份验证尝试,根据具体的认证策略来决定是否认证成功
- 在单个Realm实例的情况下,不需要配置AuthenticationStrategy
- 执行每一个Realm实例,调用Realm实例的
support
方法,该实例将给定AuthenticationToken参数来决定是否支持处理,如果支持,那么就调用该Realm中的getAuthenticationInfo
方法来处理并返回最终的认证信息
- 应用程序调用
-
Shiro 身份认证器实现默认使用 ModularRealmAuthenticator 实例同样支持具有单个Realm 的应用程序以及具有多个 Realm 的应用程序
-
在单领域应用程序中,将直接调用单个Realm
-
如果配置了两个或多个领域,它将使用一个
AuthenticationStrategy
实例来协调这些Realm进行工作 -
虽然 ModularRealmAuthenticator可以完成大部分需求,但是如果您希望使用自定义Autienticator配置到SecurityManager中,可以在shiro.ini配置
[main] authenticator = com.foo.bar.CustomAuthenticatorsecurityManager.authenticator = $authenticator
-
身份验证策略
AuthenticationStrategy
-
当为应用程序配置了两个或多个Realm时,ModularRealmAuthenticator将依靠内部 AuthenticationStrategy 组件来确定身份验证尝试成功或失败的条件
- 例如,如果只有一个 Realm 认证成功,而其他所有 Realm 都失败了该如何做出决定
-
AuthenticationStrategy是一个无状态的组件,在身份验证期间会被执行四次
- 在调用认证Realm之前
- 在调用Realm的
getAuthenticationInfo
方法之前 - 在调用Realm的
getAuthenticationInfo
方法之后 - 在所有的Realm执行之后
-
此外,AuthenticationStrategy最终还负责聚合所有Realm的结果,并将它们封装到单个
AuthenticationInfo
认证信息对象中,AuthenticationInfo
是由Authenticator最终返回的,并且shiro将它作为最终表明用户身份的对象 -
身份验证策略提供的实例
- 默认策略为AtLeastOneSuccessfulStrategy
AuthenticationStrategy
类描述 AtLeastOneSuccessfulStrategy
如果一个或多个 Realm 身份验证成功,则整体尝试被视为成功。 如果没有身份验证成功,则尝试失败。 FirstSuccessfulStrategy
仅使用从第一个成功认证的 Realm 返回的信息。 所有进一步的领域都将被忽略。 如果没有身份验证成功,则尝试失败。 AllSuccessfulStrategy
所有配置的 Realm 都必须成功进行身份验证,才能将整个尝试视为成功。 如果任何一方未成功进行身份验证,则尝试失败。 -
在INI文件中配置自己的策略INI解析
[main] authcStrategy = org.apache.shiro.authc.pam.FirstSuccessfulStrategysecurityManager.authenticator.authenticationStrategy = $authcStrategy
-
-
-
Realm认证
3. 授权配置
- 授权有三个核心元素
- 权限
- 权限在安全策略中,属于原子级别,它们是功能的声明,可以表示在程序中执行的某个具体的操作
- 正确的权限可以限定资源类型以及资源交互的操作,例如数据的操作CRUD
- 粒度
- 资源级别
- 某个库
- 实例级别
- 某个表
- 属性级别
- 某个字段
- 资源级别
- 角色
- 在授权的上下文中,角色实际上是用于简化权限和用户管理的权限集合,因此,为用户分配的是角色而不是分配权限
- Shiro支持两种角色
- 隐式角色
- 就是分配一个具体的角色,它就具有该角色下所有能操作的权限,这是最简单的,但是项目大会带来很多维护和管理的问题
- 显示角色
- 显示分配给它权限,因此它表示一组显示权限的集合,虽然你可能存在某个隐式角色,但是不一定有该隐式角色下的所有权限,对于该角色,是显示分配的,分配你什么权限,你就有什么权限
- 隐式角色
- 用户
- 在Shiro中,用户的概念就是Subject,为什么使用Subject而不是user,因为user通常只一个人,而Subject指的是任何与程序交互的事物
- 权限
- 如何使用授权
1. 以编程方式
1. Java 代码中使用 if/else块等结构执行授权检查1. 角色检查```Subject currentUser = SecurityUtils.getSubject();if (currentUser.hasRole("administrator")) {} else {}```1. 如果使用这种方式,最简单,但是,它的缺点是,它是一个隐式角色,写死了2. 权限检查1. 基于字符串```String perm = "printer:print:laserjet4400n";if(currentUser.isPermitted(perm)){} else {}```2. 基于实现Permission接口```Subject currentUser = SecurityUtils.getSubject();Permission printPermission = new PrinterPermission("laserjet3000n","print");If (currentUser.isPermitted(printPermission)) {} else {}```
2. JDK注解方式
-
将注解标注在方法上
RequiresAuthentication - 要求当前 Subject 在其当前会话期间已通过身份验证RequiresGuest - 要求当前 Subject 是“游客”RequiresPermissions - 要求当前执行程序的Subject需要特定权限才能执行RequiresRoles - 要求当前正在执行的主体具有所有指定的角色。如果他们没有角色,则不会执行该方法,并引发 AuthorizationException。RequiresUser - 要求当前 Subject 是要访问应用程序的用户
@RequiresPermissions("account:create") public void openAccount( Account acct ) { }@RequiresRoles( "teller" ) public void openAccount( Account acct ) {}
3. JSP等等标签库
-
您可以根据角色和权限控制 jsp 或 gsp 页面输出
- 官网支持的标签适用于 Apache Shiro 的 JSP/GSP 标签库
<%@ taglib prefix="shiro" uri=http://shiro.apache.org/tags %> <html> <body><shiro:hasPermission name="users:manage"><a href="manageUsers.jsp">Click here to manage users</a></shiro:hasPermission><shiro:lacksPermission name="users:manage">No user management for you!</shiro:lacksPermission> </body> </html>
4. Shiro授权原理
- 流程步骤
- 只要程序或者框架,调用
Subject hasRole*, checkRole*, isPermitted*, or checkPermission*
这种方法,传递角色权限的表现形式,就会执行授权流程 - Subject实例,通常都是DelegatingSubject或子类,当Subject调用上面方法时,实际上是委托给SecurityManager的对应方法,因为SecurityManager实现了
org.apache.shiro.authz.Authorizer
授权器接口,因此他有所有的授权方法 - SecurityManager作为基本的伞型组件(大容器,包含很多组件),调用授权器的权限校验方法,这些方法委托给内部的Authorizer组件完成授权工作
- SecurityManager将这个授权任务Permission(权限)和Role(角色)委托给Authorizer(授权器)
- 默认情况下,授权器的实例是ModularRealmAuthorizer实例,它支持在任何授权操作期间协调一个或多个Realm实例进行授权工作
- 执行每一个配置的Realm,看它是否实现了Autorizer接口,如果实现了,则调用Realm本身的hasRole等等授权方法完成授权
- 如果没有实现Authorizer接口,则该Realm不会被执行
- 只要程序或者框架,调用
- 授权Realm对象执行下面几个操作来完成授权操作
- 通过Realm授权方法
doGetAuthorizationInfo
返回的 AuthorizationInfo 授权信息 - 调用getObjectPermissions()和 getStringPermissions ()方法并聚合结果,直接识别分配给 Subject 的所有权限
- 如果注册了
RolePermissionResolver
对象,则他会调用 RolePermissionResolver对象的RolePermissionResolver.resolvePermissionsInRole()
的方法根据赋予的角色来检索该角色对应的权限
- 如果注册了
- 对于A和B的聚合权限,调用
Permission.implies()
方法来检查该权限是否是隐式权限隐式角色
- 通过Realm授权方法
- Realm的授权
4. Realm配置
1. 概念介绍
- Realm是可以访问特定于应用程序的安全数据(如用户、角色和权限)的组件, 它将应用程序的数据转换为 Shiro 可以理解的格式,因此 Shiro 可以反过来提供一个易于理解的主题编程 API
- 无论存在多少个数据源或数据,Realm通常与数据源(如关系数据库、LDAP 目录、文件系统或其他类似资源)具有 1 对 1 的关联,因此,该接口的实现使用特定于数据源的 API 来发现授权数据(角色、权限等),例如 JDBC、文件 IO、Hibernate 或 JPA,或任何其他数据访问 API
- Realm 本质上是一个查询安全数据的 DAO
2. Realm配置
-
Realm配置使用INI配置
-
显示配置
securityManager.realms = xxx
fooRealm = com.company.foo.Realm barRealm = com.company.another.Realm bazRealm = com.company.baz.RealmsecurityManager.realms = $fooRealm, $barRealm, $bazRealm
- 可以精确的控制每一个Realm的顺序
-
隐式配置
不带securityManager.realms =
,与上面效果一样,不推荐使用,顺序非常重要blahRealm = com.company.blah.Realm fooRealm = com.company.foo.Realm barRealm = com.company.another.Realm
-
3. Realm身份认证配置 身份认证流程
-
Realm的配套
AuthenticationTokens
- 在指执行Realm的认证方法之前,会调用
support
方法,只有返回true才能使用该Realm处理 - 一般情况下,Realm处理的类型为AuthenticationToken(接口或者类),例如生物人脸数据这些,Realm可能无法处理
- 在指执行Realm的认证方法之前,会调用
-
Realm执行的流程
- 获取AuthenticationToken需要验证的信息
- 基于AuthenticationToken的principal主体信息,从数据源中查找对应的数据
- 确保提供的令牌秘钥与数据源中的一致
- 如果凭据匹配,则返回AuthenticationInfo 实例,该实例为Shiro指定用户已经认证的信息对象
- 如果不匹配,引发 AuthenticationException异常
-
Realm的实现
- 一般我们不直接实现Realm,那样比较繁琐,Shiro提供了几个实现
- CachingRealm(缓存相关)
- AuthenticatingRealm(认证相关) 继承 CachingRealm(缓存相关)
- AuthorizingRealm(授权相关) 继承 AuthenticatingRealm(认证相关)
- 因此,最最简单的方式就是继承AuthorizingRealm
- 一般我们不直接实现Realm,那样比较繁琐,Shiro提供了几个实现
-
Realm完成密码匹配
- 将提交的凭据(密码)在Realm中进行加密存储或匹配时每一个Realm的责任,而不是
Authenticator
认证器的责任 - 对于每一个Realm都知道凭证的格式和存储的方式,并且可以执行具体的匹配,而Authenticator是一个通用组件,只负责认证
- 将提交的凭据(密码)在Realm中进行加密存储或匹配时每一个Realm的责任,而不是
-
在认证的Realm(AuthenticatingRealm)存在必要的CredentialsMatcher密码匹配器
-
Shiro提供了一些内置的匹配器,SimpleCredentialsMatcher 和 HashedCredentialsMatcher,可以按照以下来配置
Realm myRealm = new com.company.luck.apache.shiro.realm.MyRealm(); CredentialsMatcher customMatcher = new com.company.luck.apache.shiro.realm.CustomCredentialsMatcher(); myRealm.setCredentialsMatcher(customMatcher);
[main] customMatcher = com.company.luck.apache.shiro.realm.CustomCredentialsMatcher myRealm = com.company.luck.apache.shiro.realm.MyRealm myRealm.credentialsMatcher = $customMatcher
-
在Shiro中,默认都是使用SimpleCredentialsMatcher 进行匹配
- 例如: 用户提交了UsernamePasswordToken进行验证,则该匹配器直接获取token中的凭证与数据源中的凭证进行比较
-
对于HashedCredentialsMatcher,有多种不同的实现
- 它们更安全,并且可以加盐加密,并且可以加密多次,到达一个绝对安全的效果
org.apache.shiro.crypto.hash.XXXHash Md5Hash,Sha1Hash SimpleHash等等RandomNumberGenerator rng = new SecureRandomNumberGenerator(); Object salt = rng.nextBytes(); String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64();User user = new User(username, hashedPasswordBase64); user.setPasswordSalt(salt); userDAO.create(user);
-
在ini文件中配置算法和加密次数
[main] credentialsMatcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher credentialsMatcher.storedCredentialsHexEncoded = false credentialsMatcher.hashIterations = 1024 credentialsMatcher.hashSalted = truemyRealm = com.company..... myRealm.credentialsMatcher = $credentialsMatcher
-
如果使用了salt盐进行加密并且保存到数据库中,则返回的用户认证信息需要为``SaltedAuthenticationInfo`类型,而不能是普通类型,因为需要盐值才能正确的与数据库的密码进行匹配
User user = new User(username, hashedPasswordBase64); user.setPasswordSalt(salt); userDAO.create(user);
-
-
-
如何禁用Realm的身份验证
- 如果出于某种原因,不希望 Realm 对数据源执行身份验证(可能是因为您只想让 Realm 执行授权),则可以通过Realm的supports方法始终返回false来完全禁用 Realm 对身份验证的支持。 然后,在身份验证尝试期间,将永远不会执行Realm的认证方法
- 当然,如果要对Subject进行身份验证,那么至少需要一个Realm可以支持AuthenticationTokens 的认证
4. Realm授权配置 授权流程
-
Realm的授权执行顺序是按照配置的顺序执行的,都是迭代来执行每一个实例
-
授权Realm对象执行下面几个操作来完成授权操作
- 通过Realm授权方法
doGetAuthorizationInfo
返回的 AuthorizationInfo 授权信息 - 调用getObjectPermissions()和 getStringPermissions ()方法并聚合结果,直接识别分配给 Subject 的所有权限
- 如果注册了
RolePermissionResolver
对象,则他会调用 RolePermissionResolver对象的RolePermissionResolver.resolvePermissionsInRole()
的方法根据赋予的角色来检索该角色对应的权限
- 如果注册了
- 对于A和B的聚合权限,调用
Permission.implies()
方法来检查该权限是否是隐式权限隐式角色
- 通过Realm授权方法
-
配置全局的
PermissionResolver
-
在执行权限检查的时候,会先将String类型的权限转换为Permission实例,再来进行权限校验,这是因为权限是基于隐式逻辑来判断的,而不是直接使用相等来判断,隐式逻辑在代码中比通过字符串能更好的表示,因此,大多数Real需要将提交的权限字符串转换或解析为相应代表的Permission实例来完成操作
-
为了实现这种String到Permission的转换,Shiro提供了PermissionResolver权限解析器,大多数Realm实现PermissionResolver来支持Authorizer接口中基于字符串的权限校验方法,当调用其中的方法的时候,使用PermissionResolver解析为Permission,并以这种方式来校验权限
-
Shiro内部默认使用的是WildcardPermissionResolver(通配符的解析器)
-
比如有自己的权限字符串语法,则可以自己实现PermissionResolver,如果希望想让自己所有的Realm都支持自己的权限字符串语法,则可以定义一个全局的PermissionResolver
-
在ini文件中配置
-
单个Realm配置PermissionResolver
-
同java代码的setPermissionResolver方法
permissionResolver = com.foo.bar.authz.MyPermissionResolverrealm = com.foo.bar.realm.MyCustomRealm realm.permissionResolver = $permissionResolver
-
全局Realm配置PermissionResolver
- 必须实现PermissionResolverAware接口,这样,系统会自动将PermissionResolver设置到每一个Realm中
globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
-
-
-
-
与PermissionResolver一样,也可以配置全局的角色字符串转Permission对象的RolePermissionResolver
-
单个Realm配置
rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolverrealm = com.foo.bar.realm.MyCustomRealm realm.rolePermissionResolver = $rolePermissionResolver
-
全局Realm配置
-
必须实现RolePermissionResolverAware接口,这样,系统会自动将RolePermissionResolver设置到每一个Realm中
globalRolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver
-
-
-
-
如果应用程序需要多个Realm来执行授权,但是默认的
"短路"
迭代不是自己期待的方式,此时可以自定义授权器,因为只要有一个Realm具有该权限则其他Realm则不会处理[main] authorizer = com.foo.bar.authz.CustomAuthorizersecurityManager.authorizer = $authorizer
5. 会话管理
1. 概念
- Shiro在安全领域提供了使用于任何应用程序的完整企业级的Session解决方案,从最简单的命令行到企业的集群Web应用项目
- 在Shiro之前,如果需要开启会话支持,需要在Web容器中部署应用程序,现在Shiro的Session支持比其他机制更易于使用和管理,并且可以在任何地方使用,如论有没有Web容器
2. Shiro中Session的特征
- 基于POJO/JavaSE,对IOC比较友好,Session会话以及会话管理都是基于接口的,并且使用POJO实现,这使得它们可以兼容任何形式的配置文件,例如YML,JSON,XML等等存储的数据,可以很容易的配置会话组件
- 自定义会话存储
- 由于 Shiro 的 Session 对象是基于 POJO 的,因此会话数据可以很容易地存储在任意数量的数据源中。这允许您自定义应用程序会话数据的确切位置 - 例如,文件系统、内存、网络分布式缓存、关系数据库或专有数据存储
- 独立于容器的集群
- Shiro 的会话可以使用任何现成的网络缓存产品(如Ehcache )轻松实现集群,这意味着您可以为 Shiro 配置一次且只能配置一次会话群集,无论您部署到哪个容器,您的会话都将以相同的方式进行群集。无需特定于容器的配置
- 异构客户端的访问
- 与Web会话不同,Shiro 会话可以在各种客户端技术之间“共享”,例如,桌面应用程序可以“查看”和“共享”同一用户在 Web 应用程序中使用的相同物理会话
- 事件监听器
- 事件侦听器允许您在会话的生命周期内侦听生命周期事件。您可以侦听这些事件,并针对自定义应用程序行为做出反应,例如,在会话过期时更新用户记录
- 主机地址的保留
- Shiro 会话保留发起会话的主机的 IP 地址或主机名。这允许您确定用户所在的位置并做出相应的反应(在 IP关联确定是否为内网环境中通常很有用)
- 非活跃/过期的会话支持
- 会话按预期因不活动而过期,但如果需要,可以通过
touch()
方法延长会话以保持会话“活动”状态。这在富 Internet 应用程序 (RIA) 环境中很有用,在这些环境中,用户可能正在使用桌面应用程序,但可能不定期与服务器通信,但服务器会话不应过期。
- 会话按预期因不活动而过期,但如果需要,可以通过
- 透明的WEB使用
- Shiro 的 Web支持完全实现并支持会话的 Servlet 2.5 规范(HttpSession接口及其所有关联的 API)。这意味着您可以在现有的 Web 应用程序中使用 Shiro 会话,并且无需更改任何现有的 Web 代码
- 可用于 SSO
- 由于 Shiro 会话是基于 POJO 的,因此它们可以轻松存储在任何数据源中,并且可以在需要时在应用程序之间“共享”。我们称之为“简单版本的 SSO”,它可用于提供简单的登录体验,因为共享会话可以保留身份验证状态。
3. 使用会话
Subject currentUser = SecurityUtils.getSubject();Session session = currentUser.getSession();
session.setAttribute( "someKey", someValue);
- 如果Subject已经有Session,返回当前session
- 如果Subject不存在Session,并且参数为true,表示创建一个新的Session并返回
- 如果Subject不存在Session,并且参数为false,不会创建Session返回null
4. 会话管理器
-
SessionManager顾名思义,管理应用程序中所有Subject中会话的创建,删除,失效等验证,是维护Session的顶级组件,SessionManager默认实现为
DefaultSessionManager
-
在Web应用程序中,使用的SessionManager不同,使用的是
ServletContainerSessionManager
,它本质上是Shiro的会话API到Servlet容器的桥梁,几乎没有其他功能 -
在Shiro.ini中配置SessionManager
[main] sessionManager = com.foo.my.SessionManagerImplementation securityManager.sessionManager = $sessionManager
-
会话超时
-
默认情况下,Shiro 的实现默认为 30 分钟的会话超时。也就是说,如果任何创建的 30 分钟或更长时间保持空闲状态(未使用,其中其 lastAccessedTime 未更新),则视为已过期,不再允许使用
-
设置自定义的超时时间
- 全局会话超时时间
[main] # 3,600,000 milliseconds = 1 hour securityManager.sessionManager.globalSessionTimeout = 3600000
- 单个会话超时时间
session = org.apache.shiro.session.mgt.SimpleSession.SimpleSession session.timeout=10000000
-
-
会话监听器
- Shiro支持SessionListener的概念,并允许我们在重要的事件发生时做出回应,可以实现SessionListener接口,或者继承SessionListenerAdapter
- 由于默认的SessionManager的sessionListeners属性是一个集合,因此可以配置一个或多个监听器
- 任何会话触发事件时,都会通知SessionListeners
[main] aSessionListener = com.foo.my.SessionListener anotherSessionListener = com.foo.my.OtherSessionListener securityManager.sessionManager.sessionListeners = $aSessionListener, $anotherSessionListener
-
会话存储
-
每当创建或更新会话时,其数据都需要保存到具体存储位置,以便应用程序稍后可以访问它
-
同样,当会话无效且使用时间较长时,需要将其从存储中删除,以便会话数据存储空间不会耗尽
-
SessionManager 实现将这些创建/读取/更新/删除 (CRUD)操作委托给内部组件 SessionDAO,该组件反映了数据访问对象 (DAO)设计模式
-
SessionDAO 的强大之处在于,您可以实现此接口来与您希望的任何数据存储进行通信。这意味着您的会话数据可以驻留在内存、文件系统、关系数据库或 NoSQL 数据存储中,或者您需要的任何其他位置。您可以控制持久性行为
- 系统提供了很多默认的实现,例如基于内存的SessionDao,缓存的CachingSessionDAO,也可以自定义
-
在Shiro.ini中配置
[main] sessionDAO = com.foo.my.SessionDAO securityManager.sessionManager.sessionDAO = $sessionDAO
-
上述代码分配的sessionDAO仅在使用Shiro本机会话管理器时有效,默认情况下,Web应用不使用本机会话管理器,而是保留Servlet容器的默认会话管理器,它不支持SessionDAO,如果在Web环境需要使用到SessionDAO,必须要先配置本机的Web会话管理器
[main] sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager securityManager.sessionManager = $sessionManager securityManager.sessionManager.sessionDAO = $sessionDAO
-
默认情况下,Shiro配置的SessionManager是仅在内存中存储,这在多数时候是不适用的,大多数都是使用第三方缓存EHcache等等,或者提供自己的SessionDAO实现
- 在Web应用程序基于Servlet的SessionManager并不存在此问题,这只是使用Shiro原生的SessionManager时的问题
-
配置EHCache的SessionDAO 缓存管理
-
默认情况下不启用 EHCache,但如果您不打算实现自己的 ,强烈建议您为 Shiro 的 SessionManager 启用 EHCache 支持
-
EHCache SessionDAO 会将会话存储在内存中,并在内存受限时支持溢出到磁盘。这对于生产应用程序来说是非常可取的,以确保您不会在运行时随机“丢失”会话
-
使用 EHCache 作为您的默认值
-
如果您不是在编写自定义 SessionDAO,请务必在 Shiro 配置中启用 EHCache。EHCache 除了会话之外,还可以缓存身份验证和授权数据
-
启用EHCache
// 依赖查找 https://shiro.apache.org/download.html <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>2.0.0</version> </dependency>
-
配置EHCache
[main] sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO securityManager.sessionManager.sessionDAO = $sessionDAOcacheManager = org.apache.shiro.cache.ehcache.EhCacheManager securityManager.cacheManager = $cacheManager
- 最后一行
securityManager.cacheManager = $cacheManager
为 Shiro 的所有需求配置了一个缓存管理器,此实例自动向下传播到SessionDAO,通过实现CacheManagerAware接口的EnterpriseCacheSessionDAO的特性 - 当SessionManager请求EnterpriseCacheSessionDAO对Session进行持久化时,它会使用EHCache缓存对Session进行保存
- Web应用程序
- 不要忘记,在使用Shiro本机SessionManager实现时,分配SessionDAO是一个特性
- 默认情况下Web应用程序使用基于Servet容器的SessionManager,它不支持SessionDAO。如果你想在web应用程序中使用基于ehcache的会话存储,就像上面描述那样配置一个本地web SessionManager。
- 最后一行
-
EHCache会话缓存配置
-
默认情况下EhCacheManager使用特定于 Shiro 的
ehcache.xml
文件来设置会话缓存区域和必要的设置,确保正确的存储和检索会话<cache name="shiro-activeSessionCache"maxElementsInMemory="10000"overflowToDisk="true"eternal="true"timeToLiveSeconds="0"timeToIdleSeconds="0"diskPersistent="true"diskExpiryThreadIntervalSeconds="600"/>
- 以上配置可以修改,但是下面两个配置不要更改,非常重要
overflowToDisk="true"
- 这可确保在进程内存不足时,会话不会丢失,并且可以序列化到磁盘eternal="true"
- 确保缓存条目(会话实例)永远不会过期或被缓存自动删除。这是必要的,因为 Shiro 会根据计划的过程进行自己的验证
- 以上配置可以修改,但是下面两个配置不要更改,非常重要
-
默认情况下,在EnterpriseCacheSessionDAO请求CacheManager使用缓存要求提供一个名为
shiro-activeSessionCache
的缓存名称,这个缓存名称可以在ehcache.xml中配置-
修改默认的缓存名称
[main] sessionDAO = org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO sessionDAO.activeSessionsCacheName = myname
-
-
-
-
-
-
自定义会话ID
-
Shiro 的SessionDAO的实现内部使用一个SessionIdGenerator生成器,组件在每次创建新会话的时候生成新的会话ID,分配给新创建的Session实例,然后通过SessionDAO保存
-
默认的SessionIdGenerator为JavaUuidSessionIdGenerator,他是基于Java的UUID生成,此实现试用所有的生产环境
-
如果默认的SessionIdGenerator无法满足,可以自己实现SessionIdGenerator接口,设置到SessionDAO实现中
[main] sessionIdGenerator = com.my.session.SessionIdGenerator securityManager.sessionManager.sessionDAO.sessionIdGenerator = $sessionIdGenerator
-
-
会话验证和调度
-
必须验证会话,以便从会话数据存储中删除任何无效(过期或停止)会话。这可确保数据存储不会随时间推移而填满永远不会再次使用的会话
-
出于性能原因,Sessions仅验证它们在访问时是否已停止或过期(即
subject.getSession()
)。这意味着,如果没有额外的定期验证,无用的条目项将开始填满会话数据存储,这是很糟糕的情况 -
因此,为了防止无用的会话数据存储,SessionManager支持SessionValidationScheduler的概念,负责定期验证会话,来确保他们在必要时被删除,防止堆积
-
默认的SessionValidationScheduler为
ExecutorServiceSessionValidationScheduler
,它使用JDK的ScheduledExecutorService 来控制验证的频率,在默认情况下,该实现每小时执行一次,我们可以通过指定一个新的的实例来指定不同的间隔(毫秒为单位)来更改验证发生的速率-
配置实例验证频率
[main] sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler # Default is 3,600,000 millis = 1 hour: sessionValidationScheduler.interval = 3600000 securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler
-
自定义实例
[main] sessionValidationScheduler = com.foo.my.SessionValidationScheduler securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler
-
-
禁用会话的验证
-
在某些情况下,您可能希望完全禁用会话验证,因为您设置了一个 Shiro 无法控制(Shiro之外)的进程来为您执行验证操作
-
例如,您可能正在使用企业缓存,并依靠缓存的“生存时间”设置来自动清除旧会话。或者,您可能已经设置了一个 cron 作业来自动清除自定义数据存储。在这些情况下,您可以关闭会话验证计划
[main] securityManager.sessionManager.sessionValidationSchedulerEnabled = false
- 该禁用只是禁用Shiro的定期验证,而不是禁用所有的验证,在会话存储中检索会话的时候,仍然会对其进行验证
- 如果您关闭 Shiro 的会话验证调度程序,则必须通过其他机制(cron 作业等)执行定期会话验证。这是保证不用的会话不会填满数据存储的唯一方法
-
-
删除无用的会话
-
定期会话验证的目的主要是删除任何无效(过期或停止)的会话,以确保它们不会填满会话数据存储。
-
默认情况下,每当 Shiro 检测到无效会话时,它都会尝试通过SessionDAO.delete(session)方法将其从基础会话数据存储中删除。对于大多数应用程序来说,这是确保会话数据存储空间不会耗尽的良好做法
-
但是,某些应用程序可能不希望 Shiro 自动删除会话
-
例如,如果应用程序提供了支持可查询数据存储的会话,则应用程序团队可能希望旧的或无效的会话在一段时间内可用
-
这将允许团队对数据存储运行查询,例如,查看用户在上周创建了多少会话,或用户会话的平均持续时间,或类似的报告类型查询,在这些情况下,您完全可以关闭无效会话删除功能
[main] securityManager.sessionManager.deleteInvalidSessions = false
-
但要小心!如果关闭此功能,则有责任确保会话数据存储不会耗尽其空间。您必须自行从数据存储中删除无效会话!
-
另请注意,即使您阻止 Shiro 删除无效会话,您仍然应该以某种方式启用会话验证 - 通过 Shiro 现有的验证机制或通过您自己提供的自定义机制
- 参考上面的“禁用会话验证”部分
-
警告
- 如果配置 Shiro,使其不删除无效会话,则有责任确保会话数据存储不会耗尽其空间。您必须自行从数据存储中删除无效会话! 另请注意,禁用会话删除与禁用会话验证调度不同。您几乎应该始终使用会话验证调度机制
- Shiro 直接支持或我们自己的机制
- 如果配置 Shiro,使其不删除无效会话,则有责任确保会话数据存储不会耗尽其空间。您必须自行从数据存储中删除无效会话! 另请注意,禁用会话删除与禁用会话验证调度不同。您几乎应该始终使用会话验证调度机制
-
-
-
5. Session会话和Subject(用户)状态
-
有状态的应用程序(允许Session会话)
- 默认情况下,Shiro的SecurityManager实现将使用Subject的Session作为存储Subject的身份(PrincipalCollection)和身份认证状态(Subject.isAuthenticated())的策略来供后续使用,这通常在Subject登录之后或者RememberMe服务发现Subject的身份的时候
- 任何服务请求,调用或消息都可以将会话ID与请求/调用的数据进行关联,这是Shiro将用户与入站请求相关联所需要的全部内容
- 在初始请求中找到的任何“RememberMe”标识都可以在首次访问时保留到会话中。这确保了主体的身份可以在请求之间保存,而无需在每个请求上对其进行反序列化和解密
- 例如,在 Web 应用程序中,如果会话中的身份已知,则无需在每个请求上读取加密的 RememberMe cookie。这可能是一个很好的性能增强
-
无状态的应用程序(无Session会话)
- 虽然默认策略对于大多数应用程序来说是可以的(并且通常是可取的),但在在无状态的应用程序中尝试,这是不可取的。
- 许多无状态体系结构要求请求之间不能存在持久状态,在这种情况下,不允许会话(会话本质上表示持久状态)
- 但是,此要求是以方便为代价的 - 不能跨请求保留Subject状态。这意味着具有此要求的应用程序必须确保每个请求都可以以其他方式表示Subject状态。
- 这几乎都是通过对处理的每个请求/调用/消息进行身份验证来实现的。例如,大多数无状态 Web 应用程序通常通过强制实施 HTTP Basic身份验证来支持此功能,从而允许浏览器代表最终用户对每个请求进行身份验证。远程处理或消息传递框架必须确保将Subject主体和凭据附加到每个调用或消息中,通常由框架代码执行。
-
禁用Subject状态和Session存储
-
禁用 Shiro 将 Subject的状态持久化到会话,在securityManager中配置
[main] securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
-
这样就阻止Shiro保存Subject状态,并且可以确保每个请求都需要进行身份验证
-
但是它不会完全禁用会话,如果自己代码显示调用
subject.getSession或subject.getSession(true)
则仍然会创建会话
-
-
混合方法实现
-
案例
- 当Web浏览器用户应该能使用会话,这可以提供诸多好处
- 当客户端是API接口或者第三方应用程序,则不应该使用会话,因为他们与软件交互可能是间歇性/或不稳定
- 或者某种类型的所有Subject或者某个位置的Subject需要保持会话状态,其他的不需要,那就可以使用两种混合的方式完成
-
如果要实现这种混合的方式,可以实现SessionStorageEvaluator接口,这个接口可以精准的控制哪些Subject的状态可能在Session中保存
public interface SessionStorageEvaluator {public boolean isSessionStorageEnabled(Subject subject);}
-
Subject验证
-
实现isSessionStorageEnabled(subject)接口方法时,总可以对subject的访问做出决策
public boolean isSessionStorageEnabled(Subject subject) {boolean enabled = false;if (WebUtils.isWeb(Subject)) {HttpServletRequest request = WebUtils.getHttpRequest(subject);} else {}return enabled;}
-
我们应该牢记这种类型的访问,这样可以确保可以通过特定短环境获取Subject的实现
-
ini配置
[main] sessionStorageEvaluator = com.mycompany.shiro.subject.mgt.MySessionStorageEvaluator securityManager.subjectDAO.sessionStorageEvaluator = $sessionStorageEvaluator
-
-
-
-
Web应用程序
-
通常,Web 应用程序希望简单地基于每个请求启用或禁用会话创建,而不管哪个Subject正在执行请求,这通常用于支持 REST 架构,例如
- 使用浏览器的人,可以创建和使用会话,用户操作体验更好
- 但远程 API 客户端使用 REST,根本不应该有会话,因为它们对每个请求都进行身份验证
-
为了支持这种混合/单请求的功能,一个noSessionCreateion过滤器被添加到Shiro的默认过滤池中,该过滤池为Web应用程序启动,此过滤器将阻止在请求期间创建新会话,以保证无状态体验。
-
我们通常在所有其他过滤器之前定义此过滤器,以确保永远不会使用会话 过滤器列表
[urls] /rest/** = noSessionCreation, authcBasic, ...
- 此过滤器允许对任何现有会话使用会话,但不允许在过滤请求期间创建新会话。也就是说,在请求或主题上调用以下四种方法中的任何一种,如果没有现有会话,都将自动触发DisabledSessionException
httpServletRequest.getSession()
httpServletRequest.getSession(true)
subject.getSession()
subject.getSession(true)
- 如果一个Subject在访问noSessionCreation-protected-URL之前已经有了会话,那么上面这四个方法正常工作
- 在任何时候,以下两个方法都是可以使用的
httpServletRequest.getSession(false)
subject.getSession(false)
- 此过滤器允许对任何现有会话使用会话,但不允许在过滤请求期间创建新会话。也就是说,在请求或主题上调用以下四种方法中的任何一种,如果没有现有会话,都将自动触发DisabledSessionException
-
6. Web环境的会话管理
-
在 Web 环境中,Shiro 的默认会话管理器 SessionManager 实现是 ServletContainerSessionManager。
-
这个非常简单的实现将所有会话管理职责(包括 Servlet 容器支持的会话集群)委托给运行时 Servlet 容器。
-
它本质上是 Shiro 的会话 API 到 servlet 容器的桥梁,几乎没有其他功能。
-
使用此默认值的一个好处是,使用现有 servlet 容器会话配置(超时、任何特定于容器的聚类机制等)的应用程序将按预期工作
-
这种默认值的缺点是,您与 servlet 容器的特定会话行为相关联 例如
- 如果要对会话进行群集,但使用 Jetty 进行测试,在生产中使用 Tomcat,则特定于容器的配置(或代码)将不可移植。
- 因为不同服务器对应的配置不一样
-
web.xml配置
- 会话超时
<session-config><session-timeout>30</session-timeout> </session-config>
-
-
如果想让这些会话在Servlet容器直接可进行移植,可以启动Shiro原生的会话管理 原生会话管理
-
"原生"表示Shiro自己的企业会话管理实现用于支持所有的会话,并完全绕过Servlet容器,有一套自己的实现方案
- 例如有一套Session的抽象方案,当是Web环境,装饰或代理HttpSession,内置就是用内置的Session,只需要是需要 实现Shiro内置的Session/SessionManager,内部会完成Session的操作
-
在Web应用程序中,默认使用ServletContainerSessionManager,要为 Web 应用程序启用本机会话管理,您需要配置一个支持 Web 的本机会话管理器来覆盖基于Servlet容器的会话管理器,该管理器为
DefaultWebSessionManager
本机会话管理[main] sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager securityManager.sessionManager = $sessionManager
-
-
会话Cookie
-
支持两个特定的配置属性
- sessionIdCookieEnabled(布尔值)
- sessionIdCookie,一个 Cookie 实例。
- 该sessionIdCookie实际上是一个Cookie模板,设置的Cookie属性,此模板将用于在运行时使用适当的会话 ID 值设置实际的 HTTP 标头
-
配置
-
DefaultWebSessionManager 的sessionIdCookie默认实例是 SimpleCookie。这个简单的实现允许对要在 http Cookie 上配置的所有相关属性进行 JavaBeans 样式的属性配置
[main] securityManager.sessionManager.sessionIdCookie.domain = foo.com
-
根据servlet规范,cookie的默认名称是JSESSIONID,此外,Shiro的cookie支持HttpOnly和SameSite标志。sessionIdCookie默认将Httponly设置为true,将SameSite设置为Lax以获得额外的安全性。Shiro的Cookie概念甚至在Servlet 2.4和2.5环境中也支持Httponly标志(而Servlet API仅在2.6或更高版本中本地支持它)。
-
-
禁用本机会话Cookie
-
如果不希望使用会话 Cookie,可以通过将sessionIdCookieEnabled属性配置为 false 来禁用它们
[main] securityManager.sessionManager.sessionIdCookieEnabled = false
-
-
-
RememberMe服务
-
身份验证Token为AuthenticationToken,而RememberMe为 org.apache.shiro.authc.RememberMeAuthenticationToken,继承了AuthenticationToken
-
RememberMeAuthenticationToken存在**
boolean isRememberMe();
**方法如果该方法返回true,则会跨会话记住用户身份信息 -
最常用的UsernamePasswordToken 已经实现了RememberMeAuthenticationToken,因此在UsernamePasswordToken就可以设置RememberMe标识
UsernamePasswordToken token = new UsernamePasswordToken(username, password); token.setRememberMe(true); SecurityUtils.getSubject().login(token);
-
基于表单登录
-
对于Web应用,表单登录的默认的过滤器为authc,实现为FormAuthenticationFilter,这将支持rememberMe参数
- 他要求默认的参数名为
rememberMe
,username
,password
# 配置过滤器 [main] authc.loginUrl = /login.jsp [urls] login.jsp = authc
<form>Username: <input type="text" name="username"/> <br/>Password: <input type="password" name="password"/><input type="checkbox" name="rememberMe" value="true"/>Remember Me? </form>
- 修改默认的参数名
[main] authc.loginUrl = /whatever.jsp authc.usernameParam = uname authc.passwordParam = pwd authc.rememberMeParam = reme
// 如果要覆盖默认的过滤器配置,直接创建新的Bean,名称覆盖之前的即可@Beanpublic Filter authc() {FormAuthenticationFilter filter = new FormAuthenticationFilter();filter.setUsernameParam("uname");filter.setPasswordParam("pwd");filter.setRememberMeParam("remember-me");return filter;}
- 他要求默认的参数名为
-
-
Cookie设置
-
可以通过设置默认的RememberMeManager的Cookie属性来配置各种RememberMe的Cookie功能,例如
[main] securityManager.rememberMeManager.cookie.name = foo securityManager.rememberMeManager.cookie.maxAge = blah
-
具体的实现为RememberMeManager: CookieRememberMeManager, Cookie: SimpleCookie
-
-
如果默认的RememberMeManager无法满足要求,可以进行自定义
[main] rememberMeManager = com.my.impl.RememberMeManager securityManager.rememberMeManager = $rememberMeManager
-
对于JSP,其他模板引擎的支持看 Apache Shiro Web
-
6. 缓存管理
1. 概念
- 性能在许多方面都至关重要,缓存是 Shiro 从一开始就内置的一流功能,并确保安全操作尽可能快
- 然而,虽然缓存作为一个概念,是 Shiro 的基本组成部分,但实现完整的缓存机制将超出核心能力范围
- 为此,Shiro 的缓存支持基本上是 一个抽象(包装器)API,它将“位于”底层之上 提供缓存机制(例如 Hazelcast、Ehcache、OSCache、 Terracotta、Coherence、GigaSpaces、JBossCache 等),这允许 Shiro 最终用户配置他们喜欢的任何缓存机制。
2. 缓存API
-
CacheManager
- 所有缓存的主要管理器组件,它返回Cache实例。
-
Cache
- 缓存的抽象,维护键/值对
-
CacheManagerAware
- 希望接收和使用 CacheManager 的组件实现,通过它能获取一个CacheManager实例
-
CacheManager返回一个Cache实例,并且各种Shiro组件根据需要使用这些Cache实例来缓存数据,任何实现CacheManagerAware的Shiro组件都会自动接收一个配置好的CacheManager实例,在CacheManager可以获取到缓存实例
-
Shiro的SecurityManager实现和所有AuthenticatingRealm和AuthorizingRealm实现都实现了CacheManagerAware,如果在SecurityManager中设置了CacheManager,那么也会实现CacheManagerAware的Realm中设置
[main] securityManager.realms = $myRealm1, $myRealm2, ..., $myRealmN cacheManager = my.implementation.of.CacheManager securityManager.cacheManager = $cacheManager
-
CacheManager的实现
-
Shiro 提供了许多开箱即用的CacheManager实现,您可能会发现这些比我们自己实现更有用
-
MemoryConstrainedCacheManager
-
适用于单 JVM 生产的实现 环境
-
它不是集群/分布式的,因此,如果您的应用程序跨越多个 JVM(例如,在多个 Web 服务器上运行的 Web 应用程序),并且您希望跨 JVM 访问缓存条目,则需要改用分布式缓存实现
-
MemoryConstrainedCacheManager管理MapCache实例,每个命名缓存一个MapCache实例。每个MapCache实例都由Shiro SoftHashMap支持,它可以根据应用程序的运行时内存约束/需求(通过利用JDK的SoftReference实例)自动调整自身大小
-
由于MemoryConstrainedCacheManager可以根据应用程序的内存配置文件自动调整自身大小,因此在单jvm生产应用程序和测试需求中使用它是安全的。但是,它没有更高级的功能,如缓存条目生存时间或过期时间设置。对于这些更高级的缓存管理功能,您可能希望使用其他更高级的CacheManager
HazelcastCacheManager
EhCacheManager
-
配置
[main] cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManagersecurityManager.cacheManager = $cacheManager
-
-
3. 授权缓存失效
- 请注意AuthorizingRealm有一个cleararcachedauthorizationinfo方法,子类可以调用该方法来为特定帐户提取缓存的authzInfo(认证信息)。如果相应的帐户的authz(认证信息)数据发生了变化,则通常由自定义逻辑调用它(以确保下一次认证检查将获取新数据)
7. Shiro Web支持
- 将Shiro集成到任何Web应用中最简单的方法就是添加ServletContextListener和Filter
1. web.xml配置
-
1.2以上版本配置
-
确保shiro.ini的位置
- 配置ShiroFilter的拦截路径
/WEB-INF/shiro.ini
- classpath: shiro.ini
<listener><listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <filter><filter-name>ShiroFilter</filter-name><filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter><filter-mapping><filter-name>ShiroFilter</filter-name><url-pattern>/*</url-pattern><dispatcher>REQUEST</dispatcher><dispatcher>FORWARD</dispatcher><dispatcher>INCLUDE</dispatcher><dispatcher>ERROR</dispatcher><dispatcher>ASYNC</dispatcher> </filter-mapping>
-
EnvironmentLoaderListener用来初始化WebEnvironment实例,其中包含Shiro需要操作的所有内容,包括SecurityManager,并让它可以在ServletContext中可以访问,如果需要WebEnvironment实例,可以调用WebUtils.getRequiredWebEnvironment(servletContext)
-
ShiroFilter这个过滤器将WebEnvironment对任何需要被过滤的请求完成安全处理
-
过滤器的映射定义确保所有的请求都被ShiroFilter拦截,这是最合适的配置
-
ShiroFilter需要在其他Filter之前进行过滤,确保他们也可以在Shiro的拦截器下进行工作
-
ShiroFilter标准的Servlet过滤器,根据Servlet规范,默认编码为(ISO-8859-1),需要要指定编码,使用Context-Type
-
-
自定义WebEnvironment
-
默认情况下,EnvironmentLoaderListener将会创建一个IniWebEnvironment的实例,基于ini配置,如果你喜欢,你可以使用自定义的WebEnvironment,然后在web.xml中配置
<context-param><param-name>shiroEnvironmentClass</param-name><param-value>com.foo.bar.shiro.MyWebEnvironment</param-value> </context-param>
-
-
自定义配置文件路径
-
默认的位置(按顺序)
/WEB-INF/shiro.ini
classpath:shiro.ini
-
修改位置
<context-param><param-name>shiroConfigLocations</param-name><param-value>classpath:shiro.ini</param-value> </context-param>
-
默认情况下
- 应该由 ServletContext.getResource 方法定义的规则解析。
- 例如param-value/WEB-INF/任意位置/shiro.ini
- 但是,您也可以使用 Shiro 的 ResourceUtils 类支持的适当资源前缀来指定特定的文件系统、类路径或 URL 位置,例如:
file:/home/foobar/myapp/shiro.ini
classpath:com/foo/bar/shiro.ini
url:http://confighost.mycompany.com/myapp/shiro.ini
- 应该由 ServletContext.getResource 方法定义的规则解析。
-
-
1.2以前配置
<filter><filter-name>ShiroFilter</filter-name><filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class><init-param><param-name>configPath</param-name><param-value>/WEB-INF/anotherFile.ini</param-value></init-param> </filter><filter-mapping><filter-name>ShiroFilter</filter-name><url-pattern>/*</url-pattern><dispatcher>REQUEST</dispatcher><dispatcher>FORWARD</dispatcher><dispatcher>INCLUDE</dispatcher><dispatcher>ERROR</dispatcher> </filter-mapping>
- 和1.2+区别
- 没有配置EnvironmentLoaderListener来加载WebEnvironment
- 自定义文件路径存在init-param中,1.2存在context-param中
- 配置文件必须以
classpath:
,file:
orurl:
为前缀
- 和1.2+区别
-
SpringMVC配置该过滤器
- SpringMVC如果没有使用Web.xml配置Filter,则需要实现WebApplicationInitializer接口完成Servlet,Filter和Listener的动态注册
- 如果没有注册该过滤器,则shiro失效
@Slf4j public class AppRoot implements WebApplicationInitializer {@Overridepublic void onStartup(ServletContext container) {AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();context.register(AppRoot.class);container.addListener(new ContextLoaderListener(context));FilterRegistration.Dynamic shiroFilter = container.addFilter("shiroFilterFactoryBean", DelegatingFilterProxy.class);shiroFilter.setInitParameter("targetFilterLifecycle", "true");shiroFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "/*");ServletRegistration.Dynamic dispatcher = container.addServlet("DispatcherServlet", new DispatcherServlet(context));dispatcher.setLoadOnStartup(1);dispatcher.addMapping("/");} }
-
SpringBoot配置该过滤器
2. 过滤器配置
-
支持的过滤器,自动可用的过滤器由DefaultFilter进行定义,枚举字段可以表示过滤器名称
Filter Name Class anon org.apache.shiro.web.filter.authc.AnonymousFilter authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter authcBearer org.apache.shiro.web.filter.authc.BearerHttpAuthenticationFilter invalidRequest org.apache.shiro.web.filter.InvalidRequestFilter logout org.apache.shiro.web.filter.authc.LogoutFilter noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter port org.apache.shiro.web.filter.authz.PortFilter rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter ssl org.apache.shiro.web.filter.authz.SslFilter user org.apache.shiro.web.filter.authc.UserFilter -
启用禁用配置
-
和其他过滤器链定义机制一样(web.xml,Shiro的ini文件)等方式,只需要将过滤器包含在过滤器链定义中即可启用过滤器,然后通过从链定义中删除过过滤器来禁用过滤器
-
在Shiro的1.2中,添加了一个新功能来启用或禁用过滤器,并不需要从过滤器链中删除,默认是启用的,因为静态的过滤器链定义是很不灵活的,也不太方便
-
Shiro通过OncePerRequestFilter抽象父类来完成这个功能,Shiro提供的过滤器都实现了该类,因此可以在不从过滤器链中删除这些Filter的情况下启用或者禁用过滤器,我们只需要实现OncePerRequestFilter就行
-
OncePerRequestFilter(及其所有子类)支持在所有请求之间以及基于每个请求启用/禁用
-
只需要将过滤器的
enabled
配置设置为false(默认都是为true) -
在ini文件中配置
[main] ssl.enabled = false[urls] /some/path = ssl, authc /another/path = ssl, roles[admin]
-
-
特定于请求的启用禁用
- OncePerRequestFilter实际上根据它的isEnabled(request, response)方法确定过滤器是启用还是禁用,此方法默认返回enabled属性的值,该属性通常用于启用/禁用该过滤器
- 如果您希望基于特定于请求的标准启用或禁用过滤器,您可以覆盖OncePerRequestFilter的isEnabled方法来执行更具体的检查
-
特定路径的启用/禁用
- shiro的PathMatchingFilter (OncePerRequestFilter的一个子类)能够根据被过滤的特定路径对配置做出处理,这意味着除了传入请求和响应之外,您还可以基于路径和特定于路径的配置启用或禁用过滤器
- 如果您需要能够对匹配路径和特定于路径的配置做出处理,以确定过滤器是否启用或禁用,您将重写PathMatchingFilter isEnabled(request, response, path, pathConfig)方法而不是重写OncePerRequestFilter isEnabled(request, response)方法
-
-
全局过滤器
-
从 Shiro 1.6 开始,添加了定义全局过滤器的功能
-
添加“全局过滤器”将为所有路由添加额外的过滤器,包括先前配置的过滤器链和未配置的路径。
-
默认情况下,全局过滤器包含
invalidRequest
过滤器。此过滤器阻止已知的恶意攻击,例如,可以自定义或禁用全局过滤器[main]# 关闭全局过滤器 filterChainResolver.globalFilters = null
[main] # 启用多个过滤器 filterChainResolver.globalFilters = invalidRequest, port
[main] # 单独配置过滤器的属性 invalidRequest.blockBackslash = true invalidRequest.blockSemicolon = true invalidRequest.blockNonAscii = true
-
-
Http传输安全过滤器
-
SslFilter(及其所有子类)支持启用/禁用 HTTP 严格传输安全性
[main] ssl.enabled = true ssl.hsts.enabled = true ssl.hsts.includeSubDomains = true[urls] /some/path = ssl, authc /another/path = ssl, roles[admin]
-
8. Shiro集成Spring
1. 独立的Spring项目,非WEB环境
-
依赖
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>2.0.0</version> </dependency> <dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>${spring.version}</version> </dependency>
-
配置类
@Configuration @Import({ShiroBeanConfiguration.class,ShiroConfiguration.class,ShiroAnnotationProcessorConfiguration.class}) public class CliAppConfig {... }
- 配置类解释
Configuration 类 描述 org.apache.shiro.spring.config.ShiroBeanConfiguration 配置 Shiro 的生命周期和事件 org.apache.shiro.spring.config.ShiroConfiguration 配置 Shiro Beans(SecurityManager、SessionManager 等) org.apache.shiro.spring.config.ShiroAnnotationProcessorConfiguration 启用 Shiro 的注释处理 -
配置Realm,用于访问用户信息和权限的访问DAO,一般使用AuthorizingRealm,它实现了认证的Realm以及授权的Realm
@Component public class UsernamePasswordRealm extends AuthorizingRealm {@Autowiredprivate UsersMapper usersMapper;// 认证@SneakyThrows@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {Users users = usersMapper.selectOne(new LambdaQueryWrapper<Users>().eq(Users::getUsername, token.getPrincipal()));if (users == null) {throw new UserPrincipalNotFoundException("用户名不存在");}ByteSource salt = ByteSource.Util.bytes(SALT.getBytes(StandardCharsets.UTF_8));String userPassword = new String((char[]) token.getCredentials());SimpleHash md5Hash = new SimpleHash(userPassword, salt, COUNT);String encodePassword = md5Hash.toHex();Object principal = token.getPrincipal();if (!users.getPassword().equals(encodePassword)) {throw new AuthenticationException("用户名密码错误");}return new SimpleAuthenticationInfo(principal, encodePassword, salt, "luck");}// 授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {Object principal = principals.getPrimaryPrincipal();Users users = usersMapper.selectOne(new LambdaQueryWrapper<Users>().eq(Users::getUsername, principal));Set<String> roles = Arrays.stream(users.getRoles().split(",")).filter(f -> f.startsWith("ROLE_")).collect(Collectors.toSet());Set<String> pers = Arrays.stream(users.getRoles().split(",")).filter(f -> !f.startsWith("ROLE_")).collect(Collectors.toSet());SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);info.setStringPermissions(pers);return info;} }
-
如果要使所有的SecurityUtils方法在所有情况下都工作,最简单的方式就是使SecurityManager的Bean成为静态单例
- 不要在web应用程序中这样做请参阅下面的web应用程序部分
@Autowired private SecurityManager securityManager;@PostConstructprivate void initStaticSecurityManager() {SecurityUtils.setSecurityManager(securityManager);}
- 接下来就可以使用获取用户信息
SecurityUtils.getSubject();
2. Web|SpringMVC项目
1. 过滤器配置权限控制
-
依赖
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>2.0.0</version> </dependency> <dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-web</artifactId><version>2.0.0</version> </dependency> <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>${spring.version}</version> </dependency>
-
配置
@Configuration @Import({ShiroBeanConfiguration.class,ShiroAnnotationProcessorConfiguration.class,ShiroWebConfiguration.class,ShiroWebFilterConfiguration.class,ShiroRequestMappingConfig.class}) public class ShiroConfig {}
-
配置类解释
Configuration 类 描述 org.apache.shiro.spring.config.ShiroBeanConfiguration 配置 Shiro 的生命周期和事件 org.apache.shiro.spring.config.ShiroAnnotationProcessorConfiguration 启用 Shiro 的注解处理 org.apache.shiro.spring.web.config.ShiroWebConfiguration 配置 Shiro Beans 以供 Web 使用(SecurityManager、SessionManager 等) org.apache.shiro.spring.web.config.ShiroWebFilterConfiguration 配置 Shiro 的 Web 过滤器 org.apache.shiro.spring.web.config.ShiroRequestMappingConfig 使用 Shiro 的实现配置 Spring,以确保在两个框架中处理相同的 UrlPathHelper
-
-
配置Realm
-
过滤器配置
- 注意: 必须动态注册ShiroFilter过滤器
- web.xml配置
- Servlet3.0无web.xml配置
- 配置路径映射
@Beanpublic ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();// 以“admin”角色登录的用户chainDefinition.addPathDefinition("/login/**", "anon");chainDefinition.addPathDefinition("/admin/**", "authc, roles[ROLE_ADMIN]");// 具有“document:read”权限的登录用户chainDefinition.addPathDefinition("/docs/**", "authc, perms[sys:user:select]");// 所有其他路径都需要登录用户chainDefinition.addPathDefinition("/**", "authc");return chainDefinition;}
- 注意: 必须动态注册ShiroFilter过滤器
-
案例
@ResponseBody@PostMapping("/login")public String doLogin(String username, String password) {Subject subject = SecurityUtils.getSubject();UsernamePasswordToken token = new UsernamePasswordToken(username, password);String message = "success";try {subject.login(token);log.info(message);} catch (Exception e) {message = "fail";log.info("{}:{}", message, e.getMessage());}return message;}
2. 注解开启权限控制
-
与过滤器配置有一点不同的是,使用注解进行权限控制,我们需要将过滤器配置设置为匿名访问,将访问控制全部交给注解来处理
@Controller public class AccountInfoController {@RequiresRoles("admin")@RequestMapping("/admin/config")public String adminConfig() {return "admini";} }
-
配置过滤器,将所有的进行拦截路径放行
@Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();chainDefinition.addPathDefinition("/**", "anon"); // chainDefinition.addPathDefinition("/**", "authcBasic[permissive]");return chainDefinition; }
3. 开启缓存
-
开启缓存
@Bean protected CacheManager cacheManager() {return new MemoryConstrainedCacheManager(); }
4. 支持的配置属性
钥匙 | 默认值 | 描述 |
---|---|---|
shiro.sessionManager.deleteInvalidSessions | true | 删除无效 会话存储中的会话 |
shiro.sessionManager.sessionIdCookieEnabled | true | 启用会话 ID 到 Cookie, 用于会话跟踪 |
shiro.sessionManager.sessionIdUrlRewritingEnabled | true | 使 会话 URL 重写支持 |
shiro.userNativeSessionManager | false | 如果启用,Shiro 将管理 HTTP 会话而不是容器 |
shiro.sessionManager.cookie.name | JSESSIONID | 会话 Cookie 名称 |
shiro.sessionManager.cookie.maxAge | -1 | 会话 Cookie 最长期限 |
shiro.sessionManager.cookie.domain | null | 会话 Cookie 域 |
shiro.sessionManager.cookie.路径 | null | 会话 Cookie 路径 |
shiro.sessionManager.cookie.secure | false | 会话 cookie 安全标志 |
shiro.rememberMeManager.cookie.name | rememberMe | RememberMe 饼干 名字 |
shiro.rememberMeManager.cookie.maxAge | 一年 | RememberMe 饼干最大值 年龄 |
shiro.rememberMeManager.cookie.domain | null | RememberMe cookie 域名 |
shiro.rememberMeManager.cookie.路径 | null | RememberMe Cookie 路径 |
shiro.rememberMeManager.cookie.secure | false | RememberMe 饼干 安全标志 |
shiro.loginUrl | /login.jsp | 未经身份验证的用户时使用的登录 URL 重定向至登录页面 |
shiro.successUrl | / | 用户登录后的默认登录页面(如果 在当前会话中找不到替代方案) |
shiro.unauthorizedUrl | null | 将用户重定向到的页面(如果是) 未经授权 (403页) |
9. Shiro集成SpringBoot
-
依赖
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring-boot-web-starter</artifactId><version>2.0.0</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.7.0</version></dependency>
-
配置
// 非注解版本@Beanpublic ShiroFilterChainDefinition shiroFilterChainDefinition() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();// 以“admin”角色登录的用户chainDefinition.addPathDefinition("/login/**", "anon");chainDefinition.addPathDefinition("/login.html", "anon");chainDefinition.addPathDefinition("/admin/**", "authc, roles[ROLE_ADMIN]");// 具有“document:read”权限的登录用户chainDefinition.addPathDefinition("/docs/**", "authc, perms[sys:user:select]");// 所有其他路径都需要登录用户chainDefinition.addPathDefinition("/**", "authc");return chainDefinition;}/*** 必须,否则会出现问题,因为该配置注册Authorizer的时候标注了@ConditionalOnMissingBean* 但是我们自定义的授权Realm实现了该Authorizer接口,导致不会自动注入这个Bean,会无法进行工作* {@link org.apache.shiro.spring.config.web.autoconfigure.ShiroWebAutoConfiguration#authorizer()}** @return*/@Beanpublic Authorizer authorizer() {return new ModularRealmAuthorizer();}// 注解版本,全部交给注解控制// @Bean// @Primarypublic ShiroFilterChainDefinition shiroFilterChainDefinitionAnno() {DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();// 全部交给注解控制chainDefinition.addPathDefinition("/**", "anon");return chainDefinition;}
- 配置Realm
- 注解权限控制
- 缓存配置
- 案例
@ResponseBody@PostMapping("/login")public String doLogin(String username, String password) {Subject subject = SecurityUtils.getSubject();UsernamePasswordToken token = new UsernamePasswordToken(username, password);String message = "success";try {subject.login(token);log.info(message);} catch (Exception e) {message = "fail";log.info("{}:{}", message, e.getMessage());}return message;}@RequiresRoles("ROLE_ADMIN")// @RequiresPermissions("sys:user:delete")@ResponseBody@GetMapping("/data")public String data() {return "data";}@ResponseBody@GetMapping("/logout")public String logout() {Subject subject = SecurityUtils.getSubject();subject.logout();return "success logout";}