Ruoyi框架学习笔记-Shiro
<p>[TOC]</p>
<h1>1. 简介</h1>
<p>Apache Shiro是一个开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架具有直观、易用等特性,同时也能提供健壮的安全性,虽然它的功能不如SpringSecurity那么强大,但是在普通的项目中也够用了。</p>
<p>1.1 功能
主要功能:</p>
<ol>
<li>Authentication:认证。有时也简称为“登录”,这是一个证明用户是谁的行为。</li>
<li>Authorization:授权。访问控制的过程,也就是决定“谁”去访问“什么”。</li>
<li>Session Management:会话管理。管理用户特定的会话,即使在非Web 或EJB 应用程序。</li>
<li>Cryptography:加密。通过使用加密算法保持数据安全同时易于使用。</li>
</ol>
<p>额外功能:</p>
<ol>
<li>Web Support:Shiro的web支持的API能够轻松地帮助保护Web应用程序。</li>
<li><strong>Caching</strong>:缓存是Apache Shiro中的第一层公民,来确保安全操作快速而又高效。</li>
<li>Concurrency:Apache Shiro利用它的并发特性来支持多线程应用程序。</li>
<li>Testing:测试支持的存在来帮助你编写单元测试和集成测试。</li>
<li>"Run As":一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。</li>
<li>"<strong>Remember Me</strong>":在会话中记住用户的身份,这样用户只需要在强制登录时候登录。</li>
</ol>
<p>1.2 Shiro可以做的事情:</p>
<ol>
<li>验证用户来核实他们的身份</li>
<li>对用户执行访问控制,如:判断用户是否被分配了一个确定的安全角色;判断用户是否被允许做某事</li>
<li>在任何环境下使用Session API,即使没有Web容器</li>
<li>在身份验证,访问控制期间或在会话的生命周期,对事件作出反应</li>
<li>聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”</li>
<li>单点登录(SSO)功能</li>
<li>为没有关联到登录的用户启用"Remember Me"服务等等</li>
</ol>
<p>1.2 3大核心组件</p>
<p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=846647ef68f4e9df4a1b4d9c60d56fe8&amp;file=file.png" alt="" /></p>
<ul>
<li>
<p>Subject: 其实代表的就是当前正在执行操作的用户,只不过因为“User”一般指代人,但是一个“Subject”可以是人,也可以是任何的第三方系统,服务账号等任何其他正在和当前系统交互的第三方软件系统。所有的Subject实例都被绑定到一个SecurityManager,如果你和一个Subject交互,所有的交互动作都会被转换成Subject与SecurityManager的交互。</p>
</li>
<li>
<p>SecurityManager: 是Shiro的核心,用于管理所有的Subject ,它主要用于协调Shiro内部各种安全组件,不过我们一般不用太关心SecurityManager,对于应用程序开发者来说,主要还是使用Subject的API来处理各种安全验证逻辑。</p>
</li>
<li>Realm: 2.2介绍</li>
</ul>
<h1>2. Realm</h1>
<p>2.1 认证(登录)流程</p>
<p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=775567f60afd9df747d644d72cdb3f8b&amp;file=file.png" alt="" /></p>
<p>参照此图,我们的登录一共要经过如下几个步骤:</p>
<ol>
<li>
<p>应用程序代码调用Subject.login方法,传递创建好的包含终端用户的Principals(身份)和Credentials(凭证)的AuthenticationToken实例(即上文例子中的UsernamePasswordToken)。</p>
</li>
<li>
<p>Subject实例,通常是DelegatingSubject(或子类)委托应用程序的SecurityManager通过调用securityManager.login(token)开始真正的验证工作(在DelegatingSubject类的login方法中打断点即可看到)。</p>
</li>
<li>
<p>SubjectManager作为一个基本的“保护伞”的组成部分,接收token以及简单地委托给内部的Authenticator实例通过调用authenticator.authenticate(token)。这通常是一个ModularRealmAuthenticator实例,支持在身份验证中协调一个或多个Realm实例。ModularRealmAuthenticator本质上为Apache Shiro 提供了PAM-style 范式(其中在PAM 术语中每个Realm 都是一个'module')。</p>
</li>
<li>
<p>如果应用程序中配置了一个以上的Realm,ModularRealmAuthenticator实例将利用配置好的AuthenticationStrategy来启动Multi-Realm认证尝试。在Realms 被身份验证调用之前,期间和以后,AuthenticationStrategy被调用使其能够对每个Realm的结果作出反应。如果只有一个单一的Realm 被配置,它将被直接调用,因为没有必要为一个单一Realm的应用使用AuthenticationStrategy。</p>
</li>
<li>每个配置的Realm用来帮助看它是否支持提交的AuthenticationToken。如果支持,那么支持Realm的getAuthenticationInfo方法将会伴随着提交的token被调用。</li>
</ol>
<p>2.2 什么是Realm
Realms担当Shiro和你的应用程序的安全数据之间的“桥梁”或“连接器”。当它实际上与安全相关的数据如用来执行身份验证(登录)及授权(访问控制)的用户帐户交互时,Shiro从一个或多个为应用程序配置的Realm 中寻找许多这样的东西。在这个意义上说,<strong>Realm 本质上是一个特定安全的DAO:它封装了数据源的连接详细信息,使Shiro 所需的相关的数据可用。通常情况下,在Realm中会直接从我们的数据源中获取Shiro需要的验证信息。</strong>当配置Shiro 时,你必须指定至少一个Realm 用来进行身份验证和/或授权。SecurityManager可能配置多个Realms,但至少有一个是必须的。</p>
<p>Realm接口:</p>
<pre><code class="language-java">public interface Realm {
String getName();
boolean supports(AuthenticationToken var1);
AuthenticationInfo getAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;
}</code></pre>
<p>该接口中有三个方法,第一个getName方法用来获取当前Realm的名字,第二个supports方法用来判断这个realm所支持的token,第三个getAuthenticationInfo方法则进行了登陆逻辑判断,根据token去获取到该用户信息,进行判断.</p>
<p>Shiro中Realm接口有许多不同的实现类及其子类,Ruoyi中自定义UserRealm类如下:</p>
<pre><code class="language-java">/**
* 自定义Realm 处理登录 权限
*
* @author ruoyi
*/
public class UserRealm extends AuthorizingRealm
{
private static final Logger log = LoggerFactory.getLogger(UserRealm.class);
@Autowired
private ISysMenuService menuService;
@Autowired
private ISysRoleService roleService;
@Autowired
private SysLoginService loginService;
/**
* 获取当前登录用户的授权信息(角色+资源权限)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0)
{
SysUser user = ShiroUtils.getSysUser();
// 角色列表
Set&lt;String&gt; roles = new HashSet&lt;String&gt;();
// 功能列表
Set&lt;String&gt; menus = new HashSet&lt;String&gt;();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 管理员拥有所有权限
if (user.isAdmin())
{
info.addRole(&quot;admin&quot;);
info.addStringPermission(&quot;*:*:*&quot;);
}
else
{
roles = roleService.selectRoleKeys(user.getUserId());
menus = menuService.selectPermsByUserId(user.getUserId());
// 角色加入AuthorizationInfo认证对象
info.setRoles(roles);
// 权限加入AuthorizationInfo认证对象
info.setStringPermissions(menus);
}
return info;
}
/**
* 登录认证(根据token去数据库中查询出该用户信息)
* 1. 从token中获取到username,password
* 2. loginService.login(username, password);
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
{
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String username = upToken.getUsername();
String password = &quot;&quot;;
if (upToken.getPassword() != null)
{
password = new String(upToken.getPassword());
}
SysUser user = null;
try
{
user = loginService.login(username, password);
}
catch (CaptchaException e)
{
throw new AuthenticationException(e.getMessage(), e);
}
catch (UserNotExistsException e)
{
throw new UnknownAccountException(e.getMessage(), e);
}
catch (UserPasswordNotMatchException e)
{
throw new IncorrectCredentialsException(e.getMessage(), e);
}
catch (UserPasswordRetryLimitExceedException e)
{
throw new ExcessiveAttemptsException(e.getMessage(), e);
}
catch (UserBlockedException e)
{
throw new LockedAccountException(e.getMessage(), e);
}
catch (RoleBlockedException e)
{
throw new LockedAccountException(e.getMessage(), e);
}
catch (Exception e)
{
log.info(&quot;对用户[&quot; + username + &quot;]进行登录验证..验证未通过{}&quot;, e.getMessage());
throw new AuthenticationException(e.getMessage(), e);
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
return info;
}
......
</code></pre>
<p>还需要将自定义的Realm在配置类中设置为Bean,并设置缓存管理器以及授权缓存的名字:</p>
<pre><code class="language-java">/**
* 自定义Realm
*/
@Bean
public UserRealm userRealm(EhCacheManager cacheManager)
{
UserRealm userRealm = new UserRealm();
userRealm.setAuthorizationCacheName(Constants.SYS_AUTH_CACHE);//指定授权缓存的名字:sys-authCache
userRealm.setCacheManager(cacheManager);
return userRealm;
}</code></pre>
<p>UserRealm继承自AuthorizingRealm,实现了doGetAuthenticationInfo方法和doGetAuthorizationInfo方法,分别完成登录认证和获取当前登录用户的授权信息的功能。doGetAuthorizationInfo方法调用loginService.login(username, password)完成登录认证,login方法的主要逻辑包括:</p>
<ol>
<li>验证码校验:已由验证码过滤器CaptchaValidateFilter完成校验并将结果设置到请求的属性中</li>
<li>对username和password值的一些规范校验</li>
<li>根据username查询到数据库中该用户信息</li>
<li>判断该用户状态是否正常</li>
<li>
<p>检验用户信息中密码是否与password匹配(校验重试次数):调用SysPasswordService的validate(SysUser user, String password)方法校验,该方法缓存了尝试登录用户的失败重试次数,若重试次数达到阈值,抛出异常;数据库中保存的密码为Md5Hash(用户名+设置密码+盐值).toHex(),校验时按此规则匹配</p>
<pre><code class="language-java">/**
* 登录密码方法
*
* @author ruoyi
*/
@Component
public class SysPasswordService
{
@Autowired
private CacheManager cacheManager;
private Cache&lt;String, AtomicInteger&gt; loginRecordCache;
@Value(value = &quot;${user.password.maxRetryCount}&quot;)
private String maxRetryCount;
@PostConstruct
public void init()
{
loginRecordCache = cacheManager.getCache(ShiroConstants.LOGINRECORDCACHE);
}
/**
* 1. 从loginRecordCache缓存中获取重试次数,若缓存中不存在则初始化为0&lt;loginName,retryCount&gt;
* 2. 重试次数+1
* 3. 若重试次数达到阈值,抛出异常
* 4. 校验数据库中用户密码是否和password匹配,若不匹配则抛异常
* 5. 登录成功后清除重试次数缓存
* @param user 1
* @param password 2
* @return: void
* @author: gefeng
* @date: 2022/6/7 11:40
*/
public void validate(SysUser user, String password)
{
String loginName = user.getLoginName();
AtomicInteger retryCount = loginRecordCache.get(loginName);
if (retryCount == null)
{
retryCount = new AtomicInteger(0);
loginRecordCache.put(loginName, retryCount);
}
if (retryCount.incrementAndGet() &gt; Integer.valueOf(maxRetryCount).intValue())
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.LOGIN_FAIL, MessageUtils.message(&quot;user.password.retry.limit.exceed&quot;, maxRetryCount)));
throw new UserPasswordRetryLimitExceedException(Integer.valueOf(maxRetryCount).intValue());
}
if (!matches(user, password))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.LOGIN_FAIL, MessageUtils.message(&quot;user.password.retry.limit.count&quot;, retryCount)));
loginRecordCache.put(loginName, retryCount);
throw new UserPasswordNotMatchException();
}
else
{
clearLoginRecordCache(loginName);
}
}
public boolean matches(SysUser user, String newPassword)
{
return user.getPassword().equals(encryptPassword(user.getLoginName(), newPassword, user.getSalt()));
}
public void clearLoginRecordCache(String loginName)
{
loginRecordCache.remove(loginName);
}
public String encryptPassword(String loginName, String password, String salt)
{
return new Md5Hash(loginName + password + salt).toHex();
}
}</code></pre>
</li>
</ol>
<h1>3. Remeber Me</h1>
<p>3.1 原理流程</p>
<ol>
<li>首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并加密保存下来;</li>
<li>关闭浏览器再重新打开;会发现浏览器还是记住你的;</li>
<li>访问一般的网页服务器端还是知道你是谁,且能正常访问;</li>
</ol>
<p>3.2 配置
记住我Cookie:SimpleCookie</p>
<pre><code class="language-java">/**
* cookie 属性设置
*/
public SimpleCookie rememberMeCookie()
{
SimpleCookie cookie = new SimpleCookie(&quot;rememberMe&quot;);
cookie.setDomain(domain);//Cookie的域名 默认空,即当前访问的域名
cookie.setPath(path);
cookie.setHttpOnly(httpOnly);//设为true后,只能通过http访问,javascript无法访问+防止xss读取cookie
cookie.setMaxAge(maxAge * 24 * 60 * 60);//cookie过期时间:30天
return cookie;
}</code></pre>
<p>记住我管理器:CookieRememberMeManager</p>
<pre><code class="language-java">/**
* 记住我
*/
public CookieRememberMeManager rememberMeManager()
{
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
if (StringUtils.isNotEmpty(cipherKey))
{
cookieRememberMeManager.setCipherKey(Base64.decode(cipherKey));
}
else
{
cookieRememberMeManager.setCipherKey(CipherUtils.generateNewKey(128, &quot;AES&quot;).getEncoded());
}
return cookieRememberMeManager;
}</code></pre>
<p>在SecurityManager中配置记住我管理器</p>
<pre><code class="language-java">// 记住我
securityManager.setRememberMeManager(rememberMe ? rememberMeManager() : null);</code></pre>
<p>在登录的Controller中接受前台记住我参数,然后传给usernamePasswordToken</p>
<pre><code class="language-java">@PostMapping(&quot;/login&quot;)
@ResponseBody
public AjaxResult ajaxLogin(String username, String password, Boolean rememberMe)
{
UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
Subject subject = SecurityUtils.getSubject();
try
{
subject.login(token);
return success();
}
......
}</code></pre>
<h1>4. 缓存管理器CacheManager-(ehcache缓存)</h1>
<p>4.1 ehcache缓存配置文件:ehcache-shiro.xml</p>
<pre><code class="language-java">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
&lt;ehcache name=&quot;ruoyi&quot; updateCheck=&quot;false&quot;&gt;
&lt;!-- 磁盘缓存位置 --&gt;
&lt;diskStore path=&quot;java.io.tmpdir&quot;/&gt;
&lt;!-- maxEntriesLocalHeap:堆内存中最大缓存对象数,0没有限制 --&gt;
&lt;!-- maxElementsInMemory: 在内存中缓存的element的最大数目。--&gt;
&lt;!-- eternal:elements是否永久有效,如果为true,timeouts将被忽略,element将永不过期 --&gt;
&lt;!-- timeToIdleSeconds:失效前的空闲秒数,当eternal为false时,这个属性才有效,0为不限制 --&gt;
&lt;!-- timeToLiveSeconds:失效前的存活秒数,创建时间到失效时间的间隔为存活时间,当eternal为false时,这个属性才有效,0为不限制 --&gt;
&lt;!-- overflowToDisk: 如果内存中数据超过内存限制,是否要缓存到磁盘上 --&gt;
&lt;!-- statistics:是否收集统计信息。如果需要监控缓存使用情况,应该打开这个选项。默认为关闭(统计会影响性能)。设置statistics=&quot;true&quot;开启统计 --&gt;
&lt;!-- 默认缓存 --&gt;
&lt;defaultCache
maxEntriesLocalHeap=&quot;1000&quot;
eternal=&quot;false&quot;
timeToIdleSeconds=&quot;3600&quot;
timeToLiveSeconds=&quot;3600&quot;
overflowToDisk=&quot;false&quot;&gt;
&lt;/defaultCache&gt;
&lt;!-- 登录记录缓存 锁定10分钟 --&gt;
&lt;cache name=&quot;loginRecordCache&quot;
maxEntriesLocalHeap=&quot;2000&quot;
eternal=&quot;false&quot;
timeToIdleSeconds=&quot;600&quot;
timeToLiveSeconds=&quot;0&quot;
overflowToDisk=&quot;false&quot;
statistics=&quot;false&quot;&gt;
&lt;/cache&gt;
&lt;!-- 系统活跃用户缓存 --&gt;
&lt;cache name=&quot;sys-userCache&quot;
maxEntriesLocalHeap=&quot;10000&quot;
overflowToDisk=&quot;false&quot;
eternal=&quot;false&quot;
diskPersistent=&quot;false&quot;
timeToLiveSeconds=&quot;0&quot;
timeToIdleSeconds=&quot;0&quot;
statistics=&quot;false&quot;&gt;
&lt;/cache&gt;
&lt;!-- 系统用户授权缓存 没必要过期 --&gt;
&lt;cache name=&quot;sys-authCache&quot;
maxEntriesLocalHeap=&quot;10000&quot;
overflowToDisk=&quot;false&quot;
eternal=&quot;false&quot;
diskPersistent=&quot;false&quot;
timeToLiveSeconds=&quot;0&quot;
timeToIdleSeconds=&quot;0&quot;
memoryStoreEvictionPolicy=&quot;LRU&quot;
statistics=&quot;false&quot;/&gt;</code></pre>
<p>4.2 EhCacheManager配置,从配置文件中读取配置</p>
<pre><code class="language-java">/**
* 缓存管理器 使用Ehcache实现
*/
@Bean
public EhCacheManager getEhCacheManager()
{
net.sf.ehcache.CacheManager cacheManager = net.sf.ehcache.CacheManager.getCacheManager(&quot;ruoyi&quot;);
EhCacheManager em = new EhCacheManager();
if (StringUtils.isNull(cacheManager))
{
//从ehcache配置文件中读取配置的缓存信息
em.setCacheManager(new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream()));
return em;
}
else
{
em.setCacheManager(cacheManager);
return em;
}
}
/**
* 返回配置文件流 避免ehcache配置文件一直被占用,无法完全销毁项目重新部署
*/
protected InputStream getCacheManagerConfigFileInputStream()
{
String configFile = &quot;classpath:ehcache/ehcache-shiro.xml&quot;;
InputStream inputStream = null;
try
{
inputStream = ResourceUtils.getInputStreamForPath(configFile);
byte[] b = IOUtils.toByteArray(inputStream);
InputStream in = new ByteArrayInputStream(b);
return in;
}
catch (IOException e)
{
throw new ConfigurationException(
&quot;Unable to obtain input stream for cacheManagerConfigFile [&quot; + configFile + &quot;]&quot;, e);
}
finally
{
IOUtils.closeQuietly(inputStream);
}
}</code></pre>
<p>4.3 将缓存管理器添加到SecurityManager中</p>
<pre><code class="language-java">// 注入缓存管理器;
securityManager.setCacheManager(getEhCacheManager());</code></pre>
<p>4.4 在自定义Realm中添加缓存管理器和指定授权缓存名</p>
<pre><code class="language-java">/**
* 自定义Realm
*/
@Bean
public UserRealm userRealm(EhCacheManager cacheManager)
{
UserRealm userRealm = new UserRealm();
userRealm.setAuthorizationCacheName(Constants.SYS_AUTH_CACHE);//指定授权缓存的名字:sys-authCache
userRealm.setCacheManager(cacheManager);
return userRealm;
}</code></pre>
<h1>5. 会话管理器SessionManager</h1>
<p>5.1 SessionManager</p>
<p>5.2 SessionDAO</p>
<h1>6.Shiro过滤器</h1>
<p>6.1 过滤器继承关系
<img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=23ffca77875632db98ca00f7ece8e018&amp;file=file.png" alt="" /></p>
<p>1.AbstractFilter > 2.NameableFilter > 3.OncePerRequestFilter > 4.AdviceFilter > 5.PathMatchingFilter > 6.AccessControlFilter > 7.AuthenticationFilter > 8.AuthenticatingFilter > 9.FormAuthenticationFilter</p>
<p>6.2 过滤器简介
如果一个请求被该过滤器拦截,想要到达用户最后请求的地址,那么一定得有:filterChain.doFilter(request, response);这个方法的调用,表示通过该过滤器,直接走下一个过滤器或者请求可以到达用户的请求地址。否则用户的请求地址不会被调用,用户的请求被过滤器拦截。</p>
<p>6.3 过滤器功能</p>
<ol>
<li>
<p>OncePerRequestFilter:filter的基类(就是真正实现了Filter中的doFilter方法的类),它用来保证在每个servlet容器上,每个请求都只会被过滤一次,不会重复过滤。</p>
</li>
<li>
<p>AdviceFilter:类似于开启了AOP环绕通知,提供了preHandle,postHandle和afterCompletion这三个方法。其处理拦截的逻辑是在preHandle方法中完成的。preHandle方法返回true和false代表了通过过滤请求可以到达用户的请求地址和过滤器拦截掉了用户的请求。</p>
</li>
<li>
<p>PathMatchingFilter:这个过滤器会处理指定的请求路径,和对其它路径的请求放行。如果请求需要过滤,则处理过滤的逻辑由子类实现onPreHandle完成。</p>
</li>
<li>
<p>AccessControlFilter:如果用户没有认证(即登录),那么这个过滤器就是控制访问资源和用户重定向到登录页面的过滤器的父类。当一个用户没有认证(即登录)时,可以通过saveRequestAndRedirectToLogin这个方法,重定向到登录页面。AccessControlFilter中的onPreHandle处理真正的拦截逻辑,isAccessAllowed方法验证用户是否登录,onAccessDenied处理用户没登录后的逻辑,在这个过滤器中并没有给出isAccessAllowed和onAccessDenied方法的实现,下一步得去子类中看。</p>
</li>
<li>
<p>AuthenticationFilter:实现了isAccessAllowed方法,如果用户已登录,那么过滤器将直接放行,如果用户没有登录,那么再由其子类中的onAccessDenied方法处理后续逻辑,子类需要对未验证的请求执行特定的逻辑。</p>
</li>
<li>
<p>AuthenticatingFilter:该类会尝试基于用户的请求,自动的去执行一些身份认证,提供了一些如登录,和重定向跳转的方法,并没有onAccessDenied方法的实现,那么这个实现就只能在最后一个子类FormAuthenticationFilter中完成了。</p>
</li>
<li>FormAuthenticationFilter:
<ul>
<li>要求请求用户进行身份认证,以便使请求继续,如果没有认证,则强制让该请求重定向到你配置中的登录URL(loginUrl)</li>
<li>这个构造器会构造一个UsernamePasswordToken对象,里面包含了username, password,和rememberMe这三个请求参数。当调用Subject.login(usernamePasswordToken)方法时,它会尝试自动的执行登录操作,要注意的是,这个尝试登录的操作仅仅只会在isLoginSubmission(request,response)返回true且是一个POST请求的登录操作。</li>
<li>如果尝试登录失败,则会将AuthenticationException异常写入到request的属性当中,这个属性的key是是failureKeyAttribute(这是个变量的名字,其值为shiroLoginFailure),FQCN能用作i18n的key或查找机制,向用户解释为什么会登录失败。(也就是说,如果被拦截,可以在request. getAttribute(“shiroLoginFailure”)中得到返回的错误消息)</li>
</ul></li>
</ol>
<p>6.4 Shiro过滤流程总结</p>
<p>首先过滤器会调用doFilter(在OncePerRequestFilter中)方法,然后再调用doFilterInternal方法(在AdviceFilter中),然后再调用preHandle、executeChain、postHandle(在AdviceFilter中)这3个方法,实际拦截业务在preHandle方法中,然后再调用onPreHandle(在AccessControlFilter中)方法,然后再调用isAccessAllowed(在AuthenticationFilter中)方法和onAccessDenied(在FormAuthenticationFilter中)方法。</p>
<p>6.5 Shiro已实现的过滤器以及对应的类</p>
<p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=8effa0735dc75887d59ab172e0192530&amp;file=file.png" alt="" /></p>
<p>6.x 登录人数控制过滤器(KickoutSessionFilter)-AccessControlFilter</p>
<p>6.x 验证码校验过滤器(CaptchaValidateFilter)-AccessControlFilter</p>
<p>filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");</p>