Shiro的基于token登录流程和获取会话数据(未完待续)

编程 / 2022-09-20

前言

shiro作为一个高可用的Java安全框架,其内部逻辑结构也是高度解耦的,这让我在使用的时候十分的迷惑,最近公司的需求无法使用默认jeccgboot自带的shiro的设置了,所以记录一下我在看shiro源码的一些感受和心得。

我的需求就是不使用shiro的默认登录,不使用shiro的会话控制,采用无状态会话+JWT token控制,

Shiro的登录流程

首先,我们要明白一般shiro的登录校验是基于token的,所以可以说shiro的开始其实就是传入一个token后才正式开始的,在此之前的所有操作都是编程人员自己定义的校验规则。(当然也有不用token的)

故,如何根据业务需求生成一个合适的token是使用shiro的准备工作
一般来说的常规操作就是通过JwtUtil生成器,以用户名为主值,用户加密后的密码为加密锁,生成签名token,想要解密token,就需要加密锁secret

 String token = JwtUtil.sign(username, password);

JWT的值链是我们自定义的,我这里在链中只加了username

public static String sign(String username, String secret) {
		Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
		Algorithm algorithm = Algorithm.HMAC256(secret);
		// 附带username信息
		return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);

}

想要拓展只需照葫芦画瓢加其他内容进token也是可以的,一般来说还会添加过期时间啊等待内容进去,要注意的是:token中必须要含有能获取用户的唯一性标识!
不然在shiro中无法在数据库找到用户
(注;在JWT写入过期时间,是给前端能不需要后端支持的情况下也能让用户掉线用的比较广泛)

生成好了token之后,后端也要做限制呀,shiro是不会帮你判断token是否过期的,把token存入redis。

redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);

然后在拦截请求的时候get,判断token是否过期

还需要做的准备工作就是,登录后存在Session中的登录用户数据结构,我们需要根据需求来自定义一个登录后该用户的全局信息。如下:

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="在线用户对象",description = "在线用户对象")
public class LoginUser {

    @ApiModelProperty(value = "主键")
    private String id;

    @ApiModelProperty(value = "登录账号")
    private String username;

    @ApiModelProperty(value = "真实姓名")
    private String realname;

我们先来缕清shiro的数据结构

shiro的核心的数据结构就是Subject接口与面向它的DelegatingSubject类

/*
Implementation of the Subject interface that delegates method calls to an underlying SecurityManager instance for security checks. It is essentially a SecurityManager proxy.

This implementation does not maintain state such as roles and permissions (only Subject principals, such as usernames or user primary keys) for better performance in a stateless architecture. It instead asks the underlying SecurityManager every time to perform the authorization check.

A common misconception in using this implementation is that an EIS resource (RDBMS, etc) would be "hit" every time a method is called. This is not necessarily the case and is up to the implementation of the underlying SecurityManager instance. If caching of authorization data is desired (to eliminate EIS round trips and therefore improve database performance), it is considered much more elegant to let the underlying SecurityManager implementation or its delegate components manage caching, not this class. A SecurityManager is considered a business-tier component, where caching strategies are better managed.

Applications from large and clustered to simple and JVM-local all benefit from stateless architectures. This implementation plays a part in the stateless programming paradigm and should be used whenever possible.
*/
public class DelegatingSubject implements Subject {

    private static final Logger log = LoggerFactory.getLogger(DelegatingSubject.class);

    private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
            DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";

    protected PrincipalCollection principals;
    protected boolean authenticated;
    protected String host;
    protected Session session;
    protected boolean sessionCreationEnabled;

    protected transient SecurityManager securityManager;

通过源码给出的注释我们可以发现,

该接口将方法调用委托给底层SecurityManager实例进行安全检查。它本质上是一个SecurityManager代理。此实现不维护角色和权限等状态(仅维护Subject主体,如用户名或用户主键),以便在无状态体系结构中获得更好的性能。

什么意思?我们知道shiro的安全检查是还有上权等操作的,但我们既然摒弃了shiro的默认处理,采用无状态体系,自然不需要这些默认的权限拦截,故这是shiro帮我们无状态体系做出的一个实现类,而在Subject接口中会看到很多关于权限的接口,而在DelegatingSubject中全部移交给SecurityManager处理了,只保留了主体部分

public void checkPermission(String permission) throws AuthorizationException {
        assertAuthzCheckPossible();
        securityManager.checkPermission(getPrincipals(), permission);
}

那么哪些是主体部分的成员呢?

public class DelegatingSubject implements Subject {

    private static final Logger log = LoggerFactory.getLogger(DelegatingSubject.class);

    private static final String RUN_AS_PRINCIPALS_SESSION_KEY =
            DelegatingSubject.class.getName() + ".RUN_AS_PRINCIPALS_SESSION_KEY";

    protected PrincipalCollection principals;
    protected boolean authenticated;
    protected String host;
    protected Session session;
    protected boolean sessionCreationEnabled;

    protected transient SecurityManager securityManager;

这些就是,我们一个个来探讨

第一个 PrincipalCollection

源码给出的解释很明显

/*
Returns this Subject's principals (identifying attributes) in the form of a PrincipalCollection or null if this Subject is anonymous because it doesn't yet have any associated account data (for example, if they haven't logged in).

The word "principals" is nothing more than a fancy security term for identifying attributes associated with a Subject, aka, application user. For example, user id, a surname (family/last name), given (first) name, social security number, nickname, username, etc, are all examples of a principal.
Returns:
all of this Subject's principals (identifying attributes).
*/

它就是一个Subject类型的唯一标识,等同于用户的id,它可以是任何证明唯一性的标识,如:用户id、姓名、社会保险号、昵称、用户名等都是主体的示例。

那PrincipalCollection在无状态体系中有什么用?
它可以作为我们存在redis中的key值,在后面我会再次提到

第二个 authenticated 是否被通行/授权

在采用shiro自带的登录接口的时候的是否登录成功的标识,我们不用就不用管

第三个 host

官方注释

/*
the host name or IP associated with the client who created/is interacting with this Subject.
*/

我们知道当一个用户连接进来的时候就会创建一个与其连接的Subject对象,故在Subject我们一定能看到保存连接ip的字段。

第四个 Session 这个大家都知道是什么了吧

从这里我们可以看到Session是被Subject管理的。
从subject中我们可以获取session

第五个 sessionCreationEnabled

官方注释

/*
Returns true if this Subject is allowed to create sessions, false otherwise.
*/

该Subject是否被允许创建Session

最后一个 securityManager 最终所有的东西都会移交给securityManager处理,这里提前引入是为了处理掉Subject的一些权限等接口的移交处理

至此一个连接进来之后,在shiro中保存的数据结构我们已经了解清楚了。

现在我们再来了解一下

在单个应用程序中为所有subject(即用户)执行所有安全操作的保存地SecurityManager
函数签名:

/*
The interface itself primarily exists as a convenience - it extends the Authenticator, Authorizer, and SessionManager interfaces, thereby consolidating these behaviors into a single point of reference. For most Shiro usages, this simplifies configuration and tends to be a more convenient approach than referencing Authenticator, Authorizer, and SessionManager instances separately; instead one only needs to interact with a single SecurityManager instance.
In addition to the above three interfaces, this interface provides a number of methods supporting Subject behavior. A Subject executes authentication, authorization, and session operations for a single user, and as such can only be managed by A SecurityManager which is aware of all three functions. The three parent interfaces on the other hand do not 'know' about Subjects to ensure a clean separation of concerns.
*/
public interface SecurityManager extends Authenticator, Authorizer, SessionManage

从官方注释中,我们可以知道SecurityManager是 Authenticator, Authorizer, SessionManager的拓展和总和的单一引用点,该接口还提供了许多支持Subject行为的方法,而三个父接口则不清楚有Subject的存在

先往上走看看三个父接口

第一个 Authenticator 全局认证器


/**
 * An Authenticator is responsible for authenticating accounts in an application.  It
 * is one of the primary entry points into the Shiro API.
 * <p/>
 * Although not a requirement, there is usually a single 'master' Authenticator configured for
 * an application.  Enabling Pluggable Authentication Module (PAM) behavior
 * (Two Phase Commit, etc.) is usually achieved by the single {@code Authenticator} coordinating
 * and interacting with an application-configured set of {@link org.apache.shiro.realm.Realm Realm}s.
 * <p/>
 * Note that most Shiro users will not interact with an {@code Authenticator} instance directly.
 * Shiro's default architecture is based on an overall {@code SecurityManager} which typically
 * wraps an {@code Authenticator} instance.
 *
 * @see org.apache.shiro.mgt.SecurityManager
 * @see AbstractAuthenticator AbstractAuthenticator
 * @see org.apache.shiro.authc.pam.ModularRealmAuthenticator ModularRealmAuthenticator
 * @since 0.1
 */
public interface Authenticator {

    /**
     * Authenticates a user based on the submitted {@code AuthenticationToken}.
     * <p/>
     * If the authentication is successful, an {@link AuthenticationInfo} instance is returned that represents the
     * user's account data relevant to Shiro.  This returned object is generally used in turn to construct a
     */
    public AuthenticationInfo authenticate(AuthenticationToken authenticationToken)
            throws AuthenticationException;
}

通过注释我们可以看出来,这是一个用户进来认证的接口,是进入Shiro API的主要入口点之一。它唯一的方法就是根据提交的信息对用户进行身份验证。一般用于用户登录的时候的验证,即Subject未出现的时候。

第二个 Authorizer 每一个Subject访问接口之前的权限的认证器

官方注释:

/*
Returns true if the corresponding subject/user is permitted to perform an action or access a resource summarized by the specified permission string.
This is an overloaded method for the corresponding type-safe Permission variant. Please see the class-level JavaDoc for more information on these String-based permission methods.
*/

该接口的作用就是定义每次subject访问接口的拦截
看该接口的方法会发现与subject中的权限认证的方法几乎一致

这个是Authorizer的

boolean isPermitted(PrincipalCollection principals, String permission);

这个是Subject的

boolean isPermitted(Permission permission);

可以说就是通过subject的唯一标识锁定一个subject去调用他的认证

第三个 SessionManager

/*
A SessionManager manages the creation, maintenance, and clean-up of all application
*/

SessionManager管理所有应用程序会话的创建、维护和清理。
它有2个方法

Session start(SessionContext context);

 Session getSession(SessionKey key) throws SessionException;

第一个是根据上下文数据初始化Session实例(我们用不上)
第二个是根据SessionKey获取Session
很简单的一个管理Session的管理者

至此我们知道了SecurityManager拥有了全局认证和Subject认证和会话管理的3大功能以外还拥有这创建SubJect管理Subject的能力

往上走完了开始往下走SecurityManager的继承链,看看shiro是如何通过一步步继承拓展SecurityManager的功能的

链路中的第一站!CachingSecurityManager

官方注释:

A very basic starting point for the SecurityManager interface that merely provides logging and caching support. All actual SecurityManager method implementations are left to subclasses.

它是一个最基础的开始点对于SecurityManager,它仅仅提供了日志和缓存支持。
它主要的2个成员就是

    /**
     * The CacheManager to use to perform caching operations to enhance performance.  Can be null.
     */
    private CacheManager cacheManager;

    /**
     * The EventBus to use to use to publish and receive events of interest during Shiro's lifecycle.
     * @since 1.3
     */
    private EventBus eventBus;

CacheManager 缓存管理者,常用的有redis缓存和本地缓存,我们只需要做选择就好,不用深究。

EventBus 事件巴士。类似与消息队列,一般可以通过它来发布一下异常啊,运行记录到事件中心去,对应的监听者会捕获这些信息来显示日志,这里涉及到了为什么要采用事件巴士,直接log一个日志出来。写道日志里去不就好了吗,这里就涉及到了线程阻塞的问题,日志的打印不应该阻塞到逻辑的运行,而投递一个信息到事件中心,让异步线程去处理是一个更加高效的选择。

链路中的第二站!RealmSecurityManager

官方注释:

/*
Shiro support of a SecurityManager class hierarchy based around a collection of Realms. All actual SecurityManager method implementations are left to subclasses.
*/

RealmSecurityManager就比CachingSecurityManager增加了一个对Realm的支持和Realm对上级的兼容。

这里就得提到Realm了

/*
A Realm is a security component that can access application-specific security entities such as users, roles, and permissions to determine authentication and authorization operations.
*/

Realm是一个安全组件,它可以访问特定于应用程序的安全实体,如用户、角色和权限,以确定身份验证和授权操作。
也就是我们最常用到的AuthorizingRealm和AuthenticatingRealm的最父类,这两者的区别和Authenticator, Authorizer一样。

也就是说我们能在SecurityManager中拿到Realm

链路中的第三站!AuthenticatingSecurityManager

/*
Shiro support of a SecurityManager class hierarchy that delegates all authentication operations to a wrapped Authenticator instance. That is, this class implements all the Authenticator methods in the SecurityManager interface, but in reality, those methods are merely passthrough calls to the underlying 'real' Authenticator instance.
*/

Shiro支持SecurityManager类层次结构,将所有身份验证操作委托给封装的Authenticator实例。也就是说,这个类实现了SecurityManager接口中的所有Authenticator方法,但实际上,那些方法只是对底层“真正的”Authenticator实例的直通调用。

它这里的委托和直通调用,意思就是默认的
new了一个ModularRealmAuthenticator 这个就是封装的Authenticator实例
里面内容很简单。传入Realm,拿到一个Realm后调用Realm里的关于身份验证的方法,然后不做处理直接返回处理,其实就是对Realm的调用封装,而再次只是代理了一个认证策略的东东,再去调用Realm。

链路中的第四站!AuthorizingSecurityManager

/*
Shiro support of a SecurityManager class hierarchy that delegates all authorization (access control) operations to a wrapped Authorizer instance. That is, this class implements all the Authorizer methods in the SecurityManager interface, but in reality, those methods are merely passthrough calls to the underlying 'real' Authorizer instance.
All remaining SecurityManager methods not covered by this class or its parents (mostly Session support) are left to be implemented by subclasses.
*/

Shiro支持SecurityManager类层次结构,将所有授权(访问控制)操作委托给封装的Authorizer实例。也就是说,这个类实现了SecurityManager接口中的所有Authorizer方法,但实际上,那些方法只是对底层“真正的”Authorizer实例的直通调用。

跟第三站一样,只不过换成了授权相关的方法

链路中的第五站!SessionsSecurityManager

Shiro support of a SecurityManager class hierarchy that delegates all session operations to a wrapped SessionManager instance. That is, this class implements the methods in the SessionManager interface, but in reality, those methods are merely passthrough calls to the underlying 'real' SessionManager instance.

Shiro支持SecurityManager类层次结构,将所有会话操作委托给封装的SessionManager实例。也就是说,这个类实现了SessionManager接口中的方法,但实际上,那些方法只是对底层“真正的”SessionManager实例的直通调用。

这里它所调用默认的封装的SessionManager实例是DefaultSessionManager,而DefaultSessionManager它把对Session里的数据的增删改查委托给别人(SessionDAO),只保留了对Session本身的生命管理,如创建、停用、过期、查找等等功能

Default business-tier implementation of a ValidatingSessionManager. All session CRUD operations are delegated to an internal SessionDAO.

而SessionDAO是什么呢?
Shiro提供给我们的实现类我们一看就懂了
image-1663669596690
是吧,就是选择用哪种方式保存session的数据。
我们很显然选择了redis的

链路中的第六站!DefaultSecurityManager

/*
The Shiro framework's default concrete implementation of the SecurityManager interface, based around a collection of Realms. This implementation delegates its authentication, authorization, and session operations to wrapped Authenticator, Authorizer, and SessionManager instances respectively via superclass implementation.

To greatly reduce and simplify configuration, this implementation (and its superclasses) will create suitable defaults for all of its required dependencies, except the required one or more Realms. Because Realm implementations usually interact with an application's data model, they are almost always application specific; you will want to specify at least one custom Realm implementation that 'knows' about your application's data/security model (via setRealm or one of the overloaded constructors). All other attributes in this class hierarchy will have suitable defaults for most enterprise applications.
*/

Shiro框架的SecurityManager接口的默认具体实现,基于一组realm。该实现通过超类实现将其身份验证、授权和会话操作分别委托给封装的Authenticator、Authorizer和SessionManager实例。

为了大大减少和简化配置,该实现(及其超类)将为所有必需的依赖项创建合适的默认值,必需的一个或多个realm除外。因为Realm实现通常与应用程序的数据模型交互,所以它们几乎总是特定于应用程序的;你需要指定至少一个“知道”你的应用程序的数据/安全模型的自定义领域实现(通过setRealm或一个重载构造函数)。这个类层次结构中的所有其他属性将具有适合大多数企业应用程序的默认值。

这个类里面就分成了2部分了
第一个就是对老三样 Authenticator, Authorizer, and SessionManager 开放自定义设置入口,
第二个就是回归老本行,继续对Subject的处理,经过多重划分,几乎把Subject的功能全划分出去了,这里就剩下了对subject的生命管理了.
这时候就多了几个新东西

RememberMeManager,

A RememberMeManager is responsible for remembering a Subject's identity across that Subject's sessions with the application.

RememberMeManager负责在Subject与应用程序的会话中记住Subject的标识。

SubjectDAO

/*
Subject instance can be recreated at a later time if necessary.
*/

SubjectDAO负责持久化Subject实例的内部状态,以便在以后必要时可以重新创建Subject实例。

也就是说SecurityManager把维护subject生命周期中保存Subject和删除Subject的功能委托给了SubjectDAO,自己只保留了SubjectDAO的实例
而SubjectDAO中有个成员是SessionStorageEvaluator

/*
passed around for any further requests/invocations. This effectively allows a session id to be used for any request or invocation as the only 'pointer' that Shiro needs, and from that, Shiro can re-create the Subject instance based on the referenced Session.
*/

评估Shiro是否可以使用Subject的Session来持久化Subject的内部状态。

使用Subject的会话来保存Subject的身份和身份验证状态(例如登录后)是一种常见的Shiro实现策略,这样就不需要为任何进一步的请求/调用传递信息。这有效地允许一个session id作为Shiro需要的唯一“指针”用于任何请求或调用,并且Shiro可以基于引用的session重新创建Subject实例。

也就是说,通过设置这个成员的值,我们可以做到控制Shiro是否会用Subject来包装Session,如果你不需要shiro自带的权限控制和授权,只需要它的session的功能就可以选择把它值设为false,在SubjectDAO他的存在就是一个属性,决定了Subject的功能是否被屏蔽一部分。

If sessionStorageEnabled is true (the default setting), a new session may be created to persist Subject state if necessary.
If sessionStorageEnabled is false, a new session will not be created to persist session state.
Most applications use Sessions and are OK with the default true setting for sessionStorageEnabled.

However, if your application is a purely 100% stateless application that never uses sessions, you will want to set sessionStorageEnabled to false. Realize that a false value will ensure that any subject login only retains the authenticated identity for the duration of a request. Any other requests, invocations or messages will not be authenticated.

如果sessionStorageEnabled为true(默认设置),则可以在必要时创建一个新的会话来保持Subject状态。
如果sessionStorageEnabled为false,则不会创建新的会话来保存会话状态。
大多数应用程序使用会话,并且可以使用sessionStorageEnabled的默认true设置。
但是,如果您的应用程序是一个完全100%无状态的应用程序,从不使用会话,那么您将希望将sessionStorageEnabled设置为false。请注意,false值将确保任何主题登录在请求期间只保留经过身份验证的标识。任何其他请求、调用或消息都不会被验证。

也就是说,当值为false,除了session第一次进入系统的时候会被验证,其余任何请求shiro都不会默认去认证了,但是我们可以自定义认证方式。

SubjectFactory

/*
A SubjectFactory is responsible for constructing Subject instances as needed.
*/

SecurityManager将创建Subject实例的能力委托给了SubjectFactory,它有两个实例DefaultWebSubjectFactory能通过上下文创建webSubject保留servlet请求/响应对的能力,DefaultSubjectFactory通过上下文创建DelegatingSubject实例是Subject的默认实现类。

链路中的第七站!DefaultWebSecurityManager

Default WebSecurityManager implementation used in web-based applications or any application that requires HTTP connectivity (SOAP, http remoting, etc).

在所有的基础上添加对HTTP连接的方法支持

至此我们可以明白,整个SecurityManager就是对Subject的功能的向外委托的实例总和,分别是AuthenticatingRealm、 AuthorizingRealm、SessionManager、cacheManager、SubjectDAO、SubjectFactory、RememberMeManager其中AuthenticatingRealm、 AuthorizingRealm是Realm的实例,

为什么Shiro的开发者要通过如此复杂的分配封装委托呢?其实每一层的继承,都是给我们开放自定义嵌入自己的代码的入口,比如说我自己实现一个Manager,但是我想要自己不想要AuthenticatingRealm、 AuthorizingRealm,那么我们可以自己继承Realm实现,然后manager的编写直接继承RealmSecurityManager,那么后面的继承链就不会再被我们需要了。

既然我们知道了每一个请求进来之后用户的数据保存的数据接口是怎么样之后,我们就要去寻找请求是怎么进来的,又保存在哪里,如何再次获取这个数据了。

根据上文我们知道了通过Web支持的Manager每一个请求进来都是一个Subject对象,我们再次思考Subject什么时候被创建?

这时候就引入一个新东西Filter拦截器,对于每一个http请求访问我们的系统的时候都会被shiro的拦截器拦截这个拦截器的基类是OncePerRequestFilter,这个我们就不深究拦截器的具体实现了。

OncePerRequestFilter

 * Filter base class that guarantees to be just executed once per request,
 * on any servlet container. It provides a {@link #doFilterInternal}
 * method with HttpServletRequest and HttpServletResponse arguments.
 * <p/>

Filter基类,保证每个请求只执行一次,
在任何servlet容器上。它提供了一个{@link #doFilterInternal}
方法中包含HttpServletRequest和HttpServletResponse参数。

作为Filter的基类它最重要的方法肯定是doFilter

public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
        if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {
            log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", getName());
            filterChain.doFilter(request, response);
        } else //noinspection deprecation
            if (/* added in 1.2: */ !isEnabled(request, response) ||
                /* retain backwards compatibility: */ shouldNotFilter(request) ) {
            log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.",
                    getName());
            filterChain.doFilter(request, response);
        } else {
            // Do invoke this filter...
            log.trace("Filter '{}' not yet executed.  Executing now.", getName());
            request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

            try {
                doFilterInternal(request, response, filterChain);
            } finally {
                // Once the request has finished, we're done and we don't
                // need to mark as 'already filtered' any more.
                request.removeAttribute(alreadyFilteredAttributeName);
            }
        }
    }

源码里有一个if判断,当http的请求的属性中的alreadyFilteredAttributeName是否为空,这个就是判断这个请求是否是第一次进来的依据,如果不是第一次进来,那肯定是继续走过滤器链看看是否放行还是阻塞啦,然后进到我们的业务逻辑。

重点是第一次进来的时候,shiro它做了什么。

 	    request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);

            try {
                doFilterInternal(request, response, filterChain);
            } finally {
                // Once the request has finished, we're done and we don't
                // need to mark as 'already filtered' any more.
                request.removeAttribute(alreadyFilteredAttributeName);
            }

从这里看到,它先设置这个请求已经不是第一次进来了,然后调用了一个doFilterInternal,这是一个OncePerRequestFilter的抽象方法,说明它把它交给了他的子类来实现了,走我们去看看。

AbstractShiroFilter
其他我暂时不管就看这个doFilterInternal的实现


    /**
     * {@code doFilterInternal} implementation that sets-up, executes, and cleans-up a Shiro-filtered request.  It
     * performs the following ordered operations:
     * <ol>
     * <li>{@link #prepareServletRequest(ServletRequest, ServletResponse, FilterChain) Prepares}
     * the incoming {@code ServletRequest} for use during Shiro's processing</li>
     * <li>{@link #prepareServletResponse(ServletRequest, ServletResponse, FilterChain) Prepares}
     * the outgoing {@code ServletResponse} for use during Shiro's processing</li>
     * <li> {@link #createSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) Creates} a
     * {@link Subject} instance based on the specified request/response pair.</li>
     * <li>Finally {@link Subject#execute(Runnable) executes} the
     * {@link #updateSessionLastAccessTime(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} and
     * {@link #executeChain(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)}
     * methods</li>
     * </ol>
     * <p/>
     * The {@code Subject.}{@link Subject#execute(Runnable) execute(Runnable)} call in step #4 is used as an
     * implementation technique to guarantee proper thread binding and restoration is completed successfully.
     *
     * @param servletRequest  the incoming {@code ServletRequest}
     * @param servletResponse the outgoing {@code ServletResponse}
     * @param chain           the container-provided {@code FilterChain} to execute
     * @throws IOException                    if an IO error occurs
     * @throws javax.servlet.ServletException if an Throwable other than an IOException
     */
    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
            throws ServletException, IOException {

        Throwable t = null;

        try {
            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);

            final Subject subject = createSubject(request, response);

            //noinspection unchecked
            subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });
        } catch (ExecutionException ex) {
            t = ex.getCause();
        } catch (Throwable throwable) {
            t = throwable;
        }

        if (t != null) {
            if (t instanceof ServletException) {
                throw (ServletException) t;
            }
            if (t instanceof IOException) {
                throw (IOException) t;
            }
            //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
            String msg = "Filtered request failed.";
            throw new ServletException(msg, t);
        }
    }

dofilter设置、执行和清理shiro过滤请求的内部实现。它执行以下顺序操作:
准备传入的ServletRequest以便在Shiro处理期间使用
准备输出的ServletResponse以便在Shiro处理期间使用
基于指定的请求/响应对创建Subject实例。
最后执行updateSessionLastAccessTime(ServletRequest, ServletResponse)和executeChain(ServletRequest, ServletResponse, FilterChain)方法

通过阅读源码,我们可以看到这里就是Subject类绑定到线程当中去的地方了。
首先它根据request和response创建了一个Subject实例

 final Subject subject = createSubject(request, response);

然后将其绑定进入线程

 subject.execute(new Callable() {
                public Object call() throws Exception {
                    updateSessionLastAccessTime(request, response);
                    executeChain(request, response, chain);
                    return null;
                }
            });

点进execute的实现看看

 public <V> V execute(Callable<V> callable) throws ExecutionException {
        Callable<V> associated = associateWith(callable);
        try {
            return associated.call();
        } catch (Throwable t) {
            throw new ExecutionException(t);
        }
    }

点金associateWith看看

    public <V> Callable<V> associateWith(Callable<V> callable) {
        return new SubjectCallable<V>(this, callable);
    }

再进去 SubjectCallable

public class SubjectCallable<V> implements Callable<V> {

    protected final ThreadState threadState;
    private final Callable<V> callable;

    public SubjectCallable(Subject subject, Callable<V> delegate) {
        this(new SubjectThreadState(subject), delegate);
    }

    protected SubjectCallable(ThreadState threadState, Callable<V> delegate) {
        if (threadState == null) {
            throw new IllegalArgumentException("ThreadState argument cannot be null.");
        }
        this.threadState = threadState;
        if (delegate == null) {
            throw new IllegalArgumentException("Callable delegate instance cannot be null.");
        }
        this.callable = delegate;
    }

    public V call() throws Exception {
        try {
            threadState.bind();
            return doCall(this.callable);
        } finally {
            threadState.restore();
        }
    }

    protected V doCall(Callable<V> target) throws Exception {
        return target.call();
    }
}

SubjectCallable是一个实现了Callable接口的实现类,通过这个对象我们调用call()方法可以启动一个异步线程。
我们可以看到这个Callable对象保存了一个线程状态SubjectThreadState

public class SubjectThreadState implements ThreadState {

    private Map<Object, Object> originalResources;

    private final Subject subject;
    private transient SecurityManager securityManager;

    /**
     * Creates a new {@code SubjectThreadState} that will bind and unbind the specified {@code Subject} to the
     * thread
     *
     * @param subject the {@code Subject} instance to bind and unbind from the {@link ThreadContext}.
     */
    public SubjectThreadState(Subject subject) {
        if (subject == null) {
            throw new IllegalArgumentException("Subject argument cannot be null.");
        }
        this.subject = subject;

        SecurityManager securityManager = null;
        if ( subject instanceof DelegatingSubject) {
            securityManager = ((DelegatingSubject)subject).getSecurityManager();
        }
        if ( securityManager == null) {
            securityManager = ThreadContext.getSecurityManager();
        }
        this.securityManager = securityManager;
    }

    /**
     * Returns the {@code Subject} instance managed by this {@code ThreadState} implementation.
     *
     * @return the {@code Subject} instance managed by this {@code ThreadState} implementation.
     */
    protected Subject getSubject() {
        return this.subject;
    }

    /**
     * Binds a {@link Subject} and {@link org.apache.shiro.mgt.SecurityManager SecurityManager} to the
     * {@link ThreadContext} so they can be retrieved later by any
     * {@code SecurityUtils.}{@link org.apache.shiro.SecurityUtils#getSubject() getSubject()} calls that might occur
     * during the thread's execution.
     * <p/>
     * Prior to binding, the {@code ThreadContext}'s existing {@link ThreadContext#getResources() resources} are
     * retained so they can be restored later via the {@link #restore restore} call.
     */
    public void bind() {
        SecurityManager securityManager = this.securityManager;
        if ( securityManager == null ) {
            //try just in case the constructor didn't find one at the time:
            securityManager = ThreadContext.getSecurityManager();
        }
        this.originalResources = ThreadContext.getResources();
        ThreadContext.remove();

        ThreadContext.bind(this.subject);
        if (securityManager != null) {
            ThreadContext.bind(securityManager);
        }
    }

    /**
     * {@link ThreadContext#remove Remove}s all thread-state that was bound by this instance.  If any previous
     * thread-bound resources existed prior to the {@link #bind bind} call, they are restored back to the
     * {@code ThreadContext} to ensure the thread state is exactly as it was before binding.
     */
    public void restore() {
        ThreadContext.remove();
        if (!CollectionUtils.isEmpty(this.originalResources)) {
            ThreadContext.setResources(this.originalResources);
        }
    }

    /**
     * Completely {@link ThreadContext#remove removes} the {@code ThreadContext} state.  Typically this method should
     * only be called in special cases - it is more 'correct' to {@link #restore restore} a thread to its previous
     * state than to clear it entirely.
     */
    public void clear() {
        ThreadContext.remove();
    }
}

这就是一个将subject对象封装成一个可以被保存进入线程资源中的一个适配对象

当SubjectCallable的call方法被激活后会调用threadState.bind()方法

 public V call() throws Exception {
        try {
            threadState.bind();
            return doCall(this.callable);
        } finally {
            threadState.restore();
        }
    }
    public void bind() {
        SecurityManager securityManager = this.securityManager;
        if ( securityManager == null ) {
            //try just in case the constructor didn't find one at the time:
            securityManager = ThreadContext.getSecurityManager();
        }
        this.originalResources = ThreadContext.getResources();
        ThreadContext.remove();

        ThreadContext.bind(this.subject);
        if (securityManager != null) {
            ThreadContext.bind(securityManager);
        }
    }

这样线程的资源池中就会有SecurityManager对象

所谓的绑定就是再每一个线程的map当中put一个objec对象

    public static void bind(Subject subject) {
        if (subject != null) {
            put(SUBJECT_KEY, subject);
        }
    }

这样我们就可以再业务逻辑中获取到线程资源池即map中的资源了

至此,我们已经了解完了shiro的会话资源的创建和获取了

至此我们正式开始shiro的登录
当我们通过登录接口,获取到了token之后,前端会将token放入header参数头中,每次访问后端接口,都会验证这个token来进行拦截

如何进行拦截?

首先我们自定义的拦截方法,缓存机制等等都是保存在shiro的 SecurityManager中的,我们使用的是http协议就会使用到DefaultWebSecurityManager这个类,在这个类一长串的继承链中,我们目前只需要知道,他的成员里包含了ShiroRealm 即 AuthorizingRealm,shiroRealm就是我们重写AuthorizingRealm的supports、doGetAuthorizationInfo、doGetAuthenticationInfo、clearCache的自定义类。拦截重点就在这4个方法中

粤ICP备2022112743号 粤公网安备 44010502002407号