大咖直播-鸿蒙原生开发与智能提效实战!>>> 了解详情
写点什么

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:007634

评论

发布
暂无评论

火了!北大学霸爆肝3个月的算法小抄完整笔记,GitHub疯狂转发

Java 数据结构 算法

深度学习基础入门篇[一]:神经元简介、单层多层感知机、距离计算方法式、相似度函数

汀丶人工智能

人工智能 机器学习 深度学习 多层感知机

毕业项目-618秒杀系统

不爱学习的程序猿

一路披荆斩棘腾讯6面面经(已拿offer)大厂远没想象中的难

小小怪下士

Java 程序员 面试 后端

细节拉满,80 张图带你一步一步推演 slab 内存池的设计与实现

bin的技术小屋

内存管理 Linux Kenel 内核 内存池

涨薪跳板! 2023阿里突击版Java面试宝典

程序知音

Java 编程语言 java面试 java架构 后端技术

浅谈 Spring 如何解决 Bean 的循环依赖问题

Java spring 循环依赖

负载均衡算法的实现

王玉川

c++ 负载均衡 高可用 高并发 一致性哈希

从0为你讲解,什么是服务降级?如何实现服务降级?

设计模式之美--经常被用错的KISS、YAGNI原则

GalaxyCreater

设计模式

【算法数据结构专题】「延时队列算法」史上手把手教你针对层级时间轮(TimingWheel)实现延时队列的开发实战落地(上)

码界西柚

4月月更 时间轮(TimeWheel) 算法指南 技术调整

5.5G,运营商能接受吗?

脑极体

5.5G

业务防资损,质量保障的第一要务!

老张

业务价值 交付质量 防资损

创业公司如何不沦为OpenAI“死侍军团”:训练小众数据,服务特定用户

B Impact

真的香!腾讯SpringBoot高阶笔记,限时开源48小时

程序知音

Java 微服务 编程语言 springboot java架构

Python数据分析库介绍及引入惯例

timerring

Python pandas

卓越工程之开发过程管理

agnostic

卓越工程

字节资深架构师用7大部分13章节,彻底讲透SpringBoot生态体系

Java spring 微服务 Spring Boot 框架

前端面试实录HTML篇

Immerse

html 面试 前端 HTML5, CSS3

为 NGINX 配置免费的 Let’s Encrypt SSL/TLS 证书

NGINX开源社区

熬夜肝完! 阿里P8的Java进阶知识典藏版,我从18K飙到30K

程序知音

Java 编程语言 java面试 java架构 Java面试题

Service进阶

芯动大师

service intentservice 轮询

Django笔记一之运行系统、创建视图并访问

Hunter熊

django

阿里大佬力荐K8s项目实战笔记!图文并茂带你深度解析Kubernetes

Java Kubernetes k8s

不敲一行代码,用ChatGPT开发App

FN0

移动开发 ChatGPT

GitHub标星35k+微服务深度原理实践进阶PDF,竟让阿里换下了Dubbo

Java 架构 面试 微服务

OneCode :如何构建部署低代码引擎工程

codebee

一个神奇的需求:doc批量转docx,1行Python代码实现

程序员晚枫

Python word 自动化办公

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