Spring Security 分布式并发 session 控制

Spring Security 分布式并发 session 控制

  Spring Security 分布式并发 session 控制:Spring Security 概述、基于 RBAC 的权限模型,身份认证流程、访问授权流程、分布式配置、分布式并发 session 控制、相关源码。


—— 2025 年 7 月 22 日 甲辰年六月二十八 大暑

版本

  • jdk:17
  • spring:6.2.6
  • spring boot:3.4.5
  • spring security:6.4.5

目录

1 概述

1.1 Spring Security

1.1.1 定义

  Spring Security 是一个功能强大且高度可定制的 身份认证访问控制 权限框架。其源于 Spring 社区,可与 Spring 家族内其它产品无缝集成,如 Spring Boot、Spring Session 等。关于其更多详细信息可查看 Spring Security 官方文档

  • 认证(Authentication):你是谁?即验证用户身份,能否登录该系统。如根据用户名密码验证、短信验证码验证、Token 令牌验证等。
  • 授权(Authorization):你能做什么?即对一个已经通过认证的用来说,他可以执行哪些操作。如可看到哪些菜单或页面、可点击哪些按钮、可访问哪些数据等。
  • 与 Sa-Token 相比:
    • Sa-Token 为轻量级权限框架,Spring Security 为重量级权限框架。
    • Sa-Token 开箱即用,Spring Security 高度可定制。
1.1.2 核心特性
  • 1、身份认证(Authentication)
    • 用户名密码认证:基于用户名密码的身份认证,即表单认证,是最基本也是应用最广泛的认证方式。
    • JWT 认证:用于 RESTful API 的无状态认证。
    • OAuth 2.0 支持:支持第三方平台登录应用(如使用 GitHub、Gitee、微信、支付宝等第三方平台登录),也支持应用作为授权服务器为其它平台颁发令牌。
    • 记住我模式:通过持久化或加密 token 实现长时间免登录。
  • 2、访问授权(Authorization)
    • 请求级授权:基于请求 URL 模式进行拦截和控制。
    • 方法级授权:通过在 service 层的方法上添加 @PreAuthorize@PostAuthorize 等注解进行精细化授权。
    • 领域对象级授权:用于更复杂的、基于领域对象(Domain Object)的权限控制。如,判断用户是否有权删除某篇特定的文章,而不是是否有权删除文章。
  • 3、会话管理(SessionManagement)
    • 提供会话固定保护与防止会话劫持。
    • 并发会话控制:限制同账号同时登录数量。
  • 4、防护常见攻击(Security Protection)
    • CSRF:默认开启对跨站请求伪造(Cross-Site Request Forgery)的防护。
    • CORS:提供对跨域资源共享(Cross-Origin Resource Sharing)的灵活配置。
    • 安全头部:自动注入一系列 HTTP 安全头部。如,X-Content-Type-Options(防止 MIME 类型混淆攻击)、Strict-Transport-Security(强制使用 HTTPS)、X-Frame-Options(防止点击劫持)、X-XSS-Protection(抑制反射型 XSS 攻击)。
  • 5、与 Spring 生态无缝集成
    • 可与 Spring MVC、Spring Boot、Spring Data、Spring Session 等框架无缝集成。
  • 6、高度可定制化
    • Spring Security 通过一系列过滤器链来实现权限控制。其几乎每一个默认行为都可通过实现或扩展特定接口来覆盖或自定义,以满足任何特殊业务场景。

1.2 阅前思考

  阅读下文前请先思考以下问题:

  • RBAC 权限模型如何实现?
  • 认证流程是怎样的?
  • 授权流程是怎样的?
  • 如何实现分布式适配?
  • 并发 session(单账号多登录)如何控制?
  • 服务重启如何对用户无感知?

2 RBAC 权限模型

2.1 RBAC 概述

  RBAC(Role-Based Access Control)即基于角色的访问控制。它是一种策略无关的访问控制模型,即用户与权限不直接关联,而是通过角色间接关联。其核心模型为:

  • 用户(User):系统使用者、登录账号。
  • 角色(Role):一组权限,表示一类用户所有用的职责、职能或权限。如 管理员、业务员、访客等。
  • 权限(Perm):对系统资源的操作许可,一般分为数据权限和操作权限。如菜单、页面、按钮、数据等。

  其核心模型关系可概括为:用户 -> 分配 -> 角色 -> 分配 -> 权限。即 用户与角色 和 角色与权限 都是多对多的关系。

  当然,RBAC 权限模型有着具体的演进过程,如基础模型、角色继承、约束模型和统一模型等,详细信息可自行了解。

2.2 RBAC 设计

  一般情况下,通过 权限表、角色表、用户表、角色-权限关系表、用户-角色关系表 这五张表来实现 RBAC 权限模型。其各核心字段说明如下:

  • 权限表(sys_perm):

    字段名中文描述备注
    id主键推荐自增
    parent_id父节点(父权限)id根节点可为空或默认为 0
    perm_name权限名称如菜单名称、页面名称、按钮名称、接口名称等
    perm_type权限类型如 0:菜单;1:操作;2:资源
    access_code访问标识一般用于前端控制菜单、页面、按钮等组件的显隐,唯一
    api_urlapi 地址数据接口 path,一般用于数据权限
  • 角色表(sys_role):

    字段名中文描述备注
    id主键推荐自增
    role_code角色编码唯一
    role_name角色名称
  • 用户表(sys_user):

    字段名中文描述备注
    id主键自增
    account账号/用户名唯一
    password密码加密存储
    account_type账号类型如 0:超级管理员;1:管理员;2:普通
  • 角色权限关系表(sys_role_perm):

    字段名中文描述备注
    id主键自增
    role_id角色 id
    perm_id权限 id
  • 用户角色关系表(sys_user_role):

    字段名中文描述备注
    id主键自增
    user_id用户 id / 账号 id
    role_id角色 id

3 认证流程

3.1 认证流程图

  Spring Security 框架中根据用户名密码进行身份认证的流程图如下:

authentication-process

3.2 认证流程详述

  现阶段大多数系统为前后端分离模式,故本例默认为前后端分离,如有特殊之处会进行说明。

3.2.1 表单登录配置器 FormLoginConfigurer

  在表单登录(即用户名密码登录)场景下,Spring Security 提供了表单登录配置器 FormLoginConfigurer 来支持定制化表单登录配置。如配置登录接口地址/登录页面地址、表单参数、认证成功处理器、认证失败处理器等。其核心配置项如下:

  • loginProcessingUrl:登录地址配置。支持两种配置方式。
    • 在前后端分离模式下,其为登录接口地址,如 /admin/user/login,实际上服务端并不需要定义该接口,但需要指定一个地址,来标识登录逻辑的入口,当表单提交至该地址时,系统会自动执行内置登录处理逻辑。
    • 在 Spring MVC 环境下,该地址将映射到登录模板,如 /admin/login,即我们需要定义一个 path 为 /admin/logincontroller,也需要创建一个对应的登录模板。
  • usernameParameter:用户名参数名配置。可通过该配置指定用户名参数名,默认为 username
  • passwordParameter:密码参数名配置。可通过该配置指定密码参数名,默认为 password
  • successHandler:认证成功处理器配置。其接受一个 AuthenticationSuccessHandler 实例,可用来指定认证成功后处理逻辑,如将认证信息响应给前端。
  • failureHandler:认证失败处理器配置。其接受一个 AuthenticationFailureHandler 实例,可用来指定认证失败后的处理逻辑,如可在该处理器中处理认证异常信息,将具体异常对应的信息响应给前端,比如用户名或密码错误、账号被锁定、账号被禁用等。

  注:示例代码见 [7.11 SecurityConfig 章节](#7.11 SecurityConfig)。

3.2.2 用户名密码认证过滤器 UsernamePasswordAuthenticationFilter

  用户名密码认证过滤器 UsernamePasswordAuthenticationFilter 负责处理用户名密码认证,即当用户点击登录调用 /admin/user/login 接口时会进入该过滤器。

  首先会调用 UserDetailsService#loadUserByUsername() 方法根据表单提交的用户名获取用户数据。该接口默认从内存中获取内置的默认用户(默认情况下在服务启动时,会在控制台输出内置的用户名和密码),故一般情况下我们需要自定义实现 UserDetailsService 接口,来从数据库中获取用户数据,一般也需要返回用户角色(权限)信息,以便前端进行组件显隐的渲染。若未获取到用户数据则可抛出用户名不存在异常 UsernameNotFoundException

  获取到用户信息后框架会自动进行密码校验,若校验未通过则会抛出密码错误异常 BadCredentialsException

  认证通过后会将认证信息封装为 Authentication 对象,并进行认证信息的存储和认证 session 的处理。

3.2.3 认证信息存储

  认证通过后,会讲认证信息包装为安全上下文 SecurityContext 对象,并分别存储到安全安全上下文持有器 SecurityContextHolderStrategy 和安全上下文存储器 SecurityContextRepository 中。

  SecurityContextHolderStrategy 中存储的认证信息主要服务于当前线程,其基于 ThreadLocal 实现。即当接收到请求后会从 SecurityContextRepository 中获取安全上下文信息,并存储但当前 web 线程中,以便在当前线程运行时获取安全上下文信息,请求处理完毕后会清除其存储的安全上下文信息。

  SecurityContextRepository 中存储的认证信息服务于整个应用程序的生命周期,默认情况下其使用基于内存的实现。在程序运行期间,用户登录成功后,其安全上下文信息会一直保存在该存储器中,后续的访问授权也是基于该存储器中的信息。服务停止时,信息会被释放,当然也可依赖 redis 等外部存储介质存储。

  注:示例代码见 [7.3 SecurityContextRepository 章节](#7.3 SecurityContextRepository)。

3.2.4 session 认证策略 SessionAuthenticationStrategy

  session 认证策略主要是指在认证时关于 session 的一些操作,如 session 安全、并发 session 控制和 session 存储等。在下文 [6.1 认证时并发 session 处理流程](#6.1 认证时并发 session 处理流程) 章节中会详细描述。

3.2.5 认证成功处理器 AuthenticationSuccessHandler

  AuthenticationSuccessHandler 用来实现认证成功后的一些操作,一般需要自定义,如将认证信息(用户名、用户角色/权限等)响应给前端等。

  注:示例代码见 [7.7.1 AuthenticationSuccessHandler 章节](#7.7.1 AuthenticationSuccessHandler)。

3.2.6 认证失败处理器 AuthenticationFailureHandler

  AuthenticationFailureHandler 用来实现认证失败后的一些操作,一般需要自定义,如可在该处理器中处理认证异常信息,将具体异常对应的信息响应给前端,比如用户名或密码错误、账号被锁定、账号被禁用等。

  注:示例代码见 [7.7.1 AuthenticationFailureHandler 章节](#7.7.2 AuthenticationFailureHandler)。

4 授权流程

4.1 授权流程图

  Spring Security 框架中请求级访问授权流程图如下:

authorization-process

4.2 授权流程详述

4.2.1 初始化资源角色元数据 MySecurityMetedataSource

  为了方便访问授权和减少数据库访问,我们可以在服务启动时将每个资源和访问该资源所需的角色的映射关系数据加载到内存中,如 Map<String, Set<String>> 这种结构,其中 key 为资源标识(如 /admin/user/list),value 为访问该资源所需的角色集合(如 ["SUPER-ADMIN", "ADMIN"])。

  注:示例代码见 [7.4.1 MySecurityMetedataSource 章节](#7.4.1 MySecurityMetedataSource)。

4.2.2 授权过滤器 AuthorizationFilter

  Spring Security 过滤器链中的 AuthorizationFilter 负责访问授权处理,即当请求到达后,在经过该过滤器时会进行权限校验。

  首先需要获取包含当前登录用户认证信息 Authentication 的安全上下文 SecurityContext 数据。先从线程安全上下文持有器 SecurityContextHolderStrategy 中获取,若未获取到则从全局安全上下文存储器 SecurityContextRepository 中获取,获取到则返回认证信息 Authentication,否则会创建一个拥有匿名角色 ROLE_ANONYMOUS 的认证对象。

  之后便会通过授权管理器 AuthorizationManager 进行权限校验。

4.2.3 授权管理器 AuthenticationManager

  Spring Security 提供了授权管理器 AuthenticationManager 接口来支持定制化的授权流程实现。所以一般情况下我们需要自定义实现该接口,并实现其定义的 check() 方法。示例如下:

  • 先获取到请求路径,判断其是否为登录路径,若是则授权通过(因为登录操作不需要权限限制)。
  • 然后从资源角色元数据 MySecurityMetedataSource 中根据请求路径获取到访问该资源需要的角色列表,判断该列表是否为空。若不为空则说明该资源受限,否则说明该资源不受限则授权通过返回。
  • 若该资源受限,则判断当前用户认证信息 Authenticaion 是否为匿名角色(即判断当前用户是否登录)。若是则说明当前用户未认证,此时可抛出未认证异常 InsufficientAuthenticationException;否则进行具体的角色校验。
  • 执行到这里,说明当前访问的资源受限,且当前用户已认证,则只需要判断当前用户认证信息中的角色列表是否包含访问该资源需要的角色列表中的一个角色即可。若包含则授权通过,否则抛出访问拒绝异常 AssceeDeniedException

  注:示例代码见 [7.4.2 AuthorizationManager 章节](#7.4.2 AuthorizationManager)。

4.2.4 异常转换处理器 ExceptionTranslationFilter

  异常转换处理器 ExceptionTranslationFilter 是 Spring Security 提供的用来处理所有认证和授权异常的过滤器,所以我们在授权过程中抛出的未认证异常 InsufficientAuthenticationException 和访问拒绝异常 AccessDeniedException 会在该过滤器中被处理。

  其判断当前异常实例是否为认证异常 AuthenticationExceptionInsufficientAuthenticationException 为其子类)。若是则执行认证入口点处理器 AuthenticationEntryPoint,一般需要自定义实现该接口,可自定义向前端响应未认证相关信息,如:请先登录,前端可根据此信息跳转到登录页面。

  然后判断当前异常实例是否为访问拒绝异常 AccessDeniedException,若是则执行访问拒绝处理器 AccessDeniedHandler,一般需要自定义实现该处理器,可在该处理器中将访问拒绝信息响应给前端,如:权限不足!

4.2.5 授权时 session 处理

  授权过程中也需要对相关 session 进行处理,在下文 [6.2 请求时并发 session 处理流程](#6.2 请求时并发 session 处理流程) 章节中会详细描述。

5 分布式配置

  分布式配置是指当应用程序多节点部署时,关于认证信息数据(安全上下文 SecurityContext)和 session 相关数据的处理。在默认情况下这两类数据皆存储于内存中,显然不适用分布式系统,故我们需要通过外部存储介质来统一存储和管理这两类数据。一般需要借助 redis 实现。

5.1 安全上下文存储器 SecurityContextRepository

  Spring Security 提供了安全上下文存储器 SecurityContextRepository 接口来存储和管理安全上下文信息,所有我们只需要自定义实现该接口即可。

  注:示例代码见 [7.3 SecurityContextRepository 章节](#7.3 SecurityContextRepository)。

5.2 会话注册注册器 SessionRegistry

  Spring Security 提供了会话注册器 SessionRegistrty 接口来存储和管理 session 相关信息,所以我们只需要自定义实现该接口即可。

  注:示例代码见 [7.2.2 SessionRegistry 章节](#7.2.2 SessionRegistry)。

5.3 TokenSessionFilter

  为了解决服务重启浏览器刷新后 session 重新创建问题,我们引入了 token(并非 JWT,只是用来代替 sessionId 的字符串而已)。通过存储 token-sessionId 和 sessionId-token 的映射关系来实现。

  通过自定义过滤器 TokenSessionFilter 来实现,其流程说明见 [6.3.2 请求时 session 处理流程详述章节](#6.3.2 请求时 session 处理流程详述) ,示例代码见 [7.5 TokenSessionFilter 章节](#7.5 TokenSessionFilter)。

6 分布式并发 session 控制

6.1 会话管理配置器 SessionManagementConfigurer

  Spring Security 提供了会话管理配置器 SessionManagementConfigurer 来支持定制化的会话管理相关配置。如配置最大会话数、指定会话注册器、配置过期会话策略等。其核心配置项如下:

  • maximumSessions:最大会话数。用来配置一个账号允许的同时在线数量,即并发 session。
  • sessionRegistry:会话注册器。用来指定会话注册器。
  • expiredSessionStrategy:过期会话策略。用来指定过期会话策略,即当某个会话过期后要执行的逻辑。

6.2 认证时并发 session 处理流程

6.2.1 认证时并发 session 处理流程图

  Spring Security 框架中身份认证时并发 session 处理流程图如下:

authentication-session-proces

6.2.2 认证时并发 session 处理流程详述
  • 认证处理过滤器 AbstractAuthenticationProcessingFilter

      为 UsernamePasswordAuthenticationFilter 的父类,认证时(登录)会进入该过滤器,在认证成功后通过会话认证策略 SessionAuthenticationStrategy 来处理认证过程中的 session。

  • 会话认证策略 SessionAuthenticationStrategy

      内置了多个实现,并在 AbstractAuthenticationProcessingFilter 中持有了一个组合会话认证策略 CompositeSessionAuthenticationStrategy 实例,该实例维护了多个策略,如并发会话控制认证策略 ConcurrentSessionControlAuthenticationStrategy、注册器会话认证策略 RegisterSessionAuthenticationStrategy,这些策略会被逐一执行。

  具体会话认证过程如下:

  • 获取最大会话数,通过会话注册器 SessionRegistry 根据认证信息从 redis 中获取登录当前账号的所有有效 session 信息(记作 sessions 数组)。
  • 判断已登录有效 session 数量是否小于允许的最大会话数。
  • 若小于,则通过会话注册器 SessionRegistry 将相关 session 信息存储至 redis,如 sesion 信息、token-sessionId 映射关系、sessionId-token 映射关系、登录当前账号的 sessionId 集合等。
  • 若不小于,则从 sessions 数组中找到最近最少使用的 session,将其置为失效。

6.3 请求时并发 session 处理流程

6.3.1 请求时并发 session 处理流程图

  Spring Security 框架中请求时并发 session 处理流程图如下:

request-session-process

6.3.2 请求时 session 处理流程详述
  • 进入 token-session 过滤器 TokenSessionFilter 进行 token-sessionId 映射关系处理。
    • 首先判断请求中是否携带 token 和 sessionId。若未携带则说明该请求不需要授权,直接放过;若携带则执行一些逻辑。
    • 根据 token 从 redis 的 token-session 映射关系中获取 旧 sessionId 记为 cacheSessionId
    • 判断 cacheSessionId 是否不为空、cacheSessionIdsessionId 是否相等、redis 中是否存在 session-token 映射关系。若是则说明服务未重启,进入 ConcurrentSessionFilter;否则执行以下逻辑。
    • 判断 cacheSessionId 是否为空。若是则将 token-sessionId 和 sessionId-token 映射关系存储至 redis;若不是则执行以下逻辑。
    • 判断 cacheSessionIdsessionId 是否相等,若相等则进入 ConcurrentSessionFilter,否则执行以下逻辑。
    • 将 redis 中登录当前账号的 sessionId 集合中的当前 token 对应的 sessionId 更新为 sessionId;更新 redis 中当前 token 对应的 session 信息;根据 sessionId 更新 redis 中 token-sessionId 映射关系;根据 cacheSessionId 移除 redis 中旧的 sessionId-token 映射关系;向 redis 添加新的 session-token 映射关系。
  • 进入并发会话过滤器 ConcurrentSessionFilter 进行并发会话处理。
    • 判断当前 session 信息是否有效。若是则更新 redis 中该 session 的最后操作时间,流程结束;否则执行以下逻辑。
  • 执行登出处理器 LogoutHanlder
    • 一般需要自定义实现,在处理器中移除 redis 中该 token 对应的认证信息和 session 信息。
  • 执行会话过期策略 SessionInformationExpiredStrategy
    • 一般需要自定义实现,将相关信息响应给前端,如:账号在其它设备登录,请重新登录!

7 源码

  部分非关键性源码未列出,如 APIResponseResponseCodeResponseUtilsGlobalException 等,若未有问题请留言!

7.1 UserDetailsVo 与 UserDetailsService

7.1.1 UserDetailsVo
// UserDetails 为 Spring Security 内置接口 必须实现
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserDetailsVo implements UserDetails {

    private Long id;   // 用户 id
    private String account;   // 账号
    private String password;   // 密码
    private String token;   // token
    private List<String> roleCodes;   // 角色编码列表
    private List<String> accessCodes;   // 访问标识(前端用于控制菜单、页面、按钮等组件的显隐)

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>(this.roleCodes.size());
        this.roleCodes.forEach(role -> authorities.add(new SimpleGrantedAuthority(role)));
        return authorities;
    }

  	// 在 Spring Security 中认证信息属于敏感信息 默认情况下不支持序列化 故需自定义
    public static class UserDetailsVoDeserializer extends JsonDeserializer<UserDetailsVo> {

        @Override
        public UserDetailsVo deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {

            ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
            JsonNode jsonNode = mapper.readTree(jsonParser);

            long id = getJsonNode(jsonNode, "id").asLong();
            String account = getJsonNode(jsonNode, "account").asText();
            String password = getJsonNode(jsonNode, "password").asText("");
            String token = getJsonNode(jsonNode, "token").asText();
            List<String> roleCodes = mapper.convertValue(getJsonNode(jsonNode, "roleCodes"), new TypeReference<>() {});
            List<String> accessCodes = mapper.convertValue(getJsonNode(jsonNode, "accessCodes"), new TypeReference<>() {});

            return UserDetailsVo.builder()
                    .id(id)
                    .account(account)
                    .password(password)
                    .token(token)
                    .roleCodes(roleCodes)
                    .accessCodes(accessCodes)
                    .build();
        }

        private JsonNode getJsonNode(JsonNode jsonNode, String field) {
            return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance();
        }
    }

    @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
    @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE,
            isGetterVisibility = JsonAutoDetect.Visibility.NONE)
    @JsonDeserialize(using = UserDetailsVoDeserializer.class)
    public abstract static class UserDetailsVoMixin {}
}
7.1.2 UserDetailsService
// UserDetailsService 为 Spring Security 内置接口 必须实现
@Service
@RequiredArgsConstructor
public class MyUserDetailsService implements UserDetailsService {

    private final UserMapper userMapper;

    @Override   // 根据用户名获取用户信息 认证时调用
    public UserDetailsVo loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = this.userMapper.selectOne(new LambdaQueryWrapper<UserEntity>()
                .eq(UserEntity::getAccount, username));
        if (user == null) {
            throw GlobalException.error(ResponseCode.ACCOUNT_DOES_NOT_EXIST);
        }

        UserDetailsVo vo = UserDetailsVo.builder()
                .id(user.getId())
                .account(user.getAccount())
                .password(PASSWORD_ID + user.getPassword())
                .token(user.getId() + StringEnum.SIGN_2.getCode() + UUID.randomUUID().toString()
                        .replaceAll(StringEnum.SIGN_3.getCode(), StringEnum.SIGN_4.getCode()))
                .build();

      	// 设置用户角色 和 访问标识 列表 请自行实现
        vo.setRoleCodes();
        vo.setAccessCodes();
      
        return vo;
    }
}

7.2 SessionInformation 与 SessionRegistry

7.2.1 SessionInformation
// SessionInformation 为内置类 需要通过扩展来实现 session 过期逻辑
public class MySessionInformation extends SessionInformation {

    private boolean expireState;

    public MySessionInformation(Object principal, String sessionId, Date lastRequest, boolean expireState) {
        super(principal, sessionId, lastRequest);
        this.expireState = expireState;
    }

    @Override   // 覆盖其原有 session 过期逻辑 session 过期时调用
    public void expireNow() {
        String sessionId = getSessionId();
        MySessionInformation sessionInformation = MySessionRegistry.getSessionInformation(sessionId, null);
        if (sessionInformation != null) {
            super.expireNow();
            sessionInformation.setExpireState(true);
            MySessionRegistry.updateSessionState(sessionInformation);
        }
    }

    @Override
    public boolean isExpired() {
        return this.expireState;
    }

    public boolean isExpireState() {
        return expireState;
    }

    public void setExpireState(boolean expireState) {
        this.expireState = expireState;
    }
}
7.2.2 SessionRegistry
@Slf4j
@Component
public class MySessionRegistry implements SessionRegistry {

    // string   key: token   value: SessionInformation
    private static final String SESSION_INFORMATION_PREFIX = "security:session:information:";

    // string   key: Principal   value: sessionIds
    private static final String SESSION_SESSIONS_PREFIX = "security:session:sessions:";

    // string   key: token   value: sessionId
    private static final String TOKEN_SESSION_PREFIX = "security:session:token_session:";

    private static final String SESSION_TOKEN_PREFIX = "security:session:session_token:";

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private static RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        MySessionRegistry.redisTemplate = redisTemplate;
    }

    @Override   // 获取所有认证信息 该方法并不会被调用
    public List<Object> getAllPrincipals() {
        return new ArrayList<>(RedisUtils.getSetValue(redisTemplate, SESSION_SESSIONS_PREFIX));
    }

    @Override   // 根据认证信息从 redis 中获取登录当前账号的所有有效 session 信息
    public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) {
        if (principal == null) {
            return new ArrayList<>();
        }

        Set<Object> sessionIds = this.getSessionIds(principal);
        if (CollectionUtils.isEmpty(sessionIds)) {
            return new ArrayList<>();
        }

        SessionInformation information;
        List<SessionInformation> list = new ArrayList<>();
        for (Object sessionId : sessionIds) {
            if (sessionId == null) {
                continue;
            }

            information = getSessionInformation(sessionId.toString());
            if (information == null) {
                continue;
            }
            if (includeExpiredSessions || !information.isExpired()) {
                list.add(information);
            }
        }

        return list;
    }

    @Override   // 根据 sessionId 获取 session 信息
    public SessionInformation getSessionInformation(String sessionId) {
        return getSessionInformation(sessionId, null);
    }

    @Override   // 更新 redis 中该 session 的最后操作时间
    public void refreshLastRequest(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");

        String token = getToken(sessionId);
        SessionInformation information = getSessionInformation(sessionId, token);
        if (token != null && information != null) {
            information.refreshLastRequest();
            RedisUtils.addValue(redisTemplate, SESSION_INFORMATION_PREFIX + token, information,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
            RedisUtils.addValue(redisTemplate, TOKEN_SESSION_PREFIX + token, sessionId,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
            RedisUtils.addValue(redisTemplate, SESSION_TOKEN_PREFIX + sessionId, token,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
        }
    }

    @Override   // 认证成功后注册新 session
    public void registerNewSession(String sessionId, Object principal) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");
        Assert.notNull(principal, "Principal required as per interface contract");

        if (this.getSessionInformation(sessionId) != null) {
            removeSessionInformation(sessionId);
        }

        UserDetailsVo userDetails = OBJECT_MAPPER.convertValue(principal, new TypeReference<>() {});

        RedisUtils.addValue(redisTemplate, SESSION_INFORMATION_PREFIX + userDetails.getToken(),
                new MySessionInformation(userDetails.getAccount(), sessionId, new Date(), false),
                YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);

        RedisUtils.addValue(redisTemplate, TOKEN_SESSION_PREFIX + userDetails.getToken(), sessionId,
                YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);

        RedisUtils.addValue(redisTemplate, SESSION_TOKEN_PREFIX + sessionId, userDetails.getToken(),
                YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);

        RedisUtils.addSetValue(redisTemplate, SESSION_SESSIONS_PREFIX + userDetails.getAccount(), sessionId);
    }

    @Override   // 登出时移除 session 信息
    public void removeSessionInformation(String sessionId) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");

       this.removeSessionInformation(getToken(sessionId), sessionId);
    }

    // 处理 token 与 session 映射关系 在 TokenSessionFilter 中被调用
    public static void mappingTokenSession(String token, String sessionId) {
        if (!StringUtils.hasText(token) || !StringUtils.hasText(sessionId)) {
            return;
        }

        String cacheSessionId = MySessionRegistry.getSessionId(token);
        if (cacheSessionId != null && cacheSessionId.equals(sessionId)
                && RedisUtils.containKey(redisTemplate, SESSION_TOKEN_PREFIX + sessionId)) {
            return;
        }

        MySessionInformation information = getSessionInformation(sessionId, token);
        if (information == null) {
            return;
        }

        if (cacheSessionId == null) {
            RedisUtils.addValue(redisTemplate, TOKEN_SESSION_PREFIX + token, sessionId,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
            RedisUtils.addValue(redisTemplate, SESSION_TOKEN_PREFIX + sessionId, token,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
            return;
        }

        if (!cacheSessionId.equals(sessionId)) {
            RedisUtils.removeSetValue(redisTemplate, SESSION_SESSIONS_PREFIX + information.getPrincipal(), cacheSessionId);
            RedisUtils.addSetValue(redisTemplate, SESSION_SESSIONS_PREFIX + information.getPrincipal(), sessionId);

            RedisUtils.addValue(redisTemplate, SESSION_INFORMATION_PREFIX + token, information,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);

            RedisUtils.addValue(redisTemplate, TOKEN_SESSION_PREFIX + token, sessionId,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);

            RedisUtils.addValue(redisTemplate, SESSION_TOKEN_PREFIX + sessionId, token,
                    YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);

            RedisUtils.deleteKey(redisTemplate, SESSION_TOKEN_PREFIX + cacheSessionId);
        }
    }

    // 更新 session state
    public static void updateSessionState(SessionInformation sessionInformation) {
        RedisUtils.addValue(redisTemplate, SESSION_INFORMATION_PREFIX + getToken(sessionInformation.getSessionId()),
                sessionInformation, YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
    }

    // get session-information
    public static MySessionInformation getSessionInformation(String sessionId, String token) {
        Assert.hasText(sessionId, "SessionId required as per interface contract");

        if (token == null) {
            token = getToken(sessionId);
        }

        Object value = RedisUtils.getValue(redisTemplate, SESSION_INFORMATION_PREFIX + token);
        if (value == null) {
            return null;
        }

        Map<String, Object> map = OBJECT_MAPPER.convertValue(value, new TypeReference<>() {});
        Date lastRequest = new Date((Long) map.get("lastRequest"));
        Object principal = map.get("principal");
        boolean expireState = (boolean) map.get("expireState");

        return  new MySessionInformation(principal, sessionId, lastRequest, expireState);
    }

    // get token by sessionId
    public static String getToken(String sessionId) {
        if (sessionId == null) {
            return null;
        }

        Object value = RedisUtils.getValue(redisTemplate, SESSION_TOKEN_PREFIX + sessionId);
        return value != null ? value.toString() : null;
    }

    // get sessionId by token
    public static String getSessionId(String token) {
        if (token == null) {
            return null;
        }

        Object value = RedisUtils.getValue(redisTemplate, TOKEN_SESSION_PREFIX + token);
        return value != null ? value.toString() : null;
    }

    // 根据 principal 获取 sessionIds
    private Set<Object> getSessionIds(Object principal) {
        UserDetailsVo userDetails = OBJECT_MAPPER.convertValue(principal, new TypeReference<>() {});
        return RedisUtils.getSetValue(redisTemplate, SESSION_SESSIONS_PREFIX + userDetails.getAccount());
    }

    // remove session-information
    public void removeSessionInformation(String token, String sessionId) {
        SessionInformation sessionInformation = getSessionInformation(sessionId, token);
        if (token == null || sessionInformation == null) {
            return;
        }

        RedisUtils.deleteKey(redisTemplate, SESSION_INFORMATION_PREFIX + token);
        RedisUtils.deleteKey(redisTemplate, TOKEN_SESSION_PREFIX + token);
        RedisUtils.deleteKey(redisTemplate, SESSION_TOKEN_PREFIX + sessionId);

        if (sessionInformation.getPrincipal() == null) {
            return;
        }

        RedisUtils.removeSetValue(redisTemplate, SESSION_SESSIONS_PREFIX + sessionInformation.getPrincipal(), sessionId);
    }
}

7.3 SecurityContextRepository

@Slf4j
@Component
public class MySecurityContextRepository implements SecurityContextRepository {

    // string   key: account   value: context(SecurityContext)
    private static final String ACCOUNT_CONTEXT_PREFIX = "security:account:context:";

    // string   key: token   value: account
    private static final String ACCOUNT_TOKEN_PREFIX = "security:account:token:";

    // set   key: account   value: tokens
    private static final String ACCOUNT_TOKENS_PREFIX = "security:account:tokens:";

    private final AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

    private final RedisTemplate<String, Object> springSecurityRedisTemplate;

    private final ObjectMapper springSecurityObjectMapper;

    public MySecurityContextRepository(RedisTemplate<String, Object> springSecurityRedisTemplate,
                                       @Qualifier("springSecurityObjectMapper") ObjectMapper springSecurityObjectMapper) {
        this.springSecurityRedisTemplate = springSecurityRedisTemplate;
        this.springSecurityObjectMapper = springSecurityObjectMapper;
    }

    @Override   // 授权时获取当前登录用户的安全上下文信息(包含认证信息)
    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        return getSecurityContext(requestResponseHolder.getRequest());
    }

    @Override   // 认证通过后保存安全上下文信息
    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
        Authentication authentication = context.getAuthentication();
        if (authentication == null || this.trustResolver.isAnonymous(authentication)) {
            return;
        }

        UserDetailsVo vo = (UserDetailsVo) authentication.getPrincipal();
        String account = vo.getAccount();

        RedisUtils.addValue(springSecurityRedisTemplate, ACCOUNT_CONTEXT_PREFIX + account, context,
                YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
        RedisUtils.addValue(springSecurityRedisTemplate, ACCOUNT_TOKEN_PREFIX + vo.getToken(), account,
                YmlConfig.getRedisExpirationTimeout(), TimeUnit.SECONDS);
        RedisUtils.addSetValue(springSecurityRedisTemplate, ACCOUNT_TOKENS_PREFIX + account, vo.getToken());
    }

    @Override   // 是否包含安全上下文信息
    public boolean containsContext(HttpServletRequest request) {
        String token = request.getHeader(StringEnum.TOKEN.getCode());
        if (token == null) {
            return false;
        }

        return RedisUtils.containKey(springSecurityRedisTemplate, ACCOUNT_TOKEN_PREFIX + token);
    }

    // 清除 context 登出时调用
    public void clearContext(HttpServletRequest request) {
        String token = request.getHeader(StringEnum.TOKEN.getCode());
        if (token == null) {
            return;
        }

        SecurityContext context = this.getSecurityContext(request);

        RedisUtils.deleteKey(springSecurityRedisTemplate, ACCOUNT_TOKEN_PREFIX + token);

        if (context != null && context.getAuthentication() != null && context.getAuthentication().getPrincipal() != null) {
            UserDetailsVo vo = (UserDetailsVo) context.getAuthentication().getPrincipal();

            RedisUtils.removeSetValue(springSecurityRedisTemplate, ACCOUNT_TOKENS_PREFIX + vo.getAccount(), token);

            Set<Object> tokens = RedisUtils.getSetValue(springSecurityRedisTemplate, ACCOUNT_TOKENS_PREFIX + vo.getAccount());
            if (CollectionUtils.isEmpty(tokens)) {
                RedisUtils.deleteKey(springSecurityRedisTemplate, ACCOUNT_CONTEXT_PREFIX + vo.getAccount());
            }
        }
    }

    // 获取 security context
    private SecurityContext getSecurityContext(HttpServletRequest request) {
        SecurityContext emptyContext = SecurityContextHolder.createEmptyContext();

        String token = request.getHeader(StringEnum.TOKEN.getCode());
        if (token == null) {
            return emptyContext;
        }

        Object account = RedisUtils.getValue(springSecurityRedisTemplate, ACCOUNT_TOKEN_PREFIX + token);
        if (account == null) {
            return emptyContext;
        }

        try {
            Object result = RedisUtils.getValue(springSecurityRedisTemplate, ACCOUNT_CONTEXT_PREFIX + account);
            SecurityContext securityContext = springSecurityObjectMapper.convertValue(result, SecurityContext.class);
            return securityContext == null ? emptyContext : securityContext;
        } catch (Exception e) {
            log.error("Failed to convert Map to Object!", e);
            return emptyContext;
        }
    }
}

7.4 SecurityMetadataSource 与 AuthorizationManager

7.4.1 MySecurityMetedataSource
@Component
public class MySecurityMetadataSource {

    private final Map<String, Set<String>> requestPathRole = new HashMap<>();

    private final PermMapper permMapper;

    public MySecurityMetadataSource(PermMapper permMapper) {
        this.permMapper = permMapper;
    }
		
  	// 服务启动时初始化 资源-角色 映射关系
    @PostConstruct
    public void init() {
        this.refreshPermRole();
    }

    // 根据请求 path 获取角色
    public Set<String> getRequestPathRole(String requestPath) {
        return this.requestPathRole.get(requestPath);
    }

    // 刷新权限-角色列表映射 当 角色、权限 数据更新时调用
    public void refreshPermRole() {
        List<PermRoleVo> allPermRole = permMapper.allPermRole();

        for (PermRoleVo perm : allPermRole) {
            if (!StringUtils.hasText(perm.getApiUrl())) {
                continue;
            }

            Set<String> roles;
            if (this.requestPathRole.containsKey(perm.getApiUrl())) {
                roles = this.requestPathRole.get(perm.getApiUrl());
            } else {
                roles = new HashSet<>();
            }

            for (RoleVo role : perm.getRoles()) {
                if (!StringUtils.hasText(role.getRoleCode())) {
                    continue;
                }

                roles.add(role.getRoleCode());
            }

            this.requestPathRole.put(perm.getApiUrl(), roles);
        }
    }
}
7.4.2 AuthorizationManager
@Component
public class MyAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    private static final AuthorizationDecision AUTHORIZED = new AuthorizationDecision(true);

    private final AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

    private final MySecurityMetadataSource mySecurityMetadataSource;

    public MyAuthorizationManager(MySecurityMetadataSource mySecurityMetadataSource) {
        this.mySecurityMetadataSource = mySecurityMetadataSource;
    }

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext object) {

        Authentication tempAuthentication = authentication.get();
        Collection<? extends GrantedAuthority> grantedAuthorities = tempAuthentication.getAuthorities();

        String requestUrl = UrlUtils.buildRequestUrl(object.getRequest());

        Set<String> requestPathRole;
        if (StringEnum.LOGIN_PATH.getCode().equals(requestUrl) ||
                CollectionUtils.isEmpty(requestPathRole = this.mySecurityMetadataSource.getRequestPathRole(requestUrl))) {
            return AUTHORIZED;
        }

        if (this.authenticationTrustResolver.isAnonymous(tempAuthentication)) {
            throw new InsufficientAuthenticationException("请先登录!");
        }

        String authority;
        for (GrantedAuthority grantedAuthority : grantedAuthorities) {
            if (!StringUtils.hasText(authority = grantedAuthority.getAuthority())) {
                continue;
            }

            if (requestPathRole.contains(authority)) {
                return AUTHORIZED;
            }
        }

        throw new AccessDeniedException("权限不足!");
    }
}

7.5 TokenSessionFilter

public class TokenSessionFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String sessionId;
        HttpSession session = request.getSession(false);

        String token = request.getHeader(StringEnum.TOKEN.getCode());

        if (session != null && (sessionId = session.getId()) != null && StringUtils.hasText(token)) {
            MySessionRegistry.mappingTokenSession(token, sessionId);
        }

        filterChain.doFilter(request, response);
    }
}

7.6 AuthenticationEntryPoint

@Slf4j
@Component   // 授权时未获取到认证信息时执行 
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        log.error(authException.getMessage());
        ResponseUtils.responseJson(response, APIResponse.failure(1004, "请先登录!"));
    }
}

7.7 AuthenticationSuccessHandler 与 AuthenticationFailureHandler

7.7.1 AuthenticationSuccessHandler
@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        UserDetailsVo vo = (UserDetailsVo) authentication.getPrincipal();
        vo.setPassword(null);
        ResponseUtils.responseJson(response, APIResponse.success("Login successful!", vo));
    }
}
7.7.2 AuthenticationFailureHandler
@Slf4j
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {

        log.error(exception.getMessage());

        String message;
        if (exception instanceof UsernameNotFoundException || exception instanceof BadCredentialsException) {
            message = "用户名或密码错误!";
        } else if (exception instanceof LockedException) {
            message = "账号被锁定!";
        } else if (exception instanceof DisabledException) {
            message = "账号被禁用!";
        } else if (exception instanceof AccountExpiredException) {
            message = "账号已过期!";
        } else if (exception instanceof CredentialsExpiredException) {
            message = "密码已过期!";
        } else {
            message = exception.getMessage();
        }

        ResponseUtils.responseJson(response, APIResponse.failure(1000, message));
    }
}

7.8 LogoutHandler 与 LogoutSuccessHandler

7.8.1 LogoutHandler
@Component
public class MyLogoutHandler implements LogoutHandler {

    private final MySecurityContextRepository mySecurityContextRepository;

    private final MySessionRegistry sessionRegistry;

    public MyLogoutHandler(MySecurityContextRepository mySecurityContextRepository, MySessionRegistry sessionRegistry) {
        this.mySecurityContextRepository = mySecurityContextRepository;
        this.sessionRegistry = sessionRegistry;
    }

    @Override
    public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        sessionRegistry.removeSessionInformation(request.getHeader(StringEnum.TOKEN.getCode()), request.getSession().getId());
        mySecurityContextRepository.clearContext(request);
    }
}
7.8.2 LogoutSuccessHandler
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        ResponseUtils.responseJson(response, APIResponse.success("登出成功!"));
    }
}

7.9 AccessDeniedHandler

@Slf4j
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException)
            throws IOException, ServletException {
        log.error(accessDeniedException.getMessage());
        ResponseUtils.responseJson(response, APIResponse.failure(1001, "权限不足!"));
    }
}

7.10 SessionExpiredHandler

@Component
public class MySessionExpiredHandler implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        ResponseUtils.responseJson(event.getResponse(), APIResponse.failure(R1005, "账号在其它设备登录,请重新登录!"));
    }
}

7.11 SecurityConfig

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity,
                                                   UserDetailsService userDetailsService,
                                                   AuthenticationEntryPoint authenticationEntryPoint,
                                                   AuthenticationSuccessHandler authenticationSuccessHandler,
                                                   AuthenticationFailureHandler authenticationFailureHandler,
                                                   MySessionRegistry mySessionRegistry,
                                                   SessionInformationExpiredStrategy sessionExpiredHandler,
                                                   SecurityContextRepository securityContextRepository,
                                                   AuthorizationManager<RequestAuthorizationContext> authorizationManager,
                                                   AccessDeniedHandler accessDeniedHandler,
                                                   LogoutHandler logoutHandler,
                                                   LogoutSuccessHandler logoutSuccessHandler) throws Exception {

        // 因为是前后端分离 所以关闭 CSRF
        httpSecurity.csrf(AbstractHttpConfigurer::disable);

        // 跨域配置
        httpSecurity.cors(configurer -> configurer.configurationSource(this.configurationSource()));

        // 指定 UserDetailsService 认证时通过该实现获取用户信息
        httpSecurity.userDetailsService(userDetailsService);

        // 用户名密码认证配置
        httpSecurity.formLogin(configurer -> configurer.loginProcessingUrl("/admin/user/login")
                .usernameParameter("account")
                .passwordParameter("password")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler));

        // session 管理配置
        httpSecurity.addFilterBefore(new TokenSessionFilter(), UsernamePasswordAuthenticationFilter.class)
                .sessionManagement(configurer -> configurer.maximumSessions(2)
                .sessionRegistry(mySessionRegistry)
                .expiredSessionStrategy(sessionExpiredHandler));

        // 指定 security 安全上下文存储器 用来存储和管理认证后的用户信息
        httpSecurity.securityContext(configurer -> configurer.securityContextRepository(securityContextRepository));

        // 指定认证端点异常处理器 当当前访问的资源需要认证但未认证时会执行该处理器
        httpSecurity.exceptionHandling(configurer -> configurer.authenticationEntryPoint(authenticationEntryPoint));

        // 指定授权管理器 即自定义实现的授权逻辑
        httpSecurity.authorizeHttpRequests(authorize -> authorize.anyRequest()
                .access(authorizationManager));

        // 指定授权失败处理器 当授权失败时执行
        httpSecurity.exceptionHandling(configurer -> configurer.accessDeniedHandler(accessDeniedHandler));

        // 登出配置
        httpSecurity.logout(configurer -> configurer.logoutUrl("/admin/user/logout")
                .addLogoutHandler(logoutHandler)
                .logoutSuccessHandler(logoutSuccessHandler));

        return httpSecurity.build();
    }

    @Bean("springSecurityObjectMapper")
    public ObjectMapper springSecurityObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();

        // 因为放入 redis 的 SecurityContext 中的 Authentication 接口的相关实现类没有无参构造函数 会导致反序列化失败
        // 所以需要借助 security 中的相关 module
        objectMapper.registerModules(SecurityJackson2Modules.getModules(getClass().getClassLoader()));

        // 由于反序列化经常出现漏洞 故 spring security 增加了反序列化类的白名单 即只有加入此白名单的类才可被反序列化
        // 而放入 redis 中的 SecurityContext 中的 authentication 中的 principal 对象实际上是我们自定义实现的用户类(UserDetailsVo)
        // 该类需要被反序列化 但却没被加入白名单 所以需要将其加入白名单
        // 加入白名单有两种方式 即加入 mixIn 和 使用 jackson 相关注解(以下使用方式一)
        objectMapper.addMixIn(UserDetailsVo.class, UserDetailsVo.UserDetailsVoMixin.class);
        return objectMapper;
    }

    @Bean("objectMapper")
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }

    // 跨域
    private CorsConfigurationSource configurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.addAllowedHeader("*");
        configuration.addAllowedMethod("*");
        configuration.addAllowedOriginPattern("*");
        configuration.setAllowCredentials(true);
        configuration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",configuration);
        return source;
    }
}
标题基于Python的汽车之家网站舆情分析系统研究AI更换标题第1章引言阐述汽车之家网站舆情分析的研究背景、意义、国内外研究现状、论文方法及创新点。1.1研究背景与意义说明汽车之家网站舆情分析对汽车行业及消费者的重要性。1.2国内外研究现状概述国内外在汽车舆情分析领域的研究进展与成果。1.3论文方法及创新点介绍本文采用的研究方法及相较于前人的创新之处。第2章相关理论总结和评述舆情分析、Python编程及网络爬虫相关理论。2.1舆情分析理论阐述舆情分析的基本概念、流程及关键技术。2.2Python编程基础介绍Python语言特点及其在数据分析中的应用。2.3网络爬虫技术说明网络爬虫的原理及在舆情数据收集中的应用。第3章系统设计详细描述基于Python的汽车之家网站舆情分析系统的设计方案。3.1系统架构设计给出系统的整体架构,包括数据收集、处理、分析及展示模块。3.2数据收集模块设计介绍如何利用网络爬虫技术收集汽车之家网站的舆情数据。3.3数据处理与分析模块设计阐述数据处理流程及舆情分析算法的选择与实现。第4章系统实现与测试介绍系统的实现过程及测试方法,确保系统稳定可靠。4.1系统实现环境列出系统实现所需的软件、硬件环境及开发工具。4.2系统实现过程详细描述系统各模块的实现步骤及代码实现细节。4.3系统测试方法介绍系统测试的方法、测试用例及测试结果分析。第5章研究结果与分析呈现系统运行结果,分析舆情数据,提出见解。5.1舆情数据可视化展示通过图表等形式展示舆情数据的分布、趋势等特征。5.2舆情分析结果解读对舆情分析结果进行解读,提出对汽车行业的见解。5.3对比方法分析将本系统与其他舆情分析系统进行对比,分析优劣。第6章结论与展望总结研究成果,提出未来研究方向。6.1研究结论概括本文的主要研究成果及对汽车之家网站舆情分析的贡献。6.2展望指出系统存在的不足及未来改进方向,展望舆情
【磁场】扩展卡尔曼滤波器用于利用高斯过程回归进行磁场SLAM研究(Matlab代码实现)内容概要:本文介绍了利用扩展卡尔曼滤波器(EKF)结合高斯过程回归(GPR)进行磁场辅助的SLAM(同步定位与地图构建)研究,并提供了完整的Matlab代码实现。该方法通过高斯过程回归对磁场空间进行建模,有效捕捉磁场分布的非线性特征,同时利用扩展卡尔曼滤波器融合传感器数据,实现移动机器人在复杂环境中的精确定位与地图构建。研究重点在于提升室内等无GPS环境下定位系统的精度与鲁棒性,尤其适用于磁场特征明显的场景。文中详细阐述了算法原理、数学模型构建、状态估计流程及仿真实验设计。; 适合人群:具备一定Matlab编程基础,熟悉机器人感知、导航或状态估计相关理论的研究生、科研人员及从事SLAM算法开发的工程师。; 使用场景及目标:①应用于室内机器人、AGV等在缺乏GPS信号环境下的高精度定位与地图构建;②为磁场SLAM系统的设计与优化提供算法参考和技术验证平台;③帮助研究人员深入理解EKF与GPR在非线性系统中的融合机制及实际应用方法。; 阅读建议:建议读者结合Matlab代码逐模块分析算法实现细节,重点关注高斯过程回归的训练与预测过程以及EKF的状态更新逻辑,可通过替换实际磁场数据进行实验验证,进一步拓展至多源传感器融合场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红衣女妖仙

行行好,给点吃的吧!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值