前进之路 发表于 2025-1-3 09:04:33

安全框架:Apache Shiro

前言

Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。使用Shiro易于明确的API,您可以快速轻松地保护任何应用程序 - 从最小的移动应用程序到最大的Web和企业应用程序。
下图显示了 Shiro 的精力会集在那里:
https://i-blog.csdnimg.cn/direct/57f1cd44724643d292b607e9030ef3bc.png#pic_center


[*]Authentication(身份验证):偶然称为“登录”,这是证明用户是他们所声称的身份的举动。
[*]Authorization(授权):访问控制的过程,即确定“谁”可以访问“什么”。
[*]Session Management(会话管理):管理特定于用户的会话,纵然在非 Web 或 EJB 应用程序中也是如此。
[*]Cryptography(加密):使用加密算法确保数据安全,同时仍然易于使用。
此外,还有一些其他功能可以在差异的应用程序情况中支持和加强这些标题,特别是:


[*]Web 支持:Shiro的 Web 支持 API 有助于轻松保护 Web 应用程序。
[*]缓存:缓存是Apache Shiro的API中的第一层公民,用于确保安全操作保持快速和高效。
[*]并发:Apache Shiro 通过其并发功能支持多线程应用程序。
[*]测试:测试支持可资助您编写单位测试和集成测试,并确保您的代码按预期得到保护。
[*]“运行方式”:答应用户代入其他用户的身份(如果答应)的功能,偶然在管理方案中很有用。
[*]“Remember Me(记着我)”:记着用户跨会话的身份,因此他们只需要在必须登录时登录。
   Shiro 试图在全部应用程序情况中实现这些目标 - 从最简单的命令行应用程序到最大的企业应用程序,而无需强制依靠其他第三方框架、容器或应用程序服务器。固然,该项目标目标是尽可能地集成到这些情况中,但它可以在任何情况中开箱即用。
您的第一个 Apache Shiro 应用程序

我们创建一个Maven项目,引入依靠配置,示例代码如下(最新版2.0.2需要JDK11):
      <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.13.0</version>
      </dependency>
SpringBoot依靠配置如下:
                <dependency>
                  <groupId>org.apache.shiro</groupId>
                  <artifactId>shiro-spring-boot-web-starter</artifactId>
                  <version>1.13.0</version>
                </dependency>
要在应用程序中启用Shiro,起首要相识的是,Shiro中的险些全部内容都与称为SecurityManager的中心/核心组件相干。SecurityManager是应用程序的Shiro情况的核心
虽然我们可以直接实例化SecurityManager类,但Shiro的SecurityManager实现有充足的配置选项和内部组件,这使得在Java源代码中这样做很痛苦——使用灵活的基于文本的配置格式来配置SecurityManager会容易得多。
为此,Shiro通过基于文本的INI配置提供了默认的“公分母”办理方案。如今,人们已经厌倦了使用巨大的XML文件,而INI易于阅读、使用简单,并且只需要很少的依靠项。,INI可以有效地用于配置像SecurityManager这样的简单对象图。
在src/main/resources目录下创建一个shiro.ini文件,内容如下:
# -----------------------------------------------------------------------------
# 用户和他们(可选)分配的角色
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------

root = admin, admin
guest = guest, guest

# -----------------------------------------------------------------------------
# 已分配权限的角色
# roleName = perm1, perm2, ..., permN
# -----------------------------------------------------------------------------

admin = *
guest = user:query

此配置根本上设置了一小部分静态用户帐户,对于我们的第一个应用程序来说已经充足了。
然后创建工厂获取实例,进行登录操作,示例代码如下:
public class Test {
    public static void main(String[] args) {
      // 初始化Security工厂
      IniSecurityManagerFactory iniSecurityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
      // 解析INI文件,获取认证管理器实例
      SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
      // 将认证管理器放入SecurityUtils
      SecurityUtils.setSecurityManager(securityManager);
      // 使用实例,它只是表示“当前正在与软件交互的事物”
      Subject subject = SecurityUtils.getSubject();
      // 身份验证
      UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("guest", "12345");
      try {
            subject.login(usernamePasswordToken);
      } catch (UnknownAccountException uae) {
            // 用户名不在系统中,给他们显示错误信息?
            System.out.println("用户不存在");
      } catch (IncorrectCredentialsException ice) {
            // 密码不匹配,再试一次?
            System.out.println("密码错误");
      } catch (LockedAccountException lae) {
            // 该用户名的/帐户被锁定-无法登录。
            System.out.println("账户被锁定");
      } catch (AuthenticationException ae) {
            //意外情况-错误?
            System.out.println("意外错误");
      }

    }
}
差异的catch捕获的异常差异,上述示例的guest的密码错误会打印对应日志。
当我们登录乐成后,我们可以对当前账户进行一些校验操作,比如获取当前登录的信息,示例代码如下:
Object principal = subject.getPrincipal();
/** Output:
*root
*/
还可以测试它们是否具有特定的脚色:
boolean hasRole = subject.hasRole("president");
System.out.println(hasRole);
/** Output:
*false
*/
我们还可以查看他们是否有权对某种类型的实体执行操作:
boolean permitted = subject.isPermitted("user:query
");
System.out.println(permitted);
/** Output:
*true
*/
最后,当用户使用完应用程序后,他们可以注销:
subject.logout();
Multiple Parts(多个部分)

通配符权限支持多个级别或部分的概念。比方,您可以通过授予用户权限来重新构建前面的简单示例:
user:query
此示例中第一部分是正在操作的域 (user),第二部分是正在执行的操作 (query),冒号是一个特殊字符,用于分隔权限字符串中的下一部分。
每个部分可以包罗多个值。
user:query
,user:insert,user:delete 如果要向用户授予特定部分中的全部值,该怎么办?我们可以根据通配符执行此操作。
user:*
*:query
INI配置

INI 根本上是一种文本配置,由由唯一命名的部分构造的键/值对组成。键仅对每个部分是唯一的,而不是在整个配置中唯一的(与 JDK 属性差异)。但是,每个部分都可以被视为单个 Properties 定义。
以下是 Shiro 明确的部分示例:
# =======================
# Shiro INI configuration
# =======================


# 对象和它们的属性在这里定义,例如securityManager,Realms等等,或者需要构建SecurityManager
myRealm = com.example.study.MyRealm

# 用户和他们(可选)分配的角色 如:username = password, role1, role2, ..., roleN
admin = admin

# 已分配权限的角色,例如:roleName = perm1, perm2, ..., permN
admin = user:*

# 用于基于url的安全

部分

该 部分用于配置应用程序的 SecurityManager 实例及其任何依靠项,比方 Realms。

myRealm = com.example.shiro.MyRealm
# 定义参数
myRealm.username = hello
示例代码如下:
public class MyRealm extends AuthenticatingRealm {
    private String username;

    public void setUsername(String username) {
      this.username = username;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      System.out.println(username);
      /** Output:
         *hello
         */
      return null;
    }
}
如果您需要设置的值不是基元,而是另一个对象,该怎么办?您可以使用美元符号 ($) 来引用从前定义的实例。比方:

sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher
myRealm = com.example.shiro.MyRealm
myRealm.sha256CredentialsMatcher = $sha256Matcher
示例代码如下:
public class MyRealm extends AuthenticatingRealm {
    private Sha256CredentialsMatcher sha256CredentialsMatcher;

    public void setSha256CredentialsMatcher(Sha256CredentialsMatcher sha256CredentialsMatcher) {
      this.sha256CredentialsMatcher = sha256CredentialsMatcher;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      System.out.println(sha256CredentialsMatcher);
      /** Output:
         *org.apache.shiro.authc.credential.Sha256CredentialsMatcher@f6c48ac
         */
      return null;
    }
}
使用 INI 配置对象实例(如 SecurityManager 或其任何依靠项)听起来像是一件困难的事情,因为我们只能使用名称/值对。
我们经常喜欢将这种方法称为“贫民”依靠注入,虽然不如成熟的 Spring/Guice/JBoss XML 文件强大,但您会发现它可以完成很多工作,而不会太复杂。固然,其他配置机制也可用,但它们不是使用 Shiro 所必须的。
部分

部分答应您定义一组静态用户帐户。 部分中的每一行都必须符合以下格式:
   username = password , 脚色名称 1, 脚色名称 2, …, 脚色名称 N
这在用户帐户数量非常少或不需要在运行时动态创建用户帐户的情况中非常有用。下面是一个示例:

root = admin, admin
guest = guest, guest


[*] 等号左侧的值是用户名
[*] 等号右侧的第一个值是用户的密码。需要密码。
[*] 密码后的任何逗号分隔值都是分配给该用户的脚色的名称。脚色名称是可选的。
Shiro 1 的算法(比方 md5、SHA1、SHA256 等)长期以来被以为不安全,不再受支持。 既没有直接的迁移路径,也没有向后兼容性。
我们先天生一个密钥,示例代码如下:
import org.apache.shiro.authc.credential.DefaultPasswordService;
import org.apache.shiro.authc.credential.PasswordService;

public class Test {
    public static String encriptPassword(String password) {
      PasswordService service = new DefaultPasswordService();
      return service.encryptPassword(password);
    }

    public static void main(String[] args) {
      System.out.println(encriptPassword("guest"));
      /** Output:
         *$shiro1$SHA-256$500000$69Zvt22yzkGtnTcqQ6UrvQ==$6k7YPH02jl130ZKzEugbafHxGLAcJB/ohBTaZcMibpA=
         */
    }
}
在部分里配置加密规则,将加密后的密码放入ini配置文件中,示例代码如下:

# 设置加密
passwordMatcher=org.apache.shiro.authc.credential.PasswordMatcher
passwordService=org.apache.shiro.authc.credential.DefaultPasswordService
passwordMatcher.passwordService=$passwordService
iniRealm.credentialsMatcher=$passwordMatcher
securityManager.realms=$iniRealm
# -----------------------------------------------------------------------------
# 用户和他们(可选)分配的角色
# username = password, role1, role2, ..., roleN
# -----------------------------------------------------------------------------

root = admin, admin
guest = $shiro1$SHA-256$500000$69Zvt22yzkGtnTcqQ6UrvQ==$6k7YPH02jl130ZKzEugbafHxGLAcJB/ohBTaZcMibpA=, guest
然后执行登录操作即可。
从 Shiro 2.0 开始,该部分不能包罗纯文本密码。 您可以使用密钥派生函数对它们进行加密。 Shiro 提供了 bcrypt 和 argon2 的实现。 如果不确定,请使用 argon2 派生的密码。

# Shiro2CryptFormat


# user1 = sha256-hashed-hex-encoded password, role1, role2, ...
user1 = "$shiro2$argon2id$v=19$t=1,m=65536,p=4$H5z81Jpr4ntZr3MVtbOUBw$fJDgZCLZjMC6A2HhnSpxULMmvVdW3su+/GCU3YbxfFQ", role1, role2, ...
一旦指定了派生的文本密码值,就必须告诉Shiro这些值是加密的。您可以通过配置部分中隐式创建的方法来使用与您指定的散列算法相对应的得当实现。
部分

部分答应您将权限与 部分中定义的脚色相干联。 部分中的每一行都必须按以下格式定义脚色到权限的键/值映射:
   rolename = permissionDefinition1, permissionDefinition2, …, permissionDefinitionN
同样,这在脚色数量较少或不需要在运行时动态创建脚色的情况中非常有用。下面是一个示例:

admin = user:*
guest = user:query

部分

部分能够为应用程序中的任何匹配 URL 路径定义暂时过滤器链!这比你通常定义过滤器链的方式要灵活、强大和简洁得多,该部分中每行的格式如下:

URL Ant Path Expression(URL Ant路径表达式) = Path Specific Filter Chain(路径特定的过滤器链)
URL Ant路径表达式是一种用于匹配URL路径的模式表达式。支持使用?(匹配一个字符)、*(匹配零个或多个字符(不包括路径分隔符))、**(匹配零个或多个目录层级(包括路径分隔符))等符号来匹配路径中的特定部分。
比方:

# 匹配 /index.html 不匹配 /home/index.html
/?ndex.html = anon
# 匹配 /user/login 不匹配 /user 、/user/a/b
/user/* = anon
# 匹配 /file、/file/a/b
/file/** = authc
路径特定的过滤器链是指针对特定URL路径配置的过滤器链。Shiro答应你为差异的URL路径定义差异的过滤器链,以执行差异的安全检查和操作。
等号 = 右侧的标记是要为匹配该路径的请求执行的筛选条件的逗号分隔列表。它必须与以下格式匹配:
# filterN是在section和中定义的过滤器bean的名称。
# 是一个可选的带括号的字符串,它对特定路径的特定过滤器有意义(per-filter, path-specific configuration!)。如果过滤器不需要对URL路径进行特定配置,则可以丢弃括号
filter1, filter2, ..., filterN
因为 filter token 定义了链(又名 List),所以请记着顺序很重要!按照您盼望请求流经链的顺序定义逗号分隔的列表。
在Shiro中,两者结合起来使用,以实现基于URL路径的访问控制。比方,在Shiro的INI配置文件中,你可以这样配置:

# 允许匿名访问
/login = anon
# 表示`/admin`目录下的所有路径都需要身份验证,并且用户需要拥有**admin**角色。
/admin/** = authc, roles
# 表示/user目录下的所有路径都需要身份验证,并且用户需要拥有user:view权限。
/user/** = authc, perms
默认过滤器

当运行 Web 应用程序时,Shiro 将创建一些有用的默认实例并自动使它们在该部分中可用。您可以像配置任何其他 bean 一样配置它们,并在链定义中引用它们。比方:

...
# 注意,我们没有为FormAuthenticationFilter ('authc')定义类——它已经被实例化并且可用:
authc.loginUrl = /login.jsp
...


...
# 确保最终用户已经过身份验证。如果没有,重定向到'authc。loginUrl”上面,
# 验证成功后,将它们重定向到原始帐户页面
# 我们试图查看:
/account/** = authc
自动可用的默认过滤器实例由DefaultFilter罗列定义,罗列的字段是可用于配置的名称。
名称类形貌anonAnonymousFilter匿名过滤器,表现不需要登录即可访问的资源。这通常用于过滤静态资源,如图片、CSS、JavaScript等。authcFormAuthenticationFilter身份验证过滤器,需要用户进行身份验证(登录)才能访问资源。authcBasicBasicHttpAuthenticationFilterHTTP Basic身份验证过滤器,使用HTTP Basic协议进行身份验证。authcBearerBearerHttpAuthenticationFilterBearer Token身份验证过滤器,通常用于OAuth 2.0的Bearer Token身份验证。用户需要在请求中携带一个有效的Bearer Token。logoutLogoutFilter登出过滤器,用于处理用户的登出请求。执行登出操作后,用户通常会被重定向到登录页面或指定的其他页面。noSessionCreationNoSessionCreationFilter不创建会话过滤器,用于阻止Shiro为请求创建会话。permsPermissionsAuthorizationFilter权限授权过滤器,用于验证用户是否拥有特定的权限。portPortFilter端口过滤器,用于限定请求只能通过指定的端口访问。restHttpMethodPermissionFilterREST风格的方法权限过滤器,答应为差异的HTTP方法(如GET、POST、PUT、DELETE等)配置差异的权限。rolesRolesAuthorizationFilter脚色授权过滤器,用于验证用户是否拥有特定的脚色。sslSslFilterSSL过滤器,用于强制某些请求必须通过SSL(HTTPS)毗连进行。userUserFilter用户拦截器。如果用户未登录且未选择RememberMe,则请求可能会被重定向到登录页面(取决于配置)。invalidRequestInvalidRequestFilter无效请求过滤器,用于处理无效的请求,如请求参数不完备或格式不精确等。这通常会导致一个错误相应或重定向到错误页面。 常规启用/禁用

通常,通过将其属性设置为 true 或 false 来启用或禁用全部请求的过滤器。默认设置true是因为如果大多数过滤器在链中配置,则它们本身需要执行。
比方:

...
# 在测试时禁用Shiro的默认‘ssl’过滤器:
ssl.enabled = false


/some/path = ssl, authc
/another/path = ssl, roles
你也可以调用实现类的方法实现此操作,示例代码如下:
SslFilter sslFilter = new SslFilter();
sslFilter.setEnabled(false);
密码学

Shiro 专注于密码学的两个核心元素:使用公钥或私钥加密电子邮件等数据的密码,以及不可逆地加密密码等数据的哈希值(又名消息摘要)。
Shiro提供默认哈希(在JDK中称为消息摘要)的开箱即用实现,如:MD5、SHA-256、SHA-386、SHA-512等。比如:new Sha256Hash(data)。
Md5Hash md5Hash = new Md5Hash("123456", "admin");
Sha256Hash sha256Hash = new Sha256Hash("123456", "root", 2);
他们有三个参数:


[*]source:计算哈希的原始数据,可能是字符串、字节数组大概任何可以转换为字节流的对象。它是你盼望进行哈希处理的数据,比如密码、文件内容、消息等。
[*]salt:盐的作用是将原本雷同的输入值通过添加差异的盐值变得差异,从而防止差异的输入数据产生雷同的哈希值(即避免哈希辩论或彩虹表攻击)。盐通常是随机天生的一段额外数据。
[*]hashIterations:用于指定哈希算法重复执行的次数,通常是为了增长计算的复杂性和进步安全性。这意味着,纵然原始输入数据和盐值保持不变,增长哈希迭代的次数也能使得终极的哈希值更加安全,尤其是在面临暴力破解或彩虹表攻击时。
Shiro Hash 实例可以通过其 toHex() 和toBase64() 方法自动提供哈希数据的 Hex 和 Base-64 编码。因此,如今您无需弄清楚如何自己精确编码数据。
完备示例代码如下:
    public static void main(String[] args) {
      // md5方式
      Md5Hash md5Hash = new Md5Hash("123456", "admin");
      System.out.println("md5-tohex:"+md5Hash.toHex());
      System.out.println("md5-toBase64:"+md5Hash.toBase64());
      // sha256Hash方式
      Sha256Hash sha256Hash = new Sha256Hash("123456", "root");
      System.out.println("sha256Hash-tohex:"+sha256Hash.toHex());
      System.out.println("sha256Hash-toBase64:"+sha256Hash.toBase64());
      Sha256Hash sha256Hash2 = new Sha256Hash("123456", "root", 2);
      System.out.println("sha256Hash2-tohex:"+sha256Hash2.toHex());
      System.out.println("sha256Hash2-toBase64:"+sha256Hash2.toBase64());
      /** Output:
         *md5-tohex:a66abb5684c45962d887564f08346e8d
         *md5-toBase64:pmq7VoTEWWLYh1ZPCDRujQ==
         *sha256Hash-tohex:28f4c77c534d5358329b61b326c995cd1743e2e37dd13949ace9c9b816de1fa9
         *sha256Hash-toBase64:KPTHfFNNU1gym2GzJsmVzRdD4uN90TlJrOnJuBbeH6k=
         *sha256Hash2-tohex:16c273b943b67a662cacf11b777729103eb235d0aa1d0daf3ce8953001651cf3
         *sha256Hash2-toBase64:FsJzuUO2emYsrPEbd3cpED6yNdCqHQ2vPOiVMAFlHPM=
         */
    }
会话管理



[*]使用会话
与Shiro中的险些全部其他内容一样,您可以通过Subject交互来获取:
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
session.setAttribute("userId", "test");
在开发框架代码时,可以使用subject.getSession(false)来确保不会不必要地创建会话。
一旦你获得了一个Subject,你就可以用它做很多事情,比如设置或检索属性,设置超时,等等。调用getSession()方法可以在任何应用程序中工作,乃至是非web应用程序。


[*]会话超时
默认情况下,Shiro的实现默以为30分钟的会话超时。也就是说,如果创建的任何数据在 30 分钟或更长时间内保持空闲状态,则以为该数据已过期,不答应再使用。
https://i-blog.csdnimg.cn/direct/ffa95b4ef221418cbcc998698d823069.png#pic_center
您可以设置全部会话的默认超时值,该值以毫秒为单位(而不是秒)为单位。配置如下:

sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
# 如果需要,可以在这里配置属性(如会话超时)该值以毫秒为单位(而不是秒)为单位。
securityManager.sessionManager.globalSessionTimeout = 1800000


[*]会话侦听器
Shiro答应您在重要的会话事件发生时做出反应。你可以实现 SessionListener 接口(或扩展方便的 SessionListenerAdapter)并相应地对 session 操作做出反应。示例代码如下:
public class MySessionListener implements SessionListener {
    @Override
    public void onStart(Session session) {
      System.out.println("进来了");
    }

    @Override
    public void onStop(Session session) {
      System.out.println("结束了");
    }

    @Override
    public void onExpiration(Session session) {
      System.out.println("过期了");
    }
}
配置监听类:

anotherSessionListener = org.example.MySessionListener
securityManager.sessionManager.sessionListeners = $anotherSessionListener
执行结果如图:
https://i-blog.csdnimg.cn/direct/c7e618e2db90464d82257df03bb73aa9.png#pic_center


[*]会话存储
每当创建或更新会话时,其数据都需要保存到存储位置,以便应用程序稍后可以访问它。同样,当会话无效且使用时间较长时,需要将其从存储中删除,以免耗尽会话数据存储空间。这些实现将这些 CRUD 操作委托给内部组件 SessionDAO,它反映了数据访问对象 计划模式。
默认情况下,Web 应用程序不使用本机会话管理器,而是保留 Servlet 容器的默认会话管理器,该管理器不支持 SessionDAO。如果您想在基于 Web 的应用程序中启用 SessionDAO 以进行自定义会话存储或会话集群,则必须起首配置本机 Web 会话管理器。示例代码如下:
public class MyCustomSessionDAO implements SessionDAO {
    private final Map<Serializable, Session> sessions = new HashMap<>();
    @Override
    public Serializable create(Session session) {
      System.out.println("创建");
      String str = UUID.randomUUID().toString();
      ((SimpleSession)session).setId(str); // sessionId赋值
      sessions.put(str, session);
      return str;
    }

    @Override
    public Session readSession(Serializable serializable) throws UnknownSessionException {
      System.out.println("查询");
      return sessions.get(serializable);
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
      System.out.println("修改");
    }

    @Override
    public void delete(Session session) {
      System.out.println("删除");
    }

    @Override
    public Collection<Session> getActiveSessions() {
      return List.of();
    }
}
配置监听类:

sessionDAO = org.example.MyCustomSessionDAO
securityManager.sessionManager.sessionDAO = $sessionDAO
执行结果如图:
https://i-blog.csdnimg.cn/direct/945d1b6402ac4e7e95bde6e2af909b92.png#pic_center


[*]自定义会话 ID
Shiro的实现使用内部SessionIdGenerator组件在每次创建新会话时天生一个新的Session ID。将其分配给新创建的实例,然后通过SessionDAO保存ID。
默认值为 JavaUuidSessionIdGenerator,它基于 Java UUID 天生 ID。此实现实用于全部生产情况。
如果这不能满足你的需求,你可以在 Shiro 的实例上实现接口并配置实现。示例代码如下:
public class CustomSessionManager implements SessionIdGenerator {
    @Override
    public Serializable generateId(Session session) {
      return "custom_"+ UUID.randomUUID();
    }
}
配置监听类:

sessionIdGenerator = org.example.CustomSessionManager
securityManager.sessionManager.sessionDAO.sessionIdGenerator = $sessionIdGenerator
执行结果如图:
https://i-blog.csdnimg.cn/direct/30c0a55abd5843168a7db1afec44245d.png#pic_center


[*]会话验证和调度
必须验证会话,以便可以从会话数据存储中删除任何无效(过期或已停止)的会话。这可确保数据存储不会随着时间的推移而填满永远不会再次使用的会话。
在全部情况中默承认用的是 ExecutorServiceSessionValidationScheduler,它使用 JDK ScheduledExecutorService 来控制验证的频率。此实行将每小时执行一次验证。您可以通过指定的新实例并指定差异的隔断(以毫秒为单位)来更改验证的发生速率:
sessionValidationScheduler = org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler
# Default is 3,600,000 millis = 1 hour:
sessionValidationScheduler.interval = 3600000
如果您盼望提供自定义实现,您可以将其指定为 default 实例的属性。示例代码如下:
public class MySessionValidationScheduler implements SessionValidationScheduler {
    @Override
    public boolean isEnabled() {
      return false;
    }

    @Override
    public void enableSessionValidation() {

    }

    @Override
    public void disableSessionValidation() {

    }
}
配置监听类:

sessionValidationScheduler = org.example.MySessionValidationScheduler
securityManager.sessionManager.sessionValidationScheduler = $sessionValidationScheduler
在某些情况下,你可能盼望完全禁用会话验证,因为你已经设置了一个不受Shiro控制的流程来为你执行验证。

securityManager.sessionManager.sessionValidationSchedulerEnabled = false
   如果你关闭了 Shiro 的会话验证调度器,你必须通过其他机制(cron job 等)执行定期会话验证。这是保证 Session不会填满数据存储的唯一方法。
但是,某些应用程序可能不盼望 Shiro 自动删除会话。但是,某些应用程序可能不盼望 Shiro 自动删除会话。

securityManager.sessionManager.deleteInvalidSessions = false
Remember Me

如果 Shiro 实现了 org.apache.shiro.authc.RememberMeAuthenticationToken 接口,则 Shiro 将执行 'rememberMe' 服务。
public interface RememberMeAuthenticationToken extends AuthenticationToken {
    boolean isRememberMe();
}
以编程方式使用 rememberMe,您可以将值设置为在支持此配置的类上。比方:
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("guest", "guest");
usernamePasswordToken.setRememberMe(true);
整合SpringBoot

经过前面的内容解说,掌握了Shiro的根本知识。下面将整合SpringBoot框架,模拟Web操作(前后不分离项目)。
pom依靠配置文件如下:
    <properties>
      <java.version>8</java.version>
    </properties>
    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.7.18</version>
      <relativePath/> <!-- lookup parent from repository -->
    </parent>
        <dependencies>
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
      <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.13.0</version>
      </dependency>
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
      </dependency>
      <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.9</version>
      </dependency>
      <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
      </dependency>
    </dependencies>
   注:并没有使用最新的Shiro2.x.x版本,要求是需要JDK11以上,但是再与SpringBoot3.x.x整合过程中碰到很多标题,所以建议使用:Shiro1.x.x+SpringBoot2.x.x+JDK1.8。
然后创建Shiro需要的根本配置信息,示例代码如下:
@Configuration
public class ShiroConfig {
    /**
   * 创建Shiro Web应用的整体安全管理
   */
    @Bean
    public DefaultWebSecurityManager securityManager(){
      DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
      // 可以添加其他配置,如缓存管理器、会话管理器等
      return defaultWebSecurityManager;
    }
}
一切就绪后,运行一下没标题,最根本的SpringBoot+Shiro整合已经完成。
https://i-blog.csdnimg.cn/direct/64cb4caddbb7486cac7c5126ccce9817.png#pic_center
登录

框架搭建完成后,作为权限认证和授权最重要的一部分,也是项目访问的入口,我们先从登录开始解说。
根据.ini文件的配置,所需要的表有三个,用户表、脚色表、权限表,然后用户表和脚色表关联中心表,脚色表和权限表管理中心表,一共有5个表:
-- 用户表
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '密码',
`age` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 角色表
CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(64) DEFAULT NULL,
`description` varchar(100) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`),
KEY `role_id_IDX` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 权限表
CREATE TABLE `permission` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 用户-角色关联表
CREATE TABLE `user_role` (
`id` int NOT NULL,
`user_id` int NOT NULL,
`role_id` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 角色-权限管理表
CREATE TABLE `role_permission` (
`id` int NOT NULL AUTO_INCREMENT,
`role_id` int NOT NULL,
`permission_id` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
配置默认超管数据:
-- 插入用户(生成密码通过Sha256Hash.tobase64,密码123456)
INSERT INTO `user` (id, username, password, age) VALUES(1, 'admin', 'rA59A3gXCU6eC0RB+brjIJ1nsC+khJFwZfcbFhCaGng=', 22);
-- 插入角色
INSERT INTO `role` (id, name, description) VALUES(1, 'admin', '超级管理员');
-- 插入权限(所有权限)
INSERT INTO permission (id, name) VALUES(1, '*');
-- 插入用户-角色关联关系
INSERT INTO user_role (id, user_id, role_id) VALUES(1, 1, 1);
-- 插入角色-权限关联关系
INSERT INTO role_permission (id, role_id, permission_id) VALUES(1, 1, 1);
天生对应POJO实体以及Mapper文件(这里使用Mybatis-plus,你要可以使用其他框架,天生的代码就不展示了,只先容重点)。
我们先创建一个Realm类,继承AuthorizingRealm抽象类(该类可以直接自定义验证和授权的业务),示例代码如下:
@Component
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;
    // 验证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
      String username = (String) authenticationToken.getPrincipal(); // 用户名
      String password = new String((char[]) authenticationToken.getCredentials()); // 凭证=密码,需要转换类型
      User user = userService.getOne(new QueryWrapper<User>().ge("username", username));
      if (user == null) {
            throw new UnknownAccountException("账号不存在");
      }
      // 手动验证密码是否正确,shiro不会帮你做这些
      Sha256Hash sha256Hash = new Sha256Hash(password.getBytes(StandardCharsets.UTF_8), username);
      if (!sha256Hash.toHex().equals(user.getPassword())) {
            throw new IncorrectCredentialsException("密码错误");
      }
      SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, sha256Hash.toHex(), ByteSource.Util.bytes(username), getName());
      return simpleAuthenticationInfo;
    }
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      User user = (User) principalCollection.getPrimaryPrincipal();
      // 获取角色
      List<Role> roleList = roleService.getByUserId(user.getId());
      SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
      roleList.forEach(item ->{
            simpleAuthorizationInfo.addRole(item.getName());
      });
      List<Integer> roleIds = roleList.stream().map(Role::getId).collect(Collectors.toList());
      // 获取权限
      List<Permission> permissions = permissionService.listByIds(roleIds);
      permissions.forEach(item->{
            simpleAuthorizationInfo.addStringPermission(item.getName());
      });
      return simpleAuthorizationInfo;
    }

}
简单先容下SimpleAuthenticationInfo类,以四个构造参数为例:


[*]principal:表现用户的身份信息,这个参数可以通过subject.getPrincipal()方法获取,表现当前记录的用户,进而可以获取该用户的一系列所需属性。
[*]credentials:表现用户的凭据信息,通常是密码明文或密码的加密情势。如果未指定密码匹配规则就是明文,否则就要按指定加密方式进行传参,比如:MD5、SHA-256等等。不然就会报异常错误。
[*]salt:盐值,用于加密密码。这是为了防止两个用户的初始密码雷同,通过添加盐值可以增长密码的复杂性和唯一性。在前面密码学章节有先容。
[*]realmName:表现当前方法调用的Realm名称。
Realm创建好后,需要交给SecurityManager进行管理,示例代码如下:
@Configuration
public class ShiroConfig {
    /**
   * 创建Shiro Web应用的整体安全管理
   */
    @Bean
    public DefaultWebSecurityManager securityManager(){
      DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
      defaultWebSecurityManager.setRealm(realm());
      // 可以添加其他配置,如缓存管理器、会话管理器等
      return defaultWebSecurityManager;
    }
    /**
   * 注册Realm的对象,用于执行安全相关的操作,如用户认证、权限查询
   */
    @Bean
    public Realm realm() {
      UserRealm userRealm = new UserRealm();
      userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法
      return userRealm;
    }
}
另外数据库使用的SHA-256加密,在Realm校验时,向SimpleAuthenticationInfo传参时,为了保持一致,防止报错,所以还需要指定密码算法规则。
    /**
   * 指定密码加密算法类型
   */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
      HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
      hashedCredentialsMatcher.setHashAlgorithmName("SHA-256"); // 设置哈希算法
      return hashedCredentialsMatcher;
    }
然后我们需要配置接口的过滤和拦截,否则访问时会被拦截,如图所示:
https://i-blog.csdnimg.cn/direct/a34316f55e9249e8aa0074787a6883d7.png#pic_center
示例代码如下:
    /**
   * 核心安全过滤器对进入应用的请求进行拦截和过滤,从而实现认证、授权、会话管理等安全功能。
   */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
      ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
      shiroFilterFactoryBean.setSecurityManager(securityManager);
      // 当未登录的用户尝试访问受保护的资源时,重定向到这个指定的登录页面。
      //shiroFilterFactoryBean.setLoginUrl("/user/index");
      // 配置拦截器链,指定了哪些路径需要认证、哪些路径允许匿名访问
      Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
      filterChainDefinitionMap.put("/user/login", "anon");
      filterChainDefinitionMap.put("/user/index", "anon");
      filterChainDefinitionMap.put("/**", "authc");
      shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
      return shiroFilterFactoryBean;
    }
然后我们编写Controller层代码,示例代码如下:
@RestController
@RequestMapping(value = "/user")
public class UserController {
    // 登录跳转
    @GetMapping("/index")
    public String index() {
      return "login.html";
    }
    @PostMapping("/login")
    public ResponseEntity<String> login(@RequestBody User user) {
      UsernamePasswordToken token = new UsernamePasswordToken(
                user.getUsername(),//身份信息
                user.getPassword());//凭证信息
      // 对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      try {
            subject.login(token);
      } catch (UnknownAccountException e) {
            return ResponseEntity.ok("账号或密码错误");
      }
      return ResponseEntity.ok("success");
    }
    // 登录成功后跳转页面
    @GetMapping("/main")
    public String main() {
      return "main.html";
    }
}
然后我们输入地点:http://localhost:8080/user/index,进入登录页面,输入精确的账号,SESSIONID也天生了,进入首页(前端代码不展示),如图所示:
https://i-blog.csdnimg.cn/direct/70e887780fd24768a2a99865dedc8d4f.png#pic_center


[*]未授权跳转
前面的示例中在filterChainDefinitionMap中定义了/user/index登录页面接口为匿名,未登录的情况下访问其他就会重定向到http://localhost:8080/login.jsp,如图所示:
https://i-blog.csdnimg.cn/direct/8dea00cf11ff442688ff965a93e2d765.png#pic_center
如今.jsp页面用的也比力少,如果想让他未授权的时间访问任何地点都会跳转到指定登录页面,我们可以通过setLoginUrl()方法进行设置,示例代码如下:
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
      ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
      shiroFilterFactoryBean.setSecurityManager(securityManager);
      // 当未登录的用户尝试访问受保护的资源时,重定向到这个指定的登录页面。
      shiroFilterFactoryBean.setLoginUrl("/user/index");
      // 配置拦截器链,指定了哪些路径需要认证、哪些路径允许匿名访问
      Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
      filterChainDefinitionMap.put("/user/login", "anon");
      filterChainDefinitionMap.put("/**", "authc");
      shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
      return shiroFilterFactoryBean;
    }
这样设置后,未授权的情况下访问任何接口都可以跳到登录页面。
https://i-blog.csdnimg.cn/direct/0fab8dfc0fe9487f9cf990038344a8a1.gif#pic_center


[*]已授权跳转
前面我们通过setLoginUrl()方法指定未登录情况下的页面跳转,setUnauthorizedUrl()方法用于登陆后访问没权限的资源:
                ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
      shiroFilterFactoryBean.setSecurityManager(securityManager);
      // 当未登录的用户尝试访问受保护的资源时,重定向到这个指定的登录页面。
      shiroFilterFactoryBean.setLoginUrl("/user/index");
      // 成功后跳转地址,但是测试时未生效
//      shiroFilterFactoryBean.setSuccessUrl("/user/main");
      // 当用户访问没有权限的资源时,系统重定向到指定的URL地址。
      shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
如果没有设置接口拦截(指定接口访问脚色等设置),访问的一些接口就会报错误页面,如图所示:
https://i-blog.csdnimg.cn/direct/46b2c4c16e094e82b028a433295b23eb.png#pic_center
设置接口拦截(指定脚色访问),示例代码如下:
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
      ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
      shiroFilterFactoryBean.setSecurityManager(securityManager);
      // 当用户访问没有权限的资源时,系统重定向到指定的URL地址。
      shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
      Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
      filterChainDefinitionMap.put("/user/login", "anon");
      // 设置/root下的资源只能root角色访问
      filterChainDefinitionMap.put("/root/**", "roles");
      filterChainDefinitionMap.put("/**", "authc");
      shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
      return shiroFilterFactoryBean;
    }
使用其他脚色访问/root/**下的资源,就会跳转指定页面,如图所示:
https://i-blog.csdnimg.cn/direct/4d5bf4ac43ca440b81047a9db03f570c.png#pic_center
setSuccessUrl()方法用于乐成后页面跳转,但是在测试过程中没有见效(如果知道办理办法的,可以批评区交流),不过可以在登录乐成后前端或后端跳转,办理这种标题。
如今很多互联网公司在跳转登录页面时会判定用户会话是否存在,如果存在就直接跳转首页,不会进行登录操作,除非登录退出后重新登录,shiro提供非常方便的操作,示例代码如下:
    @GetMapping("/index")
    public ModelAndView index() {
      Subject subject = SecurityUtils.getSubject();
      if (subject.isAuthenticated() || subject.isRemembered()) {
            return new ModelAndView("redirect:main");
      }
      return new ModelAndView("login.html");
    }
我们在跳转登录页面时通过isAuthenticated()方法(用户已登录)和isRemembered()方法(用户是否勾选“记着我”功能)判定用户身份,如果有存在的会话就不让他重新登录,直接跳到首页即可。
另外在多说一句,在前后不分离项目中,可能ShiroFilterFactoryBean 用的比力多,如果前后分离,还有一种方式定义请求过滤链的组件,示例代码如下:
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
      DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
      chainDefinition.addPathDefinition("/user/login", "anon");
      chainDefinition.addPathDefinition("/**", "authc");
      return chainDefinition;
    }
登录超时

登录超时是一种安全措施,如果用户在公共计算机或共享设备上登录后忘记退出,登录超时可以确保他们的会话在一段时间后自动结束,从而降低被他人冒用的风险。
另一方面开释服务器端资源。每个生动的会话都会占用服务器的肯定资源,包括内存、数据库毗连等。通过设置合理的登录超时时间,可以自动开释那些不再需要的会话资源,进步服务器的性能和效率。
示例代码如下:
    /**
   * 创建Shiro Web应用的整体安全管理
   */
    @Bean
    public DefaultWebSecurityManager securityManager(){
      DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
      defaultWebSecurityManager.setRealm(realm());
      defaultWebSecurityManager.setSessionManager(defaultWebSessionManager()); // 注册会话管理
      // 可以添加其他配置,如缓存管理器、会话管理器等
      return defaultWebSecurityManager;
    }
    /**
   * 创建会话管理
   */
    @Bean
    public DefaultWebSessionManager defaultWebSessionManager(){
      DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
      defaultWebSessionManager.setGlobalSessionTimeout(10000); //设置会话过期时间,毫秒
      return defaultWebSessionManager;
    }
上述代码设置了会话过期时间为10s,当时间范围内没有操作,自动跳转登录页面,
记着我

“记着我”(Remember Me)功能答应体系记着用户的登录状态,纵然用户关闭浏览器后(关闭整个浏览器,不是只关闭页面),下一次访问时仍然能够自动登录。这个功能通常通过在用户登录时设置一个特殊的 rememberMe cookie 来实现。
调用登录接口,设置RememberMe的参数值,示例代码如下:
// 方法一:
UsernamePasswordToken token = new UsernamePasswordToken(username, password, true);
// 方法二:
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
然后登录乐成后,我们可以在浏览器的Application中看到,有一个rememberMe属性,如图所示:
https://i-blog.csdnimg.cn/direct/10dea88501a14a98b409ce1f2b481a2a.png#pic_center
   注:当你再次把浏览器全部关闭后,rememberMe可能仍然未见效,你需要检查你的拦截器链,将authc过滤器改为user过滤器
示例代码如下:
      Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
      filterChainDefinitionMap.put("/root/**", "roles");
      filterChainDefinitionMap.put("/user/login", "anon");
      filterChainDefinitionMap.put("/**", "user");
然后再次尝试即可(user过滤器可以过滤未登录和rememberMe功能)。
默认情况下,cookie有效期为一年,如图所示:
https://i-blog.csdnimg.cn/direct/53cc8dd41ff44f66a6e956030a7a55d7.png#pic_center
你也可以自定义cookie有效期,示例代码如下:
    /**
   * cookie管理对象;记住我功能,rememberMe管理器
   * @return
   */
    @Bean
    public CookieRememberMeManager rememberMeManager(){
      CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
      cookieRememberMeManager.setCookie(rememberMeCookie());
      //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
      cookieRememberMeManager.setCipherKey(Base64.getDecoder().decode("h0nCDfE5LEh3owvvugjP+HyYa7NDuduM2bUUPJf8zII="));
      return cookieRememberMeManager;
    }
    @Bean
    public SimpleCookie rememberMeCookie(){
      // 使用默认命名:rememberMe
      SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
      // setcookie 的 httponly 属性如果设为 true 的话,会增加对 xss 防护的安全系数,
      // 只能通过http访问,javascript无法访问,防止xss读取cookie
      simpleCookie.setHttpOnly(true);
      simpleCookie.setPath("/");
      // 记住我 cookie 生效时间10秒 ,单位是秒
      simpleCookie.setMaxAge(10);
      return simpleCookie;
    }
    /**
   * aes加密方法
   * @return
   */
    public static String generateRandomKey(int keySize) throws Exception {
      // 创建一个密钥生成器,指定算法为AES(或其他你选择的算法)
      KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
      // 初始化密钥生成器,指定密钥长度(例如128, 192, 或 256位)
      keyGenerator.init(keySize);
      // 生成一个密钥
      SecretKey secretKey = keyGenerator.generateKey();
      // 将密钥转换为字节数组
      byte[] keyBytes = secretKey.getEncoded();
      // 将字节数组编码为Base64字符串
      String encodedKey = Base64.getEncoder().encodeToString(keyBytes);
      return encodedKey;
    }
然后将rememberMeManager注册到DefaultWebSecurityManager中,示例代码如下:
    /**
   * 创建Shiro Web应用的整体安全管理
   */
    @Bean
    public DefaultWebSecurityManager securityManager(){
      DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
      defaultWebSecurityManager.setRealm(realm());
      defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
      // 可以添加其他配置,如缓存管理器、会话管理器等
      return defaultWebSecurityManager;
    }
设置完成后,重启后,访问页面登录后,把浏览器关闭等待10s后再次打卡访问就会跳转到登录页面。
有的时间会很容易把过期时间和“记着我”弄混,下面谈谈他们的区别:


[*]过期时间指的是某个对象、数据或会话在创建后能够保持有效或可用的时间段。一旦超过这个时间,对象、数据或会话将不再有效或可用。
[*]“记着我”是一个功能选项,通常出如今登录表单中。当用户勾选此选项并乐成登录后,纵然关闭浏览器或清除会话,下次访问网站时也会自动登录,无需重新输入用户名和密码。
也就是说纵然勾选了“记着我”登录后,到会话过期时间后未操作一样会重新登录,如今很多公司登录页面都没有“记着我”的功能选项了,反而更偏向于会话时长。
注解

Shiro框架提供的一种简化开发的方式,通过在代码中添加注解,可以方便地实现权限控制和身份认证。


[*]@RequiresAuthentication注解:
表现当前用户必须已经通过身份验证(即登录)才能访问被注解的方法或类。相称于Subject.isAuthenticated()返回true。为了演示我们把config中的权限都去掉,示例代码如下:
    @GetMapping("/list")
    @RequiresAuthentication
    public ResponseEntity<String> list() {
      Subject subject = SecurityUtils.getSubject();
      Session session = subject.getSession();
      System.out.println(session.getId());
      return ResponseEntity.ok("success");
    }
如果我们没有登录的情况下访问该接口,默认情况下就会报错(我们可以自定义处理方式,跳转登录页面),如图所示:
https://i-blog.csdnimg.cn/direct/89182ef15b2e4ec0836395259207f6b9.png#pic_center
登录后就可以返回精确的数据,如图所示:
https://i-blog.csdnimg.cn/direct/ebb7a74258644a2fb1813121493de4bd.png#pic_center


[*]@RequiresUser注解:
表现当前用户已经进行了身份验证,大概通过“记着我”功能进行了登录。这个注解比@RequiresAuthentication更宽松,因为它答应通过“记着我”功能登录的用户访问。
    @GetMapping("/list")
    @RequiresUser
    public ResponseEntity<String> list() {
      //2.2对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      Session session = subject.getSession();
      System.out.println(session.getId());
      return ResponseEntity.ok("success");
    }
该注解不仅支持登录后访问,还支持“记着我”功能,浏览器关闭后也可以继承访问。


[*]@RequiresGuest注解:
表现当前用户没有进行身份验证,大概是以游客身份访问。如果用户已经登录,则会被拒绝访问。
    @GetMapping("/list")
    @RequiresGuest
    public ResponseEntity<String> list() {
      //2.2对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      Session session = subject.getSession();
      System.out.println(session.getId());
      return ResponseEntity.ok("success");
    }
感觉这个注解用的应该挺少把,游客可以访问,登录用户不能访问,如图所示:
https://i-blog.csdnimg.cn/direct/2fa9c170ebd642469bb012060d159768.png#pic_center


[*]@RequiresRoles注解:
表现当前用户必须拥有指定的脚色才能访问被注解的方法或类。可以指定一个或多个脚色,并设置逻辑关系(如AND、OR)。
    @GetMapping("/list")
    @RequiresRoles(value = {"admin","root"}, logical = Logical.AND)
    public ResponseEntity<String> list() {
      //2.2对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      Session session = subject.getSession();
      System.out.println(session.getId());
      return ResponseEntity.ok("success");
    }
如图所示:
https://i-blog.csdnimg.cn/direct/a81a33837d9c45faa1d81ca51c3c7e38.png#pic_center


[*]@RequiresPermissions注解:
当前用户必须拥有指定的权限才能访问被注解的方法或类。可以指定一个或多个权限,并设置逻辑关系(如AND、OR)。
    @GetMapping("/list")
    @RequiresPermissions(value = {"user:list","user:insert"}, logical = Logical.OR)
    public ResponseEntity<String> list() {
      //2.2对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      Session session = subject.getSession();
      System.out.println(session.getId());
      return ResponseEntity.ok("success");
    }
当未登录大概没有该权限的用户访问就会报错,否则返回success,如上图所示。
登录后跳回之前页面

通常我们盼望在用户乐成登录后能够跳回到他们之前尝试访问的页面(即被拦截之前的页面)。
Shiro提供了SavedRequest功能来自动处理这种情况。当你配置了Shiro的Web过滤器后,Shiro会自动捕获被拦截的请求并存储在会话中。一旦用户乐成登录,你可以从会话中获取这个请求并重定向到它。
https://i-blog.csdnimg.cn/direct/34dcdfe197dc425da0727d3ed80ad910.png#pic_center
比如:登录超时后重新登录跳转上次地点页面,示例代码如下:
    @PostMapping("/login")
    public ModelAndView login(HttpServletRequest request, @RequestParam("username") String username
            , @RequestParam("password") String password) {
      UsernamePasswordToken token = new UsernamePasswordToken(
                username,//身份信息
                password);//凭证信息
      ModelAndView modelAndView = new ModelAndView();
      // 对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      if (subject.isAuthenticated() || subject.isRemembered()) {
            modelAndView.setViewName("redirect:main");
            return modelAndView;
      }
      try {
            subject.login(token);
            // 判断savedRequest不为空时,获取上一次停留页面,进行跳转
            SavedRequest savedRequest = WebUtils.getSavedRequest(request);
            if (savedRequest != null) {
                String requestUrl = savedRequest.getRequestUrl();
                modelAndView.setViewName("redirect:"+ requestUrl);
                return modelAndView;
            }
      } catch (AuthenticationException e) {
            modelAndView.addObject("responseMessage", "用户名或者密码错误");
            modelAndView.setViewName("login");
            return modelAndView;
      }
      modelAndView.setViewName("redirect:main");
      return modelAndView;
    }
如图所示:
https://i-blog.csdnimg.cn/direct/56ee3bd349784868beed2589a41b1340.png#pic_center
如果上一个页面是错误页面就会导致登录进来直接进入登录错误页面,所以不建议这种处理方式
自定义缓存

Shiro提供的一个缓存管理器MemoryConstrainedCacheManager使用JVM的内存来存储缓存数据,开发者可能会选择使用EhCache作为默认的缓存实现,因为Shiro提供了与EhCache的集成支持。
登录时虽然继承的父类AuthorizingRealm,实际进入的是AuthenticatingRealm进行的缓存操作,当缓存数据为null时,进入对应的Realm进行数据库查询,否则使用缓存,减轻数据库压力,提供体系性能。
https://i-blog.csdnimg.cn/direct/242fa00f48e84a738d673ec8a8eca2e3.png#pic_center
下面使用Redis当作Shiro缓存(参考MemoryConstrainedCacheManager和MapCache),示例代码如下:
// 定义cachemanage
public class RedisCacheManage implements CacheManager {
    private final RedisTemplate<String, Object> redisTemplate;
    public RedisCacheManage(RedisTemplate<String, Object> redisTemplate) {
      this.redisTemplate = redisTemplate;
    }
    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
      return new RedisCache<>(s, redisTemplate);
    }
}
// 定义shiro缓存
public class RedisCache<K, V> implements Cache<K, V> {
    private final HashOperations<String, K, V> hashOperations;
    private final String name;
    public RedisCache(String name, RedisTemplate<String, Object> redisTemplate) {
      this.name = name;
      this.hashOperations = redisTemplate.opsForHash();
    }
    @Override
    public V get(K k) throws CacheException {
      return hashOperations.get(name, k);
    }

    @Override
    public V put(K k, V v) throws CacheException {
      hashOperations.put(name, k, v);
      return v;
    }

    @Override
    public V remove(K k) throws CacheException {
      V v = hashOperations.get(name, k);
      hashOperations.delete(name, k);
      return v;
    }

    @Override
    public void clear() throws CacheException {
      hashOperations.delete(name);
    }

    @Override
    public int size() {
      return hashOperations.size(name).intValue();
    }

    @Override
    public Set<K> keys() {
      return hashOperations.keys(name);
    }

    @Override
    public Collection<V> values() {
      return hashOperations.values(name);
    }
}
为了防止会话堆积你可以设置缓存过期时间。
Config中设置缓存管理,示例代码如下:
    /**
   * 创建Shiro Web应用的整体安全管理
   */
    @Bean
    public DefaultWebSecurityManager securityManager(){
      DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
      defaultWebSecurityManager.setRealm(realm());
      // 可以添加其他配置,如缓存管理器、会话管理器等
      return defaultWebSecurityManager;
    }
    /**
   * 注册Realm的对象,用于执行安全相关的操作,如用户认证、权限查询
   */
    @Bean
    public Realm realm() {
      UserRealm userRealm = new UserRealm();
      userRealm.setCachingEnabled(true); // 启动全局缓存
      userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存
      userRealm.setAuthenticationCacheName("Authentication"); // 定义验证缓存名
      //userRealm.setAuthorizationCachingEnabled(true); // 启动授权缓存
      //userRealm.setAuthorizationCacheName("Authorization"); // 定义授权缓存名
      userRealm.setCacheManager(cacheManager());
      return userRealm;
    }

    @Bean
    public CacheManager cacheManager() {
      RedisCacheManage redisCacheManage = new RedisCacheManage(redisTemplate());
      return redisCacheManage;
    }
然后进行登录操作,第一次登录会查询数据库,如图所示:
https://i-blog.csdnimg.cn/direct/ab3c83d34fd1420b9cda524c7afc4a59.png#pic_center
并在Redis中会插入一条会话的数据(实际上缓存了验证时间的用户信息,授权时间并没缓存也就不需要开启),如图所示:
https://i-blog.csdnimg.cn/direct/9e53f4c6d3b242bfa87c680dbc02d3ff.png#pic_center
第二次登录时,就会直接从缓存中获取数据,不会进入自定义Realm中。
https://i-blog.csdnimg.cn/direct/03ccf86fade54cbd90668d626987e2db.png#pic_center
你有可能碰到ByteSource无法序列化的标题,你需要自己实现序列号和ByteSource(具体逻辑参考SimpleByteSource),示例代码如下:
public class ByteSourceSerializable implements ByteSource,Serializable {
    private static final long serialVersionUID = 9206836077237410719L;
    private final byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public ByteSourceSerializable(byte[] bytes) {
      this.bytes = bytes;
    }

    public ByteSourceSerializable(char[] chars) {
      this.bytes = CodecSupport.toBytes(chars);
    }
    // ... ...
}
然后再自定义Realm中,盐值使用自定义的类,示例代码如下:
SimpleAuthenticationInfo sai= new SimpleAuthenticationInfo(user, sha256Hash.toHex(), new ByteSourceSerializable(username), getName());
自定义SessionDao

在Shiro框架中,SessionDAO的默认实现是MemorySessionDAO。MemorySessionDAO是Shiro已经实现的Session的CRUD(创建、读取、更新、删除)接口类,它内部维护了一个ConcurrentMap来保存session数据,即将session数据缓存在内存中。
https://i-blog.csdnimg.cn/direct/ba3f23f991794d37afeea35f2a14df22.png#pic_center
好像在单机部署的情况下够用,但是在技能发展敏捷的今天来说带来很多标题,比如:


[*]不提供长期化功能,这意味着当服务器重启或发生故障时,用户的会话状态无法规复。
[*]在分布式体系中,由于session数据仅在单个服务器内存中保存,因此当用户请求被负载均衡到其他服务器时,无法访问到之前的session数据。
为了办理这些标题,使用Redis作为Session存储可以办理分布式体系中的Session共享标题,并提供较高的读写性能和可靠性。
创建自定义SessionDao(参考EnterpriseCacheSessionDAO),示例代码如下:
public class RedisSessionDao extends CachingSessionDAO {

    protected Serializable doCreate(Session session) {
      Serializable sessionId = this.generateSessionId(session);
      this.assignSessionId(session, sessionId);
      return sessionId;
    }

    protected Session doReadSession(Serializable sessionId) {
      return null;
    }

    protected void doUpdate(Session session) {
    }

    protected void doDelete(Session session) {
    }
}
Config中设置自定义SessionDao,示例代码如下:
    /**
   * 创建Shiro Web应用的整体安全管理
   */
    @Bean
    public DefaultWebSecurityManager securityManager(){
      DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
      defaultWebSecurityManager.setRealm(realm());
      defaultWebSecurityManager.setSessionManager(defaultWebSessionManager()); // 注册会话管理
      // 可以添加其他配置,如缓存管理器、会话管理器等
      return defaultWebSecurityManager;
    }
    /**
   * 创建会话管理
   */
    @Bean
    public DefaultWebSessionManager defaultWebSessionManager() {
      DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
      defaultWebSessionManager.setGlobalSessionTimeout(10000); // 缓存过期时间
      defaultWebSessionManager.setSessionDAO(sessionDAO());
      defaultWebSessionManager.setCacheManager(cacheManager()); // 设置缓存管理器,自动给sessiondao赋值
      return defaultWebSessionManager;
    }
    @Bean
    public SessionDAO sessionDAO() {
      RedisSessionDao redisSessionDao = new RedisSessionDao();
//      redisSessionDao.setSessionIdGenerator(sessionIdGenerator());
//      redisSessionDao.setCacheManager(cacheManager()); // 设置缓存管理器
      redisSessionDao.setActiveSessionsCacheName("shiro:session"); // 自定义redis存放的key名称
      return redisSessionDao;
    }
    @Bean
    public SessionIdGenerator sessionIdGenerator(){
      return new CustomSessionManager();
    }
    @Bean
    public CacheManager cacheManager() {
      RedisCacheManage redisCacheManage = new RedisCacheManage(redisTemplate());
      return redisCacheManage;
    }
    // redis配置
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
      RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
      redisTemplate.setConnectionFactory(redisConnectionFactory);
      //设置默认序列化方式,类型转换错误
      redisTemplate.setDefaultSerializer(new JdkSerializationRedisSerializer());
      //启动默认序列化
      redisTemplate.setEnableDefaultSerializer(true);
      return redisTemplate;
    }
登录后跳转正常,如图所示:
https://i-blog.csdnimg.cn/direct/0464d0a8dd024bf794e8a6ea42f8910a.png#pic_center
查看Redis中是否保存Session信息,如图所示:
https://i-blog.csdnimg.cn/direct/54da0103e5044d7db967fadfaf2f365d.png#pic_center
等待10s后会话过期,自动跳回登录页面,也没标题(每次过期后会将当前Session对象删除再创建新的Session对象,新的对象未使用隔段时间会定时清除)。
自定义过滤器

Shiro的内置过滤器可能无法满足全部复杂的权限控制需求。自定义过滤器可以用于实现特定的认证逻辑,如验证码验证、第三方登录等;也可以实现基于用户访问频率的限定,防止恶意请求或太过使用资源。


[*]OncePerRequestFilter: 确保每个请求只会被过滤一次。如果你只是需要执行一些通用的过滤逻辑(比方日志、请求记录等),可以继承这个类。
public class UserLoginFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
      System.out.println("进来了");
      if (false) {
            // 执行下一个过滤器
            filterChain.doFilter(request, response);
      } else {
            // 方法一:返回json数据
            Map<String, Object> map = new HashMap<>();
            map.put("code", "fail");
            map.put("message", "错误");
            response.getWriter().println(map);
            // 方法二:重定向其它页面
            ((HttpServletResponse)response).sendRedirect("/user/main");
      }
    }
}
Config配置文件,示例代码如下:
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
      ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
      shiroFilterFactoryBean.setSecurityManager(securityManager);
                // 过滤器赋值
      Map<String, Filter> filterMap = new HashMap<>();
      filterMap.put("userLoginFilter", userLoginFilter());
      shiroFilterFactoryBean.setFilters(filterMap);
      // 配置拦截器链,指定了哪些路径需要认证、哪些路径允许匿名访问
      Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
      filterChainDefinitionMap.put("/user/login", "anon");
      filterChainDefinitionMap.put("/**", "userLoginFilter");
      shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
      return shiroFilterFactoryBean;
    }
    @Bean
    public UserLoginFilter userLoginFilter(){
      return new UserLoginFilter();
    }



[*]AccessControlFilter:用来进行访问控制,比如判定用户是否有权限访问某个 URL。如果你要实现基于认证、授权等条件的控制,可以继承这个类。
public class UserLoginFilter extends AccessControlFilter {
    /**
   * 确定是否允许访问。如果返回true,则允许访问,继续进行;如果返回false,则会调用onAccessDenied方法
   */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object o) throws Exception {
      System.out.println("允许访问-进来了");
      return ((HttpServletRequest)request).getRequestURI().equals("/user/main");
    }

    /**
   * 当isAccessAllowed方法返回false时,此方法被调用。在这里可以实现访问被拒绝时的处理
   */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
      System.out.println("拒绝访问-进来了");
      // 方法一:返回json数据
      Map<String, Object> map = new HashMap<>();
      map.put("code", "fail");
      map.put("message", "错误");
      response.getWriter().println(map);
      // 方法二:重定向其它页面
      ((HttpServletResponse)response).sendRedirect("/user/index");
      return false;
    }
}
当访问/user/main时被答应,访问其它页面则拒绝,进入onAccessDenied()方法,然后进行对应拒绝处理,如图所示:
https://i-blog.csdnimg.cn/direct/0565b5a77e8f49efa6e8c05b88b26e4c.png#pic_center
登录退出

在Apache Shiro中,退出体系通常意味着开释当前用户的全部认证信息并结束会话。
    @GetMapping("/logout")
    public void logout() {
      SecurityUtils.getSubject().logout();
    }

调用logout()方法就可以实现登录退出的功能,我们可以看下内里有做哪些处理,
https://i-blog.csdnimg.cn/direct/b9a281f11bdc4ef395502df22e13b8b6.png#pic_center
以缓存为例,进入CachingRealm类,如图所示:
https://i-blog.csdnimg.cn/direct/df02f83dd94b42a294f1a4b54bbde6fc.png#pic_center
根据Debug先进入AuthorizingRealm类(前面先容过缓存没保存授权的记录,如果开启了缓存仍然会删除操作),先进入AuthenticatingRealm.doClearCache(),然后获取缓存和凭据进行删除操作,如图所示:
https://i-blog.csdnimg.cn/direct/4d054420285e4ec7978ffd501b36db32.png#pic_center
如果使用自定义了SessionDAO使用缓存进行管理的话,进入finally代码块,执行会话清除工作,如图所示:
https://i-blog.csdnimg.cn/direct/19da441fe9844a659462a5786382177c.png#pic_center
然后CachingSessionDAO类将原来的Session删除后,天生一个新的Session对象用于下次登录操作(登录时再执行修改操作将登录的信息保存到该Session),如图所示:
https://i-blog.csdnimg.cn/direct/2561c9ca914c462eb52b29f4b662bfd5.png#pic_center
单用户登录

单用户登录(Single User Login)其核心意义在于一个账号在同一时间仅能由一个用户进行登录。如果某个用户已经使用某一账号乐成登录体系,那么当另一用户尝试以同一账号登录时,体系将会阻止这次登录尝试,大概迫使先前登录的用户退出体系,以便新用户可以登录,确保账号使用的唯一性和安全性。
网上最常见的方法就是获取全部已存在会话,通过遍历匹配会话进行删除,示例代码如下:
@Controller
@RequestMapping(value = "/user")
public class UserController {
    @Autowired
    private SessionDAO sessionDAO;
    @PostMapping("/login")
    public ModelAndView login(HttpServletRequest request, @RequestParam("username") String username
            , @RequestParam("password") String password) {
      // 提前加密,解决自定义缓存匹配时错误
      UsernamePasswordToken token = new UsernamePasswordToken(
                username,//身份信息
                password);//凭证信息
      ModelAndView modelAndView = new ModelAndView();
      // 对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      if (subject.isAuthenticated() && subject.isRemembered()) {
            modelAndView.setViewName("redirect:main");
            return modelAndView;
      }
      try {
            subject.login(token);
            // 单用户登录:匹配已存在会话进行清除
            Collection<Session> activeSessions = sessionDAO.getActiveSessions();
            for (Session session : activeSessions) {
                String oldUsername = (String) session.getAttribute("username");
                if (oldUsername != null && oldUsername.equals(username)) {
                  sessionDAO.delete(session);
                }
            }
      } catch (AuthenticationException e) {
            e.printStackTrace();
            modelAndView.addObject("responseMessage", "用户名或者密码错误");
            modelAndView.setViewName("redirect:index");
            return modelAndView;
      }
      // 设置会话属性
      Session session = subject.getSession();
      session.setAttribute("username", username);
      modelAndView.setViewName("redirect:main");
      return modelAndView;
    }
}
通过遍历去匹配会话的方式,在会话数量巨大的情况下,影响体系的团体性能。
办理该标题的另一种方法就是再定义一个username的Key,内里存放会话信息,这样匹配的效率进步。示例代码如下:
public class RedisSessionDao extends CachingSessionDAO {
    private final static String usernameKey = "basic_";
    protected Serializable doCreate(Session session) {
      Serializable sessionId = this.generateSessionId(session);
      this.assignSessionId(session, sessionId);
      return sessionId;
    }

    protected Session doReadSession(Serializable sessionId) {
      return null;
    }

    protected void doUpdate(Session session) {
      if (session.getAttribute("username") != null){
            String username = (String) session.getAttribute("username");
            getActiveSessionsCache().put(usernameKey+username, session);
      }
    }

    protected void doDelete(Session session) {
      if (session.getAttribute("username") != null){
            String username = (String) session.getAttribute("username");
            getActiveSessionsCache().remove(usernameKey+username);
      }
    }

    public Session getByUsername(String username){
      String key = usernameKey+username;
      return getActiveSessionsCache().get(key);
    }
}
然后调用getByUsername()方法进行下线逻辑,示例代码如下:
@Controller
@RequestMapping(value = "/user")
public class UserController {
    @Autowired
    private SessionDAO sessionDAO;
    @PostMapping("/login")
    public ModelAndView login(HttpServletRequest request, @RequestParam("username") String username
            , @RequestParam("password") String password) {
      // 提前加密,解决自定义缓存匹配时错误
      UsernamePasswordToken token = new UsernamePasswordToken(
                username,//身份信息
                password);//凭证信息
      ModelAndView modelAndView = new ModelAndView();
      // 对用户信息进行身份认证
      Subject subject = SecurityUtils.getSubject();
      if (subject.isAuthenticated() && subject.isRemembered()) {
            modelAndView.setViewName("redirect:main");
            return modelAndView;
      }
      try {
            subject.login(token);
            // 单用户登录:匹配已存在会话进行清除
            Session activeSessions = sessionDAO.getByUsername(username);
            if (activeSessions != null) {
                sessionDAO.delete(activeSessions);
            }
      } catch (AuthenticationException e) {
            e.printStackTrace();
            modelAndView.addObject("responseMessage", "用户名或者密码错误");
            modelAndView.setViewName("redirect:index");
            return modelAndView;
      }
      modelAndView.setViewName("redirect:main");
      return modelAndView;
    }
}
标题排查

在整合Shiro过程会碰到很多标题,相干文章如下:
办理SpringBoot整合Shiro报Submitted credentials for token

办理SpringBoot整合Shiro报Submitted credentials for token org.apache.shiro.authc.UsernamePasswordToken…
办理Springboot整合Shiro自定义SessionDAO+Redis管理会话,登录后不跳转首页

办理Springboot整合Shiro自定义SessionDAO+Redis管理会话,登录后不跳转首页
办理Springboot整合Shiro+Redis退出登录后不清除缓存

办理Springboot整合Shiro+Redis退出登录后不清除缓存
项目示例

项目示例

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 安全框架:Apache Shiro