☰
Java OAuth2.0服务端与客户端实现:JWT令牌集成实战
2026/10/16 9:38:20 网站建设 项目流程

简介:这是一套面向Java后端开发者与安全入门者的OAuth 2.0完整实现示例,基于Maven构建,借助Apache OLTU库搭建授权服务器与客户端,并引入JWT作为访问令牌,解决第三方应用在无需共享用户凭证前提下安全访问受保护资源的问题。压缩包共221个文件,约355KB,以176个java源码为主体,辅以22个xml配置、properties与json等资源文件,另含license、notice及依赖清单,便于直接导入IDE运行调试。内容覆盖授权端点、令牌端点、刷新令牌端点与资源端点四大核心组件,并给出AuthorizationServerConfig、ResourceServerConfig、OAuth2AuthenticationProvider、OAuth2AccessTokenGenerator等关键类的实现思路,同时演示MD5密码哈希与JWT签名校验流程。目前已有2184人学习下载,适合希望深入理解OAuth 2.0授权流程、掌握服务端与客户端联调及令牌安全生成方式的开发者参考。

1. 从一次授权码换不到 token 说起:这套 OAuth2 + JWT 环境到底装什么

去年帮一个做企业内部系统的团队排查登录问题,现象很典型:前端拿到授权码后去换 token,服务端返回 401,日志里只有一行invalid_client。折腾了一下午才发现,不是代码写错了,是客户端密钥的编码方式和服务端校验方式对不上——一个用了明文,一个按 BCrypt 校验。这种坑在 OAuth2 里太常见了,因为它不是单个工具,而是一整套「授权服务器 + 资源服务器 + 客户端」的协作环境,任何一端的配置偏差都会让整条链路断掉。

这份资源做的就是 Java 实现 OAuth2.0 服务端加客户端,并且把 JWT 作为令牌格式串进去。它适合两类人:一类是正在给自家系统接第三方登录、需要自己搭授权服务的后端开发者;另一类是已经用了 Spring Security OAuth2 但被版本迁移、令牌格式、签名校验搞得头大的运维和架构同学。装这套环境的核心不是敲几条命令,而是把版本、依赖、密钥、签名算法这几件事一次性对齐,否则后面每一步都在还债。

2. 环境准备与版本选型:JDK、Spring Boot 和 JWT 库怎么搭

2.1 先定 JDK 和构建工具,别在这步省事

这套东西跑起来的最低要求是 JDK 8,但我一般会直接上 JDK 17,原因是 Spring Boot 3.x 已经强制要求 JDK 17,而 Spring Security 6 的 OAuth2 授权服务器实现只在 Boot 3.x 里才正式提供。如果你还在用 JDK 8,那就只能走 Spring Boot 2.7 + Spring Security 5 的老路子,配置写法完全是两套。

构建工具用 Maven 或 Gradle 都行,我习惯 Maven,因为依赖树看起来直观。先确认本机环境:

java -version # 期望输出:openjdk version "17.0.x" 或更高 mvn -version # 期望输出:Apache Maven 3.8+,并且 Java version 指向 17

如果java -version显示的是 8 或 11,别急着改全局环境变量,用 SDKMAN 或直接下载解压的方式切一个 17 出来更干净。我见过太多人把系统默认 JDK 改来改去,最后 IDE 和命令行用的不是同一个,编译能过、运行报UnsupportedClassVersionError。

2.2 依赖清单:三个模块各自要引什么

这套环境分三块:授权服务器、资源服务器、客户端。实际部署时授权服务器和资源服务器可以合并成一个应用,但为了讲清楚,我按分开的方式列依赖。

授权服务器和资源服务器共用的核心依赖:

<!-- Spring Boot 3.x 的 Web 基础 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Security 6,OAuth2 授权服务器在这个包里 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-authorization-server</artifactId> <version>1.2.0</version> </dependency> <!-- JWT 的编码与签名,用 Nimbus 这套,Spring 内部也是它 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency>

客户端侧如果只是做授权码登录,额外引spring-boot-starter-oauth2-client就够了。这里有个版本取舍:spring-security-oauth2-authorization-server的 1.x 对应 Boot 3.2,0.4.x 对应 Boot 3.0/3.1,版本对不上会在启动时直接报NoClassDefFoundError,不是运行到某个接口才炸,所以启动日志一定要看全。

2.3 JWT 签名方式:对称还是非对称,先想清楚

JWT 的签名算法决定了你后面密钥怎么配。HS256 是对称的,服务端和资源端共用同一个密钥,配置简单但密钥一旦泄露全线失守;RS256 是非对称的,授权服务器用私钥签、资源服务器用公钥验,适合服务拆分部署。

我一般会选 RS256,因为资源服务器不需要持有能签发的密钥,权限最小化。生成密钥对可以用 JDK 自带的 keytool:

keytool -genkeypair -alias oauth2-jwt \ -keyalg RSA -keysize 2048 \ -storetype PKCS12 \ -keystore oauth2-jwt.p12 \ -validity 3650 \ -storepass changeit

-keysize 2048是底线,1024 位的 RSA 现在很多安全扫描会直接标红;-validity 3650是十年有效期,内部系统够用,但生产环境建议配合轮换机制。生成的 p12 文件放到src/main/resources下,后面在配置类里加载。

3. 授权服务器搭建:从密钥加载到令牌签发

3.1 加载密钥并注册 JWKSource

授权服务器要签 JWT,第一步是把刚才生成的密钥对读进来,包装成 Spring Security 能识别的JWKSource。这段代码是整个服务端的地基:

@Configuration public class JwtConfig { @Bean public JWKSource<SecurityContext> jwkSource() throws Exception { // 从 classpath 加载 p12 密钥库 KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(getClass().getResourceAsStream("/oauth2-jwt.p12"), "changeit".toCharArray()); // 取出私钥和公钥,组装成 RSAKey RSAKey rsaKey = RSAKey.load(keyStore, "oauth2-jwt", "changeit".toCharArray()); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } }

RSAKey.load会同时拿到私钥和公钥,私钥用于签发,公钥会通过 JWK Set 端点暴露出去给资源服务器拉取。changeit是密钥库口令,生产环境别用默认值,改成从环境变量读。这里最容易翻车的是密钥库类型:如果你用keytool默认生成的 JKS 格式,却按 PKCS12 加载,会报Keystore was tampered with, or password was incorrect,其实不是密码错,是格式不匹配。

3.2 注册客户端信息

OAuth2 的客户端不是随便谁都能来换 token 的,得先在授权服务器登记。Spring Authorization Server 用RegisteredClientRepository管理:

@Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("demo-client") // 注意:这里存的是编码后的密钥,不是明文 .clientSecret("{noop}demo-secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("http://127.0.0.1:8080/login/oauth2/code/demo-client") .scope(OidcScopes.OPENID) .scope("read") .build(); return new InMemoryRegisteredClientRepository(client); }

{noop}前缀表示密钥不加密、明文比对,只适合本地调试。生产环境要换成{bcrypt}并存入 BCrypt 哈希值,否则就是前面说的invalid_client的经典来源。redirectUri必须和客户端发起请求时带的一模一样,多一个斜杠、端口号不同都会导致授权码换 token 时被拒。

3.3 配置令牌生成器,把 JWT 塞进去

默认的令牌是自包含的 JWT,但你要控制它的有效期、签名算法和 claims,就得显式配JwtEncoder和OAuth2TokenCustomizer:

@Bean public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) { return new NimbusJwtEncoder(jwkSource); } @Bean public OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() { return context -> { // 只在 access_token 里加自定义字段 if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { context.getClaims().claim("tenant_id", "demo-tenant"); } }; }

NimbusJwtEncoder会自动用 JWKSource 里的私钥签名,算法跟着密钥类型走,RSA 密钥就是 RS256。OAuth2TokenCustomizer是往 JWT 里塞业务字段的入口,比如租户 ID、用户角色,资源服务器拿到 token 后可以直接解析这些字段做鉴权,不用再回查数据库。

3.4 安全过滤链的授权端点配置

最后把授权服务器的各个端点挂到安全过滤链上:

@Bean @Order(1) public SecurityFilterChain authServerFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); // 开启 OIDC 端点 return http.build(); }

@Order(1)保证这条链先于普通业务接口的链执行。applyDefaultSecurity会帮你把/oauth2/authorize、/oauth2/token、/oauth2/jwks这些端点配好,不用手写。开启 OIDC 后会多出/.well-known/openid-configuration发现端点,客户端可以靠它自动获取各端点地址,省去硬编码。

4. 资源服务器与客户端接入:JWT 校验和授权码流程

4.1 资源服务器怎么验 JWT

资源服务器不签发 token,只负责验。它需要拿到授权服务器的公钥,有两种方式:一是直接配 JWK Set URI 让它定期拉取,二是本地放公钥文件。前者更适合服务拆分:

spring: security: oauth2: resourceserver: jwt: jwk-set-uri: http://auth-server:9000/oauth2/jwks

配好之后,Spring Security 会自动用这个公钥验签、校验exp和iss。这里有个常见误解:很多人以为资源服务器也要配 client-secret,其实不需要,它只认公钥。如果你发现资源服务器启动时去连授权服务器失败,检查jwk-set-uri的网络可达性,容器部署时最容易栽在服务名解析上。

4.2 客户端发起授权码流程

客户端这边,如果用的是 Spring Security 的 oauth2-client,配置相当省心:

spring: security: oauth2: client: registration: demo-client: client-id: demo-client client-secret: demo-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: openid,read provider: demo-client: issuer-uri: http://auth-server:9000

issuer-uri一配,客户端会自动去拉/.well-known/openid-configuration,把授权端点、token 端点、jwks 地址全拿到,不用一个个手写。{baseUrl}和{registrationId}是占位符,Spring 会替换成实际值,这样本地和线上不用改配置。

4.3 手动换 token 时要注意的请求格式

如果你不用框架封装、自己用 RestTemplate 或 HttpClient 去换 token,请求体格式必须对:

curl -X POST http://auth-server:9000/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -u "demo-client:demo-secret" \ -d "grant_type=authorization_code" \ -d "code=收到的授权码" \ -d "redirect_uri=http://127.0.0.1:8080/login/oauth2/code/demo-client"

Content-Type必须是application/x-www-form-urlencoded,用 JSON 会直接 415。-u走的是 Basic 认证,对应服务端的CLIENT_SECRET_BASIC;如果你在服务端配的是CLIENT_SECRET_POST,那 client-id 和 secret 要放到请求体里。这两种方式混用是换 token 失败的高频原因,报错通常只给一个笼统的 401,得对着服务端日志看。

4.4 解析 JWT 里的自定义 claims

资源服务器验完签名后,业务代码里要拿自定义字段,可以从JwtAuthenticationToken里取:

@GetMapping("/me") public Map<String, Object> me(JwtAuthenticationToken authentication) { Jwt jwt = authentication.getToken(); return Map.of( "subject", jwt.getSubject(), "tenant", jwt.getClaim("tenant_id"), "scopes", authentication.getAuthorities() ); }

getClaim拿到的就是授权服务器在OAuth2TokenCustomizer里塞进去的值。注意 claims 是明文可读的,别往里放敏感信息,JWT 只保证不被篡改,不保证不被看见。

5. 避坑与排查:五条血泪记录

5.1 换 token 报 invalid_client

现象是授权码明明拿到了,换 token 时返回 401,body 里error是invalid_client。原因通常是客户端密钥的存储格式和校验方式不匹配——服务端存了{bcrypt}哈希,客户端传的是明文,或者反过来存了{noop}明文但客户端传了哈希。解决方式是统一:调试期用{noop}加明文,上线前改成{bcrypt}并把明文用BCryptPasswordEncoder编码后存进去,客户端始终传明文。

5.2 资源服务器验签失败 invalid signature

现象是资源服务器收到 token 后返回 401,日志里是Jwt signature validation failed。原因多半是资源服务器拉到的公钥和授权服务器签发用的私钥不是一对,常见于授权服务器重启后重新生成了密钥,而资源服务器还在用缓存的旧公钥。解决方式是确认授权服务器的密钥是持久化的(从 p12 文件加载,而不是每次启动随机生成),资源服务器配了jwk-set-uri的话它会定期刷新,实在不行重启资源服务器。

5.3 redirect_uri 不匹配

现象是授权请求直接被拒,页面提示invalid redirect_uri。原因是客户端发起请求时带的redirect_uri和授权服务器登记的不完全一致,包括协议、主机名、端口、路径、末尾斜杠。解决方式是把登记值和实际值逐字符比对,本地开发用127.0.0.1就别写成localhost,这两个在 OAuth2 里算不同地址。

5.4 启动报 NoClassDefFoundError 或版本冲突

现象是应用启动直接失败,堆栈指向spring-security-oauth2-authorization-server里的某个类找不到。原因是 Spring Boot 版本和授权服务器库版本不匹配,比如 Boot 3.2 配了 0.4.x 的库。解决方式是查官方版本对应表,Boot 3.2 用 1.2.x,Boot 3.1 用 1.1.x,Boot 3.0 用 1.0.x,别凭感觉选。

5.5 JWT 过期时间设太长导致无法吊销

现象是用户登出或改密码后,旧 token 还能继续访问资源。原因是 JWT 是自包含的,签发后服务端不存状态,过期前一直有效。解决方式是 access_token 设短一点(比如 15 分钟),配合 refresh_token 续期;确实需要即时吊销的,在资源服务器侧维护一个黑名单,或者改用不透明 token 加 introspection 端点。这是 JWT 的固有取舍,不是配置能绕过去的。

6. 装完怎么验证跑通:一条完整链路的自检清单

环境装完,别急着接业务,先按下面这条链路走一遍,任何一环断了都能立刻定位。

第一步,确认授权服务器的发现端点能访问:

curl http://auth-server:9000/.well-known/openid-configuration

返回的 JSON 里应该有authorization_endpoint、token_endpoint、jwks_uri三个字段,缺任何一个说明 OIDC 没开或过滤链没生效。

第二步,确认 JWK Set 端点能拿到公钥:

curl http://auth-server:9000/oauth2/jwks

返回的keys数组里应该有一个kty为RSA、alg为RS256的条目。如果这里是空的,说明JWKSource没注册成功,回头查密钥加载那段。

第三步,走一遍完整的授权码流程。浏览器访问授权端点,登录后拿到重定向回来的 code,再用 curl 换 token,把返回的access_token复制出来。

第四步,用这个 token 访问资源服务器的受保护接口:

curl -H "Authorization: Bearer 粘贴access_token" http://resource-server:8081/me

能返回用户信息和自定义 claims 就算通了。如果返回 401,把 token 贴到 jwt.io 上看三段结构,重点核对iss字段和资源服务器配的 issuer 是否一致,以及exp有没有过期。

第五步,验证刷新流程。等 access_token 过期后,用 refresh_token 换新的:

curl -X POST http://auth-server:9000/oauth2/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -u "demo-client:demo-secret" \ -d "grant_type=refresh_token" \ -d "refresh_token=粘贴refresh_token"

能拿到新的 access_token 且旧 refresh_token 按配置失效,整条链路才算真正跑通。

我自己的习惯是,每次改完授权服务器配置,这五步强制走一遍,尤其是第三步和第四步,因为大部分配置错误都在这两环暴露。有一次我图省事只测了发现端点就上线,结果资源服务器因为 issuer 配错一直验不过,半夜被叫起来改配置。从那以后,不管多小的改动,这条自检清单我都从头到尾跑一次,宁可多花十分钟,也不赌运气。希望这套流程能帮你少走点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询