2025上半年,最新 AI实践都在这!20+ 应用案例,任听一场议题就值回票价 了解详情
写点什么

Spring Security 配置不再难!基于流程图的定制化配置全攻略

作者:Alexandr Manunin

  • 2025-04-15
    北京
  • 本文字数:8603 字

    阅读完需:约 28 分钟

大小:2.14M时长:12:29
Spring Security配置不再难!基于流程图的定制化配置全攻略

本文要点

  • Spring Security 是一个 Java/Jakarta EE 框架,为企业级应用程序提供了认证、授权和其他安全特性。

  • 开发人员可以基于 Spring Security 的SecurityFilterChain提供综合式的配置,以管理 CORS、CSRF 防护和身份认证过滤器,同时允许配置特定的端点,比如注册和登录。

  • 可以策略性地使用访问令牌和刷新令牌,以平衡安全性问题和用户便利性,最大限度地减少令牌泄露的风险,同时增强用户体验。

  • Axios 可用于客户端应用程序,以便于高效处理基于令牌的请求,它的拦截器能够管理令牌插入和刷新场景,确保健壮和无缝的用户交互。

  • 流程图可以更好地理解 Spring Security 在幕后是如何协调 API 调用的。


在本文中,我们将会使用Spring Security的基础设施、访问和刷新令牌,研究通过客户端 JavaScript 应用程序注册和认证用户的解决方案。


使用 Spring Security 的基础样例非常多,所以本文的目标是使用流程图更详细地描述可能的过程。


在 GitHub 的仓库中,你可以找到样例的源码。


注意:本文主要关注基础的成功场景,省略了错误和异常处理。


术语

  • 认证是验证用户身份标识的过程。

  • 授权是确定允许用户访问哪些资源和执行哪些操作的过程。

  • 令牌(访问令牌)是一个数据实体,包含了识别用户身份或授予用户访问受限资源的必要信息。

  • 刷新令牌是一种凭证,它能够让客户端应用获取新的访问令牌,而无需用户再次登录。刷新令牌的理念涉及到安全性和用户便利性之间的权衡。保留一个长时间有效的访问令牌会带来泄露的风险,而频繁提示用户进行登录又会降低用户的体验。刷新令牌通过如下方式解决了这个问题:

  • 允许客户端应用在访问令牌过期后获取一对新的令牌,而无需用户再次登录。

  • 减少访问令牌受攻击的时间窗口。

基本处理流程与 Spring Security 配置列表

系统支持如下的基本场景:


  1. 用户注册

  2. 通过登录表单进行用户的认证和授权,然后重定向到用户的页面

  3. 业务处理 - 请求已注册的用户数

  4. 令牌刷新


Spring Security 的整体配置可以通过SecurityConfiguration类中定义的filterChain()方法来实现:


@BeanSecurityFilterChain filterChain(final HttpSecurity http) throws Exception {    http       .cors(cors ->  cors.configurationSource(corsConfigurationSource()))       .csrf(AbstractHttpConfigurer::disable)       .exceptionHandling(configurer -> configurer           .accessDeniedHandler(accessDeniedHandler))       .sessionManagement(configurer -> configurer           .sessionCreationPolicy(SessionCreationPolicy.STATELESS))       .authorizeHttpRequests(authorize -> authorize           .requestMatchers(SIGNIN_ENTRY_POINT).permitAll()           .requestMatchers(SIGNUP_ENTRY_POINT).permitAll()           .requestMatchers(SWAGGER_ENTRY_POINT).permitAll()           .requestMatchers(API_DOCS_ENTRY_POINT).permitAll()           .requestMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll()           .anyRequest().authenticated()       )       .addFilterBefore(buildLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)       .addFilterBefore(buildTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)       .addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class);http.oauth2Login(configurer -> configurer       .authorizationEndpoint(config -> config               .authorizationRequestRepository(authorizationRequestRepository()))       .failureHandler(failureHandler)       .successHandler(oauth2AuthenticationSuccessHandler));

return http.build();}
复制代码


下面,我们逐一分析每种场景。

用户注册

当用户在注册表单中填完所有必填的字段并提交请求后,将会执行如图 1 所示的步骤:


图 1:用户注册


为了允许访问/signup端点,并绕过 Spring Security 默认的认证需求,我们需要配置 Spring Security 允许未认证的用户访问这个特殊的端点。这可以通过修改安全配置来实现,即将/signup端点排除在认证要求之外。


如下代码展示了如何使用SecurityConfiguration类中定义的上述filterChain()方法来配置 Spring Security,以允许访问/signup端点:


.authorizeHttpRequests(authorize -> authorize   .requestMatchers(SIGNIN_ENTRY_POINT).permitAll()   .requestMatchers(SIGNUP_ENTRY_POINT).permitAll()   .requestMatchers(SWAGGER_ENTRY_POINT).permitAll()   .requestMatchers(API_DOCS_ENTRY_POINT).permitAll()   .requestMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll()   .anyRequest().authenticated())
复制代码


下一个重要的地方的是配置中包含了一个令牌过滤器,它会拦截所有的请求并检查请求中的令牌,如filterChain()方法中这一部分所示:


.addFilterBefore(buildLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(buildTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
复制代码


为了在注册请求中排除这种验证,我们需要在构建令牌过滤器的时候指定如何识别该过滤器用于哪些路径的机制。我们看一下SecurityConfiguration类中定义的buildTokenAuthenticationFilter()方法:


protected TokenAuthenticationFilter buildTokenAuthenticationFilter() {    List<String> pathsToSkip = new ArrayList<>(Arrays.asList(SIGNIN_ENTRY_POINT, SIGNUP_ENTRY_POINT, SWAGGER_ENTRY_POINT, API_DOCS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT));    SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip);    TokenAuthenticationFilter filter = new TokenAuthenticationFilter(jwtTokenProvider, matcher, failureHandler);    filter.setAuthenticationManager(this.authenticationManager);    return filter;}
复制代码


在这里,我们使用了SkipPathRequestMatcher类(如下所示),它将pathsToSkip参数中声明的路径从过滤器的路径中排除掉了(在样例中,我们将SIGNUP_ENTRY_POINT添加到了这个数组中)。


public class SkipPathRequestMatcher implements RequestMatcher {    private final OrRequestMatcher matchers;

public SkipPathRequestMatcher(final List<String> pathsToSkip) { Assert.notNull(pathsToSkip, "List of paths to skip is required."); List<RequestMatcher> m = pathsToSkip.stream() .map(AntPathRequestMatcher::new) .collect(Collectors.toList()); matchers = new OrRequestMatcher(m); }

@Override public boolean matches(final HttpServletRequest request) { return !matchers.matches(request); } }
复制代码

用户通过表单进行认证和授权

如图 2 所示,当请求成功绕过令牌过滤器后,就会由业务控制器来进行处理:


图 2:用户通过表单进行认证和授权


  1. 客户端向服务器端点/login发送用户名和密码。

  2. 为了让LoginAuthenticationFilter拦截请求,我们需要相应地配置 Spring Security:


  • 使用SecurityConfiguration类中定义的buildLoginProcessingFilter()来定义该过滤器,并声明过滤请求的 URI:


@Beanprotected LoginAuthenticationFilter buildLoginProcessingFilter() {    LoginAuthenticationFilter filter = new LoginAuthenticationFilter(SIGNIN_ENTRY_POINT, authenticationSuccessHandler, failureHandler);    filter.setAuthenticationManager(this.authenticationManager);    return filter;}
复制代码


注意,在创建过滤器的时候,除了 URI 之外,我们还声明了认证成功和失败时的处理器,以及认证管理器。下面我们会对其进行详细讨论。


  • 使用 SecurityConfiguration 类中定义的 buildTokenAuthenticationFilter()方法,将该 URI 添加到令牌过滤器的排除列表中:


List<String> pathsToSkip = new ArrayList<>(Arrays.asList(SIGNIN_ENTRY_POINT, SIGNUP_ENTRY_POINT, SWAGGER_ENTRY_POINT, API_DOCS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT));
复制代码


  • 通过filterChain()方法添加创建的过滤器:


@BeanSecurityFilterChain filterChain(final HttpSecurity http) throws Exception {     http             .cors(cors -> cors.configurationSource(corsConfigurationSource()))

// 构建器配置

.addFilterBefore(buildLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(buildTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class);



// 构建器配置

return http.build();}
复制代码


LoginAuthenticationFilter类中,我们重写了 Spring 在执行过滤器时要调用的两个方法。第一个方法是attemptAuthentication(),在这个方法中,我们初始化了一个发往AuthenticationManager的认证请求,而AuthenticationManager是我们在创建过滤器的时候提供的。但是,管理器(manager)本身并不会执行认证操作,它只会作为处理该任务的提供器(provider)的容器。AuthenticationManager接口会负责找到合适的提供器并将请求传递给它。如下是创建管理器和注册管理器的过程:


@Beanpublic AuthenticationManager authenticationManager(final ObjectPostProcessor<Object> objectPostProcessor) throws Exception {        var auth = new AuthenticationManagerBuilder(objectPostProcessor);        auth.authenticationProvider(loginAuthenticationProvider);        auth.authenticationProvider(tokenAuthenticationProvider);        auth.authenticationProvider(refreshTokenAuthenticationProvider);        return auth.build();}
复制代码


接下来,管理器会被声明为所有已创建过滤器的参数。


  1. 为了让AuthenticationManager能够找到所需的提供器(在我们的样例中,也就是LoginAuthenticationProvider),在提供器本身中,需要声明它所支持的类型,如下面的supports()方法所示:


@Overridepublic boolean supports(final Class<?> authentication) {   return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));}
复制代码


在样例中,我们声明提供器能够支持UsernamePasswordAuthenticationToken类。当在过滤器中创建UsernamePasswordAuthenticationToken类型的对象并将其传递给AuthenticationManager时,它就能使用LoginAuthenticationFilter类中定义的attemptAuthentication()方法,根据对象类型正确地找到所需的提供器:


@Overridepublic Authentication attemptAuthentication(final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException {    // some code above    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());    token.setDetails(authenticationDetailsSource.buildDetails(request));    return this.getAuthenticationManager().authenticate(token);}
复制代码


  1. AuthenticationManager找到了所需的提供器之后,它会调用authenticate()方法,提供器会直接对用户的登录名和密码进行验证。然后,将结果返回给过滤器。

  2. 我们重写的第二个方法是successfulAuthentication(),在 Spring 认证成功后会调用该方法。处理认证成功的任务落在了 Spring Security 的AuthenticationSuccessHandler接口上,当我们创建过滤器时(如前文所述)声明了该接口。该处理器有一个重写方法onAuthenticationSuccess(),我们一般会在该方法中记录生成的令牌并为请求设置请求成功的响应码。


// LoginAuthenticationSuccessHandler

@Overridepublic void onAuthenticationSuccess(final HttpServletRequest request, final HttpServletResponse response, final Authentication authentication) throws IOException { UserDetails userDetails = (UserDetails) authentication.getPrincipal();

JwtPair jwtPair = tokenProvider.generateTokenPair(userDetails);

response.setStatus(HttpStatus.OK.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); JsonUtils.writeValue(response.getWriter(), jwtPair);}
复制代码


接下来,Spring 的基础设施会将成功的响应转发给客户端。

业务处理 - 请求已注册的用户数

在本样例中,我们将业务请求定义为检索数据库中用户的数量。预期行为是这样的,对于已登录用户的所有请求,我们就会检查令牌。令牌校验的流程是由TokenAuthenticationFilter发起的,然后按照与前文类似的流程,请求会委托给TokenAuthenticationProvider。在验证成功之后,过滤器将请求转发给 web 应用的标准过滤器链,最终,请求会抵达业务控制器AuthController,如图 3 所示。


图 3:请求已注册的用户数


  1. 客户端发送一个请求到服务器的端点/users/count,并在请求中附带令牌。

  2. 为了让TokenAuthenticationFilter拦截该请求,我们需要在 Spring Security 中对其进行配置:


  • 创建该过滤器(我们已经在前文的流程中看到了这个过滤器)并声明过滤请求的 URI(在本例中,也就是除了SkipPathRequestMatcher类中排除的请求之外的所有请求),我们需要在 Spring Security 配置中借助buildTokenAuthenticationFilter()方法来配置它,如下所示:


protected TokenAuthenticationFilter buildTokenAuthenticationFilter() {    List<String> pathsToSkip = new ArrayList<>(Arrays.asList(SIGNIN_ENTRY_POINT, SIGNUP_ENTRY_POINT, SWAGGER_ENTRY_POINT, API_DOCS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT));    SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip);    TokenAuthenticationFilter filter = new TokenAuthenticationFilter(jwtTokenProvider, matcher, failureHandler);    filter.setAuthenticationManager(this.authenticationManager);    return filter;}
复制代码


和前面的过滤器一样,我们声明了AuthenticationManager,这个类会被调用以寻找提供器


  • 使用filterChain()方法将创建的过滤器添加到配置中:


@BeanSecurityFilterChain filterChain(final HttpSecurity http) throws Exception {    http        .cors(cors -> cors.configurationSource(corsConfigurationSource()))

// 构建器配置

.addFilterBefore(buildLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(buildTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class).addFilterBefore(buildRefreshTokenProcessingFilter(), UsernamePasswordAuthenticationFilter.class);



// 构建器配置 return http.build();}
复制代码


为了让AuthenticationManager找到所需的提供器,我们使用了authenticationManager()方法:


@Beanpublic AuthenticationManager authenticationManager(final ObjectPostProcessor<Object> objectPostProcessor) throws Exception {    var auth = new AuthenticationManagerBuilder(objectPostProcessor);    auth.authenticationProvider(loginAuthenticationProvider);    auth.authenticationProvider(tokenAuthenticationProvider);    auth.authenticationProvider(refreshTokenAuthenticationProvider);    return auth.build();}
复制代码


  • 在提供器本身中,通过TokenAuthenticationProvider类的supports()方法指定过滤请求的类型:


@Overridepublic boolean supports(final Class<?> authentication) {   return (JwtAuthenticationToken.class.isAssignableFrom(authentication));}
复制代码


所以,过滤器应该构建一个JwtAuthenticationToken对象。AuthenticationManager 会基于其类型,查找适当的提供器并发送该对象,以便于TokenAuthenticationFilter类中定义的attemptAuthentication()方法进行认证。


@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {    return getAuthenticationManager().authenticate(new JwtAuthenticationToken(tokenProvider.getTokenFromRequest(request)));}
复制代码


  1. 认证成功之后,successfulAuthentication()会将原始的请求转发给标准过滤器链,在这里它最终会抵达业务控制器AuthController

令牌刷新

令牌刷新的过程如图 4 所示。


图 4:令牌刷新


令牌刷新的过程与登录的过程非常类似:


  1. 客户端发送令牌刷新请求到/refreshToken端点。

  2. 请求被RefreshTokenAuthenticationFilter拦截,因为端点的 URI 包含在了该过滤器允许的 URI 列表中。

  3. 过滤器使用 attemptAuthentication()方法尝试进行认证,访问AuthenticationManager,后者又会调用RefreshTokenAuthenticationProvider。如前文的两个样例所述,这个提供器之所以能够被选中,是因为它支持一个特定的类型,也就是我们在过滤器中所构造的RefreshJwtAuthenticationToken


@Overridepublic boolean supports(final Class<?> authentication) {    return (RefreshJwtAuthenticationToken.class.isAssignableFrom(authentication));}
复制代码


  1. 认证成功之后,与登录过程一样,successAuthentication()方法会调用相同的处理器LoginAuthenticationSuccessHandler,它会在响应中记录生成的令牌对。

客户端的流程描述

要使用流程图来说明 JavaScript 应用端的流程似乎比较麻烦,因为流程的分支取决于服务器的响应。因此,我们直接关注代码,并逐步描述代码中发生的情况,我们看一下apiClient.js文件:


// 导入语句

const userStore = useUserStore();

// axios客户端初始化const apiClient = axios.create({ baseURL: process.env.API_URL});

// 从userStore中添加令牌function authHeader() { let token = userStore.getToken; if (token) { return {Authorization: 'Bearer ' + token}; } else { return {}; }}

// 添加拦截器以便于在每个请求中包含令牌apiClient.interceptors.request.use(function (config) { config.headers = authHeader(); return config;});

// 添加处理每个响应的拦截器apiClient.interceptors.response.use(function (response) { return response; // 成功响应}, function (error) { // 失败响应 const req = error.config; if (isTokenExpired(error)) { if (isRefreshTokenRequest(req)) { //refreshToken已过期,清除令牌信息并转发至登录页面 clearAuthCache(); window.location.href = '/login?expired=true'; } // 令牌已过期,需要刷新令牌 return authService.refreshToken(userStore.getRefreshToken).then(response => { // 将新令牌对保存到存储中 userStore.login(response); // 重复原始的业务请求 return apiClient.request(req); }); } // 在失败的认证中,我们在服务器端设置了401代码 // 包含不正确或空令牌 if (error.response?.status === 401) { clearAuthCache(); } return Promise.reject(error);});

export default apiClient;
复制代码


  1. 我们使用Axios库向服务器发送请求。

  2. 在 Axios 中注册一个拦截器,它会拦截所有的请求并向请求中添加令牌(借助authHeader()方法)。

  3. 我们在 Axios 中注册了一个响应拦截器,它会拦截所有的响应并执行如下的逻辑:

  4. 如果响应失败的话,我们会检查状态码:

  5. 如果响应中包含401状态码(比如,令牌无效或缺失令牌),我们会删除现有令牌的所有信息,并重定向到登录页面。

  6. 如果响应中包含令牌过期代码(该代码是由服务器在TokenAuthenticationProviderRefreshTokenAuthenticationProvider进行令牌检验时生成的),我们会额外检查原始请求是否为令牌刷新请求:

  7. 如果原始请求是普通的业务请求,则令牌过期信息表明访问令牌已经过期。为了刷新访问令牌,我们发送一个带有refreshToken的刷新请求。然后,我们会保存响应中的新令牌对,并使用更新后的令牌重复原始的业务请求。

  8. 如果原始请求是令牌刷新请求,令牌过期信息则表明refreshToken也已经过期了。此时,需要用户再次进行登录。因此,我们会删除现有令牌的所有信息,并重定向到登录页面。

  9. 如果响应成功的话,我们将其转发到客户端。

结论

在本例中,我们使用流程图详细研究了使用 Spring Security 和令牌的几个关键流程。超出本文范围的是异常处理和 OAuth2,我们将在其他文章中单独介绍。


作者简介:

Alexandr Manunin 拥有超过 15 年的 IT 行业工作经验,具有系统和业务分析背景。目前,他在一个金融科技项目中担任全栈工程师(JavaScript/Java)。Alexandr 感兴趣的领域包括金融科技、安全、分析可视化和图数据结构。


原文链接:

Spring Security Configuration with Flow Diagrams

2025-04-15 08:007157

评论

发布
暂无评论

真香!180页100+题15W+字解析的《Java高级面试指南》,果断收下

Java 编程 程序员 架构师

阿里秋招面试必问的几个知识点:Spring+Redis+MySQL+HashMap+多线程,不看我真的怕你后悔

Java 程序员 架构 面试 计算机

MES、ERP和低代码下的智慧工厂

优秀

低代码 ERP mes

ES本地debug详解

泽睿

ES 搜索引擎;

漏洞非小事,金融服务机构如何对抗代码缺陷?

鉴释

金融科技 代码安全检测

“Talk is cheap, show me the code”你一行代码有多少漏洞?

鉴释

代码质量 静态代码分析

如何利用FL Studio中文版做出失真效果

懒得勤快

新时代程序员都用什么写代码?

程序员鱼皮

Java Python 大前端 Web 开发工具

因聚而生 | 图扑受邀参加“生态融合,智创未来”大会

一只数据鲸鱼

数据可视化 数字孪生 智慧工业 智能制造

鉴释获得 A+ 轮融资,将加强对新技术的投资并扩展中国团队规模

鉴释

企业融资 创业公司

耗时半年,堪称奇迹!阿里架构师整合出258W字Java全栈面试题

钟奕礼

Java 程序员 架构 面试 计算机

从零开始学习3D可视化之数据对接(2)

ThingJS数字孪生引擎

大前端 数据 可视化 数字孪生

为什么“内存管理”漏洞值得你的绝对关注!

鉴释

代码审查 内存 代码

真的强!来自扫地僧总结的39W字上千道Java一线大厂面试题手册,成功助我拿下蚂蚁金服offer!

钟奕礼

Java 编程 程序员 架构 面试

5000页?一份字节跳动Java面试全解手册发布!瞬间登顶各大搜索栏

钟奕礼

Java 编程 程序员 架构 面试

把凭据嵌入源代码,来看看你的代码里有这样的操作吗?

鉴释

代码安全

华为云MVP程云:知识化转型,最终要赋能一线

华为云开发者联盟

人工智能 自然语言处理 机器学习 华为云 智能问答机器人

鉴释陈新中:源代码安全在物联网时代的重要性

鉴释

物联网 源代码

Camtasia入门技巧之视频剪辑

淋雨

视频剪辑 Camtasia 录屏软件

不保护数据的代价!

鉴释

数据 数据安全

给需要关心安全的技术人员的一些建议

鉴释

网络安全 安全

阿里集团业务驱动的升级 —— 聊一聊Dubbo 3.0 的演进思路

阿里巴巴中间件

云计算 阿里云 云原生 dubbo 中间件

抓住“开源盛世“,这个工具你必须了解一下

鉴释

开源 代码安全

支持60+数据传输链路,华为云DRS链路商用大盘点

华为云开发者联盟

华为云 DRS

字节大牛的1850页Leetcode刷题笔记外泄!用实力折服众人

进击的王小二

Java 面试 算法 LeetCode

IDC报告深度解析:谁将领跑中国RPA市场?

ToB行业头条

RPA IDC

彩印图文版《Elasticsearch实战》文档,阿里内部共享,堪称精品

白亦杨

Java 编程 程序员 架构师 计算机

赖建新:关于静态代码分析的问与答

鉴释

静态代码分析

iOS面试·一个iOS程序员的BAT面试全记录(内含百度+网易+阿里面试真题)

iOSer

ios 面试 iOS 知识体系

阿里内部最新出炉“SpringCloudAlibaba笔记”号称微服务界的里程碑!

Java 编程 架构 微服务 计算机

Kubernetes实战:高可用集群的搭建和部署

华为云开发者联盟

Kubernetes 高可用 集群 高可用集群 apiserver

Spring Security配置不再难!基于流程图的定制化配置全攻略_编程语言_InfoQ精选文章