
简介这是一套面向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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring Security 6OAuth2 授权服务器在这个包里 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-authorization-server/artifactId version1.2.0/version /dependency !-- JWT 的编码与签名用 Nimbus 这套Spring 内部也是它 -- dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-jose/artifactId /dependency客户端侧如果只是做授权码登录额外引spring-boot-starter-oauth2-client就够了。这里有个版本取舍spring-security-oauth2-authorization-server的 1.x 对应 Boot 3.20.4.x 对应 Boot 3.0/3.1版本对不上会在启动时直接报NoClassDefFoundError不是运行到某个接口才炸所以启动日志一定要看全。2.3 JWT 签名方式对称还是非对称先想清楚JWT 的签名算法决定了你后面密钥怎么配。HS256 是对称的服务端和资源端共用同一个密钥配置简单但密钥一旦泄露全线失守RS256 是非对称的授权服务器用私钥签、资源服务器用公钥验适合服务拆分部署。我一般会选 RS256因为资源服务器不需要持有能签发的密钥权限最小化。生成密钥对可以用 JDK 自带的 keytoolkeytool -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 JWKSourceSecurityContext 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和OAuth2TokenCustomizerBean public JwtEncoder jwtEncoder(JWKSourceSecurityContext jwkSource) { return new NimbusJwtEncoder(jwkSource); } Bean public OAuth2TokenCustomizerJwtEncodingContext 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:9000issuer-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_typeauthorization_code \ -d code收到的授权码 \ -d redirect_urihttp://127.0.0.1:8080/login/oauth2/code/demo-clientContent-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 MapString, 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 时返回 401body 里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.xBoot 3.1 用 1.1.xBoot 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_typerefresh_token \ -d refresh_token粘贴refresh_token能拿到新的 access_token 且旧 refresh_token 按配置失效整条链路才算真正跑通。我自己的习惯是每次改完授权服务器配置这五步强制走一遍尤其是第三步和第四步因为大部分配置错误都在这两环暴露。有一次我图省事只测了发现端点就上线结果资源服务器因为 issuer 配错一直验不过半夜被叫起来改配置。从那以后不管多小的改动这条自检清单我都从头到尾跑一次宁可多花十分钟也不赌运气。希望这套流程能帮你少走点弯路。本文还有配套的精品资源点击获取