自定义从 JWT Claim 到 Spring Security Authority 的映射

1、简介 本文将带你了解如何自定义从 JWT(JSON Web Token)Claim 到 Spring Security 权限(Authority)的映射。 2、背景 基于 Spring Security 的应用接收到请求时,它会经过一系列步骤,本质上旨在实现两个目标。 认证请求,以便应用知道谁在访问它 决定通过身份认证的请求是否可以执行相关操作 对于使用 JWT 的应用,授权方面包括: 从 JWT payload(通常是 scope 或 scp Claim)中提取 Claim 值 将这些 Claim 声明映射到一组 GrantedAuthority 对象中 一旦 Security 引擎设置了这些权限,它就可以评估是否有任何访问限制适用于当前请求,并决定是否可以继续处理。 3、默认的映射 在开箱即用的情况下,Spring 使用一种直接的策略将 Claim 声明转换为 GrantedAuthority 实例。首先,它会提取 scope 或 scp Claim,并将其拆分成一个字符串列表。接下来,它会为每个字符串创建一个新的 SimpleGrantedAuthority,使用前缀 SCOPE_,后跟 scope 值。 接下来,创建一个简单的端点来演示这个策略。看看 Authentication 实例有哪些关键属性。 @RestController @RequestMapping("/user") public class UserRestController { @GetMapping("/authorities") public Map<String,Object> getPrincipalInfo(JwtAuthenticationToken principal) { Collection<String> authorities = principal.getAuthorities() .

跨域请求异常:The 'Acess-Control-Allow-Origin' header contains multiple values '*, *". but only one is allowed

1. 问题 在浏览器中进行跨域请求一个接口时报错如下: Access to XMLHttoRequest at ‘https://xxx.lzw.me/abc/getToken from origin “http://localhost:3001 has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The ‘Acess-Control-Allow-Origin’ header contains multiple values ‘*, *”. but only one is allowed. 2. 原因 从报错信息上理解,是 Acess-Control-Allow-Origin 的值为 *, *,设置了多个。从 nginx 配置上查看设置的值是 *。以非跨域的方式请求该接口,则可以在 Response 中看到 header 信息中包含了两个 Acess-Control-Allow-Origin 的设置。那么原因就是在 nginx 之外的其它网关或应用程序中还设置了该 header 值。找到它并移除个性化的设置逻辑即可。 3. 解决方法 如果 nginx 出口网关的设置是多余的,CORS 在应用程序层已有管理,则可以直接移除此处的配置。 如果是应用程序内部针对该接口添加了设置,考虑到需支持允许所有接口跨域访问,应修改应用程序移除相关逻辑。 如果希望在 nginx 层作统一配置管理,也可在应用程序出口网关层作 header 过滤。例如针对 Spring zuul 网关的设置实例:

Spring 单例 Bean 如何处理并发请求?

1、Spring Bean 和 Java 堆内存 Java 堆是一个全局共享内存,应用中的所有运行线程都可以访问它。当 Spring 容器创建 Singleton Scope Bean 时,该 Bean 将存储在堆中。这样,所有并发线程都能指向同一个 Bean 实例。 2、如何处理并发请求? 举例来说,Spring 应用中有一个名为 ProductService 的单例 Bean: @Service public class ProductService { private final static List<Product> productRepository = asList( new Product(1, "Product 1", new Stock(100)), new Product(2, "Product 2", new Stock(50)) ); public Optional<Product> getProductById(int id) { Optional<Product> product = productRepository.stream() .filter(p -> p.getId() == id) .findFirst(); String productName = product.map(Product::getName) .orElse(null); System.out.printf("Thread: %s; bean instance: %s; product id: %s has the name: %s%n", currentThread().

Spring Cloud 通过配置文件禁用服务发现

1、概览 在 Spring Cloud 中可以通过配置文件来启用、禁用服务发现,而不需要改动代码。 2、 设置 Eureka Server 和 Eureka Client 先创建一个 Eureka 服务器和一个 Discovery Client。 Eureka 服务创建过程,略! 2.1、Discovery Client 设置 创建 “Discovery Client” 应用,在 Eureka Server 上进行注册。 在 pom.xml 中添加 Web 和 Eureka Client starter 依赖: <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> 还需要在 dependencyManagement 中添加 spring-cloud-starter-parent 依赖,用于定义 cloud 组件的版本。 如果使用 Spring Initializr 创建项目,这些参数已经设置好了。如果没有,可以手动添加到 pom.xml 文件中: <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-parent</artifactId> <version>${spring-cloud-dependencies.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <properties> <spring-cloud-dependencies.

Spring Boot 属性迁移

1、简介 本文将带你了解 spring-boot-properties-migrator 模块,它是 Spring 为促进 Spring Boot 升级而提供的支持。用于帮助迁移 application properties。 随着每个 Spring Boot 版本的升级,可能会有一些属性被标记为已弃用、不再支持或新引入的属性。Spring 为每个升级发布了详细的 变更日志。然而,阅读这些变更日志可能会有些繁琐。这就该 spring-boot-properties-migrator 出场了,它通过为我们的设置提供个性化的信息来帮助我们进行属性迁移。 让我们来看看如何使用。 2、Demo 应用 把 Spring Boot 应用从 2.3.0 升级到 2.6.3。 2.1、Properties 在演示应用中,有两个 properties 文件。在默认 properties 文件 application.properties 中,添加如下配置: spring.resources.cache.period=31536000 spring.resources.chain.compressed=false spring.resources.chain.html-application-cache=false dev Profile YAML 文件 application-dev.yaml: spring: resources: cache: period: 31536000 chain: compressed: true html-application-cache: true properties 文件包含了几个在 Spring Boot 2.3.0 和 2.6.3 之间被替换或移除的属性。 同时提供了 .properties 和 .yaml 文件,以便更好地进行演示。 2.2、添加依赖 添加 spring-boot-properties-migrator 模块。

处理 Spring Security 异常

1、概览 本文通过一个示例来带你了解如何处理 Spring Security Resource Server 产生的 Spring Security 异常。 2、Spring Security Spring Security 是 Spring 的一个子项目。它试图将 Spring 项目中的所有用户访问控制功能进行整合。访问控制允许限制特定用户或角色在应用中可以执行的选项。 在本例中,我们重点关注 Exception Handler 的配置。Spring Security 提供了三种不同的接口来实现这一目的并控制产生的事件: Authentication Success(认证成功)Handler Authentication Failure(认证失败)Handler Access Denied(拒绝访问)Handler 3、Security Configuration 首先,配置类必须创建一个 SecurityFilterChain Bean。它将负责管理应用的所有安全配置。因此,我们必须在这里引入 Handler。 定义所需的配置: @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf() .disable() .httpBasic() .disable() .authorizeRequests() .antMatchers("/login") .permitAll() .antMatchers("/customError") .permitAll() .antMatchers("/access-denied") .permitAll() .antMatchers("/secured") .hasRole("ADMIN") .anyRequest() .authenticated() .and() .formLogin() .failureHandler(authenticationFailureHandler()) .successHandler(authenticationSuccessHandler()) .and() .exceptionHandling() .accessDeniedHandler(accessDeniedHandler()) .and() .logout(); return http.

在 Spring Boot 中处理 GraphQL 异常

1、概览 本文将带你了解如何在 Spring Boot 中处理 GraphQL 异常,以及 GraphQL 规范中关于错误响应的规定。 2、根据 GraphQL 规范进行响应 根据 GraphQL 规范,每个接收到的请求都必须返回一个格式良好的响应。这个格式良好的响应是一个 Map,包含了来自相应成功或失败的请求操作的数据或错误。此外,响应可能包含部分成功的结果数据和字段错误。 响应 Map 的关键 key 是 errors、data 和 extensions。 响应中的 errors 描述了请求操作过程中出现的任何异常、错误。如果没有错误发生,则响应中不得出现 errors 字段。接下来,我们将了解规范中描述的不同类型的错误。 data 字段描述了成功执行所请求操作的结果。如果操作是 Query,该字段就是 Query Root Operation 类型的对象。如果操作是 Mutation,则该字段是 Mutation Root Operation 类型的对象。 如果请求的操作在执行前就因信息缺失、验证错误或语法错误而失败,那么响应中就必须没有 data 字段。如果操作在执行过程中失败,且结果不成功,则 data 必须为 null。 响应 map 可能包含一个名为 extensions 的附加字段,它是一个 map 对象。该字段便于实现者在响应中提供他们认为合适的其他自定义内容。因此,对其内容格式没有额外的限制。 如果响应中没有 data 字段,则必须有 errors 字段,并且必须至少包含一个 error。此外,它还应说明失败的原因。 下面是一个 GraphQL 错误示例: mutation { addVehicle(vin: "NDXT155NDFTV59834", year: 2021, make: "Toyota", model: "Camry", trim: "XLE", location: {zipcode: "75024", city: "Dallas", state: "TX"}) { vin year make model trim } } 当违反唯一性约束时,错误响应如下所示:

Spring 获取所有带自定义注解的 Bean

1、概览 本文将带你了解如何在 Spring 中获取到所有带自定义注解的 Bean。 不同的 Spring 版本,实现方式也不同。 2、使用 Spring Boot 2.2 或更高版本 从 Spring Boot 2.2 起,可以使用 getBeansWithAnnotation 方法。 创建一个示例。首先,定义自定义注解。注意,要使用 @Retention(RetentionPolicy.RUNTIME) 元注解,以确保程序在运行时可以访问注解: @Retention(RetentionPolicy.RUNTIME) public @interface MyCustomAnnotation { } 现在,用注解定义第一个 Bean(还要添加 @Component 注解): @Component @MyCustomAnnotation public class MyComponent { } 然后,定义另一个带有注解的 Bean。不过,这次通过 @Configuration 中的 @Bean 方法来创建: public class MyService { } @Configuration public class MyConfigurationBean { @Bean @MyCustomAnnotation MyService myService() { return new MyService(); } } 现在,写一个测试,测试 getBeansWithAnnotation 方法能否获取到上述两个 Bean:

Spring RestClient 教程

1、简介 RestClient 是 Spring 6.1 M2 中引入的同步 HTTP 客户端,它取代了 RestTemplate。同步 HTTP 客户端以阻塞方式发送和接收 HTTP 请求和响应,这意味着它会等待每个请求完成后才继续下一个请求。 本文将带你了解 RestClient 的功能以及它与 RestTemplate 的比较。 2、RestClient 和 RestTemplate RestTemplate,顾名思义,是基于模板设计模式构建的。它是一种行为设计模式,用于在方法中定义算法的框架,允许子类为某些步骤提供特定的实现。虽然这是一种强大的模式,但它会导致需要进行方法覆写,这可能是不方便的。 为了改进这一点,RestClient 采用了 Fluent API。Fluent API 也是一种设计模式,它允许以一种使代码更易读和表达的方式进行方法链式调用,通常无需使用中间变量。 从创建一个基本的 RestClient 开始: RestClient restClient = RestClient.create(); 3、使用各种 HTTP 方法发起请求 与 RestTemplate 或其他 REST 客户端类似,RestClient 可以使用不同的 HTTP 方法发起请求。 创建一个用于操作的 Article 类: public class Article { Integer id; String title; // 构造函数和 getter/setter } 3.1、使用 GET 请求获取资源 GET 请求用于检索 Web 服务器上指定资源的数据,而不对其进行修改。通常,这是一个只读的操作! 获取服务器响应的字符串(String):

Spring Boot 下载文件名乱码

对于文件的下载功能来说,我归纳为 2 头 1 流。极其简单。 Content-Type 头,告诉客户端文件类型。 Content-Disposition 头,告诉客户端对于这个文件的处理方式(在浏览器中显示,还是下载)。 Output 流,写入文件内容到客户端。 文件名称乱码的问题 Content-Disposition 头有三个属性值,可以指定文件的名称。 name filename filename* 至于区别,我也说不大清楚。反正是有个规范,我感觉很乱。具体可以参考如下链接: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition 这就是容易出问题的地方了,由于文件名称设置方法不正确。或者是浏览器不兼容问题,如果 文件名称包含中文,可能会导致 下载文件名称乱码: response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + "中文の文件.txt"); 如上,直接用 + 拼接中文字符串,在谷歌浏览器中下载就是乱码。 常见的解决方案 以下我是在网上看到的一些解决方案。 对文件名称进行URI编码 response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + URLEncoder.encode("中文の文件.txt", StandardCharsets.UTF_8)); 修改字符编码 response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + new String("中文の文件.txt".getBytes("GBK"),"ISO-8859-1")); 经过测试,上面2种办法都OK。 比较优雅的方式 Spring 提供了一个工具类: org.springframework.http.ContentDisposition,专门用于处理文件下载中的 Content-Disposition Header,它会自动处理好中文文件名称的编码问题。 ContentDisposition 使用实在是太简单了,这里就不多说了,给一个完整的使用示例就能看明白。 package io.springboot.demo.web.controller; import java.io.IOException; import java.nio.charset.StandardCharsets; import javax.servlet.http.HttpServletResponse; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.GetMapping; import org.