资讯动态

Java 实现 OAuth2.0 + JWT 统一授权服务端与客户端实战

发布时间:2026/10/16 9:42:35 来源:尧图企业网站定制
简介这份资源是一套基于Java的OAuth 2.0服务端与客户端完整实现面向希望深入理解授权框架、掌握令牌安全传输的Java开发者与后端学习者。项目以Maven管理依赖借助Apache OLTU库搭建授权端点、令牌端点、刷新令牌端点与资源端点并引入JWT作为访问令牌通过签名保证数据完整性与来源可信同时结合MD5处理密码哈希帮助读者厘清资源所有者、客户端、资源服务器与授权服务器四类角色的协作流程。压缩包共221个文件以176个java源码为核心辅以22个xml配置、properties与json等资源文件整体约355KB结构紧凑便于按模块研读。内容涵盖AuthorizationServerConfig、ResourceServerConfig、OAuth2AuthenticationProvider、OAuth2AccessTokenGenerator等关键类的实现思路读者可据此搭建可运行的授权与资源服务理解令牌签发、校验与刷新机制并掌握JWT在OAuth 2.0中的落地方式。目前已有2184人学习下载适合作为授权体系入门与实战参考。1. 从一次联调翻车说起这套 OAuth2 JWT 服务端客户端到底解决什么问题去年帮一个做企业内部工具的朋友看联调问题场景很典型他们有一套自研的用户中心另外还有三个内部系统工单、报表、运维面板每个系统都自己存了一份账号密码用户改一次密码要同步三处运维同事离职后账号还散落在各个库里。他们想上一套统一授权但又不想引入太重的外部依赖最后选了自己用 Java 实现一套 OAuth2.0 服务端加客户端令牌格式用 JWT。这套东西的价值就在这儿——把「谁能访问什么」从各个业务系统里抽出来收敛到一个授权服务器业务系统只认令牌不认密码。这份资源就是围绕这个场景展开的一个可运行的 OAuth2.0 授权服务端配套一个客户端示例令牌用 JWT 自包含签发。适合两类人一类是想搞懂 OAuth2 四种授权模式到底怎么在代码里落地、不想只看 RFC 文档的开发者另一类是手上正好有多个内部系统要做单点登录、想找一份能直接改的骨架代码的团队。下面我按「它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改」的顺序拆一遍。2. 项目结构与技术选型授权服务器和资源客户端是怎么分层的2.1 整体模块划分这类项目通常拆成两个独立可启动的模块一个负责发令牌一个负责用令牌。从工程结构上看常见做法是这样组织的oauth2-demo/ ├── auth-server/ # 授权服务器 │ ├── src/main/java/.../ │ │ ├── config/ # 安全配置、令牌配置 │ │ ├── controller/ # 授权、令牌、用户信息端点 │ │ ├── service/ # 客户端注册、用户认证、令牌签发 │ │ └── store/ # 客户端信息、令牌存储 │ └── src/main/resources/ │ ├── application.yml │ └── keystore.jks # JWT 签名用的密钥库 ├── resource-client/ # 资源客户端业务系统侧 │ ├── src/main/java/.../ │ │ ├── config/ # 资源服务器安全配置 │ │ ├── controller/ # 受保护的业务接口 │ │ └── filter/ # 令牌解析、上下文注入 │ └── src/main/resources/ │ └── application.yml └── pom.xml # 父工程统一依赖版本授权服务器对外暴露几个关键端点/oauth/authorize负责授权码模式的授权页跳转/oauth/token负责用授权码或密码换令牌/oauth/check_token给资源服务器做令牌校验如果不用 JWT 自校验的话/oauth/userinfo返回当前令牌对应的用户信息。资源客户端这边则是一个标准的资源服务器拦截请求、解析 Bearer 令牌、把用户身份塞进安全上下文。2.2 技术栈与依赖选择技术选型上这套东西基本围绕 Spring 生态走因为 Spring Security OAuth2 把协议细节封装得比较完整自己从零写过滤器链容易在签名校验、令牌过期这些地方翻车。核心依赖大致是依赖作用备注spring-boot-starter-webWeb 容器与 MVC两个模块都要spring-boot-starter-security安全过滤链认证与授权基础spring-security-oauth2OAuth2 协议实现授权服务器核心spring-security-jwtJWT 编解码令牌自包含jjwt 或 nimbus-jose-jwtJWT 工具库视版本而定mysql-connector 或 h2客户端/令牌存储演示可用内存库这里有个选型上的取舍值得说清楚JWT 令牌是自包含的资源服务器拿到令牌后本地验签就能拿到用户信息不需要每次回授权服务器查代价是令牌一旦签发在过期前很难主动吊销。所以常见做法是 JWT 里只放非敏感的声明用户 ID、角色、过期时间敏感信息仍然走/oauth/check_token或用户信息端点查。这个权衡在后面改造章节还会展开。2.3 授权模式怎么选OAuth2 有四种授权模式这套项目一般会实现其中两到三种授权码模式authorization_code最完整、最安全适合有后端能力的 Web 应用令牌不经过浏览器前端。密码模式password用户直接把账号密码交给客户端客户端去换令牌适合自家高度信任的客户端比如内部系统。客户端模式client_credentials没有用户参与纯服务间调用适合后台任务。简化模式implicit令牌直接从前端拿安全性差现在基本不推荐。内部系统做单点登录最常用的是授权码模式如果客户端本身就是自家后端密码模式写起来最省事。项目里通常两种都留了配置开关改application.yml里的authorized-grant-types就能切换。3. 跑起来从零启动授权服务器和客户端3.1 环境准备与数据库初始化先把环境对齐避免版本玄学。建议 JDK 8 或 11Maven 3.6MySQL 5.7如果不想装数据库把存储换成内存实现也能跑。数据库里至少要有两张表客户端信息表和令牌存储表用 JWT 的话令牌表可以省掉但授权码还是要存。-- 客户端注册表记录每个接入系统的 client_id / secret / 回调地址 CREATE TABLE oauth_client_details ( client_id VARCHAR(256) PRIMARY KEY, client_secret VARCHAR(256) NOT NULL, scope VARCHAR(256), authorized_grant_types VARCHAR(256), -- 允许的授权模式逗号分隔 web_server_redirect_uri VARCHAR(256), -- 授权码模式的回调地址 authorities VARCHAR(256), access_token_validity INT, -- 令牌有效期秒 refresh_token_validity INT ); -- 授权码存储表授权码模式临时用用完即删 CREATE TABLE oauth_code ( code VARCHAR(256), authentication BLOB );authorized_grant_types这一列是权限控制的关键写authorization_code,refresh_token就只允许这两种模式写错了会在换令牌时直接报unauthorized_grant_type。access_token_validity建议内部系统设 7200 秒左右太长等于给令牌吊销挖坑。3.2 授权服务器配置授权服务器的核心是一个继承AuthorizationServerConfigurerAdapter的配置类把客户端来源、令牌签发方式、端点安全策略三件事配好。Configuration EnableAuthorizationServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { Autowired private AuthenticationManager authenticationManager; Autowired private DataSource dataSource; // 1. 客户端信息来源从数据库读支持动态注册 Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } // 2. 令牌端点安全密码模式需要 AuthenticationManager Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints .authenticationManager(authenticationManager) .accessTokenConverter(jwtAccessTokenConverter()) // 用 JWT 签发 .reuseRefreshTokens(false); // 每次刷新换新 refresh_token } // 3. 端点访问策略令牌端点允许表单认证 Override public void configure(AuthorizationServerSecurityConfigurer security) { security .tokenKeyAccess(permitAll()) // 暴露公钥给资源服务器验签 .checkTokenAccess(isAuthenticated()) // check_token 需要认证 .allowFormAuthenticationForClients(); } Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter new JwtAccessTokenConverter(); converter.setSigningKey(your-rsa-private-key); // 生产环境用密钥库别硬编码 return converter; } }三个configure方法分别管三件事clients决定「哪些客户端能来换令牌」endpoints决定「令牌怎么签发、刷新策略是什么」security决定「哪些端点谁能访问」。reuseRefreshTokens(false)是个容易被忽略的点——设成 true 时刷新令牌会复用一旦泄露影响面更大设成 false 每次刷新都换新的配合刷新令牌轮换更安全。setSigningKey这里演示用了对称密钥生产上建议换成 RSA 非对称密钥资源服务器只拿公钥验签私钥不出授权服务器。3.3 客户端接入与令牌获取客户端这边要做两件事一是配置资源服务器二是拿到令牌后带着它访问业务接口。先看资源服务器的配置Configuration EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/api/public/**).permitAll() // 公开接口放行 .antMatchers(/api/**).authenticated() // 其余接口要令牌 .anyRequest().authenticated(); } Bean public JwtTokenStore jwtTokenStore() { // 用 JWT 时资源服务器本地验签不需要回授权服务器 return new JwtTokenStore(jwtAccessTokenConverter()); } Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter new JwtAccessTokenConverter(); converter.setSigningKey(your-rsa-private-key); // 与授权服务器一致 return converter; } }配置好之后用密码模式换令牌就是一条 curl 的事# 密码模式换令牌client_id/secret 走 Basic 认证用户凭证走表单 curl -X POST http://localhost:8080/oauth/token \ -H Authorization: Basic $(echo -n web-client:secret | base64) \ -d grant_typepassword \ -d usernamealice \ -d password123456 \ -d scoperead返回的 JSON 里access_token就是 JWTrefresh_token用来续期。拿这个令牌访问业务接口curl -X GET http://localhost:8081/api/user/profile \ -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...如果返回 401先看令牌有没有带对前缀Bearer再看资源服务器和授权服务器的签名密钥是否一致——这两个不一致是最常见的 401 来源。4. 核心代码拆解JWT 令牌从签发到校验的完整链路4.1 令牌签发JWT 里到底放了什么JWT 由三部分组成头部算法、载荷声明、签名。授权服务器签发时会把用户身份和权限塞进载荷。默认的JwtAccessTokenConverter会写入user_name、scope、exp、authorities这些字段但业务上往往需要更多信息比如用户 ID、租户 ID。常见做法是自定义一个TokenEnhancerpublic class CustomTokenEnhancer implements TokenEnhancer { Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { MapString, Object info new HashMap(); // 从认证信息里取出业务用户塞进 JWT 载荷 Object principal authentication.getPrincipal(); if (principal instanceof UserDetails) { UserDetails user (UserDetails) principal; info.put(userId, user.getUsername()); info.put(tenantId, resolveTenant(user)); // 自定义业务字段 } ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }然后在AuthorizationServerEndpointsConfigurer里把 enhancer 链进去endpoints.tokenEnhancer(enhancer).accessTokenConverter(converter)。注意顺序enhancer 要在 converter 之前否则附加信息不会进 JWT。载荷里放什么要有节制——JWT 是 Base64 编码不是加密任何人都能解开看所以别放手机号、身份证这类敏感字段只放标识和权限。4.2 令牌校验资源服务器怎么本地验签资源服务器拿到 JWT 后JwtTokenStore会做三件事验签、检查过期、解析权限。验签用的是和授权服务器一致的密钥对称或公钥非对称。校验通过后OAuth2Authentication被放进SecurityContext业务代码里就能直接取当前用户RestController RequestMapping(/api/user) public class UserController { GetMapping(/profile) public MapString, Object profile(Authentication authentication) { MapString, Object result new HashMap(); result.put(username, authentication.getName()); // 从 JWT 附加信息里取自定义字段 if (authentication instanceof OAuth2Authentication) { OAuth2Authentication oauth (OAuth2Authentication) authentication; MapString, Object details (MapString, Object) oauth.getOAuth2Request().getExtensions().get(userId); result.put(userId, details); } return result; } }这里有个细节authentication.getName()拿到的通常是 JWT 里的user_name而自定义字段要通过OAuth2Request的扩展信息取。如果取不到八成是签发时 enhancer 没生效或者字段名对不上。调试时可以把 JWT 贴到任意在线解析工具里看载荷确认字段真的写进去了。4.3 刷新令牌与过期处理访问令牌过期后客户端不该让用户重新登录而是用刷新令牌换新的访问令牌curl -X POST http://localhost:8080/oauth/token \ -H Authorization: Basic $(echo -n web-client:secret | base64) \ -d grant_typerefresh_token \ -d refresh_tokeneyJhbGciOiJIUzI1NiIs...刷新令牌的有效期一般比访问令牌长得多比如访问令牌 2 小时刷新令牌 7 天。客户端要做的判断是请求返回 401 且错误码是invalid_token就用刷新令牌换一次换到新令牌后重试原请求如果刷新也失败才跳登录。这个重试逻辑建议封装在 HTTP 客户端拦截器里别散落在每个业务调用处。5. 避坑与排查联调时最容易翻车的几个点5.1 401 但令牌明明没过期现象客户端带着令牌访问接口返回 401但令牌解析出来exp还没到。原因九成是资源服务器和授权服务器的签名密钥不一致或者一个用对称密钥一个用非对称公钥。JWT 验签失败时Spring Security 默认不告诉你具体原因只给 401。解决把两边的signingKey或密钥库配置对齐确认算法HS256 / RS256一致。调试时可以在资源服务器打开logging.level.org.springframework.securityDEBUG验签失败的堆栈会打出来。5.2 授权码模式回调报 redirect_uri 不匹配现象授权页点同意后跳回客户端报redirect_uri_mismatch。原因oauth_client_details表里web_server_redirect_uri配的地址和请求里带的redirect_uri不完全一致包括端口、路径末尾斜杠、http/https 的差异。解决两边逐字符比对别靠肉眼。多个回调地址用逗号分隔存在同一字段里。本地开发常用localhost和127.0.0.1混用这俩在 OAuth2 校验里算不同地址统一成一个。5.3 密码模式报 unauthorized_grant_type现象用密码模式换令牌返回unauthorized_grant_type。原因客户端注册时authorized_grant_types字段没写password或者写成了passwd之类的错拼。解决查oauth_client_details表确认字段值是password多个模式用逗号分隔且不带空格。改完记得清缓存有些实现会把客户端信息缓存起来。5.4 JWT 载荷里的自定义字段取不到现象签发时明明塞了userId资源服务器里取出来是 null。原因TokenEnhancer没生效或者 enhancer 和 converter 的执行顺序反了附加信息在转 JWT 之前没写进去。解决确认endpoints.tokenEnhancer(enhancer).accessTokenConverter(converter)的调用顺序enhancer 在前。再把令牌贴到解析工具里看载荷确认字段真的在 JWT 里而不是只在内存对象里。5.5 令牌无法主动吊销现象用户登出或权限变更后旧令牌在过期前仍然能用。原因JWT 是自包含的资源服务器本地验签不查授权服务器所以授权服务器删了令牌记录也没用。解决这是 JWT 的固有特性不是 bug。要支持吊销常见做法是维护一个短有效期的访问令牌比如 15 分钟加一个黑名单或者关键操作回授权服务器做一次check_token。别指望 JWT 能像服务端 session 那样随时失效。6. 二次开发把演示骨架改成能上内部系统的授权中心拿到这套代码后真正要改的地方通常集中在三块存储、密钥管理、令牌策略。先说存储——演示里常用内存或 H2上生产要换成 MySQL 或 Redis。客户端信息走数据库令牌如果用 JWT 可以不落库但授权码和刷新令牌建议放 Redis 并设过期时间避免数据库膨胀。改的时候注意ClientDetailsServiceConfigurer从inMemory换成jdbc(dataSource)同时把建表脚本跑一遍。密钥管理是第二个重点。演示里setSigningKey直接写字符串生产上必须换成密钥库。用 RSA 的话授权服务器持有私钥签发资源服务器只配公钥验签这样即使某个资源服务器被攻破也拿不到签发能力。生成密钥库的命令大致是keytool -genkeypair -alias oauth2 -keyalg RSA -keysize 2048 \ -keystore oauth2.jks -validity 3650 \ -storepass changeit -keypass changeit然后在配置里用KeyStoreKeyFactory加载把JwtAccessTokenConverter的setKeyPair设成从密钥库取出的密钥对。资源服务器那边只导出公钥用setVerifierKey配置。这一步做完签名密钥就不会出现在代码和配置文件里了。令牌策略是第三个要动的地方。默认访问令牌有效期可能偏长内部系统建议压到 15 到 30 分钟配合刷新令牌轮换。刷新令牌的有效期按业务定报表类系统可以长一点运维面板这种敏感系统短一点。另外建议在 JWT 里加一个jti令牌唯一 ID配合 Redis 黑名单实现「准实时吊销」——登出时把jti写进黑名单资源服务器验签后多查一次黑名单。这个查询有性能开销但比每次回授权服务器查轻得多。验证改造是否成功我一般会走一遍完整链路密码模式换令牌 → 带令牌访问受保护接口 → 手动改令牌一个字符确认验签失败 → 等令牌过期确认刷新流程 → 登出后确认黑名单生效。这五步走通基本就能上内部环境试用了。说个我自己的教训早期做这类授权服务时我图省事把签名密钥硬编码在代码里结果代码一提交到仓库密钥就跟着泄露了后来不得不紧急轮换所有令牌。从那以后我每次接 OAuth2 项目第一件事就是把密钥从代码里挪出去走密钥库或环境变量并且强制在 CI 里加一条密钥扫描。这个习惯帮我省了不少后悔药。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑