资讯动态

SpringBoot手写OAuth2授权码模式接入码云登录实战

发布时间:2026/10/15 10:17:37 来源:尧图企业网站定制
1. 为什么要在自己的项目里接入码云授权登录先说说这个项目的来龙去脉。我手里有一个小型的内部工具站之前一直用最原始的账号密码登录用户表里存着加密后的密码。后来团队里有人提了一嘴每次注册都要想用户名、想密码、收验证码太麻烦了能不能直接用码云账号登录这个需求其实很典型——第三方授权登录也就是常说的 OAuth2 授权码模式。码云Gitee作为国内主流的代码托管平台几乎每个开发者都有账号用它来做第三方登录用户接受度高接入成本也低。这个 Demo 要解决的核心问题就是让用户不注册、不填密码直接点一个按钮用码云身份完成登录服务端拿到用户唯一标识并建立本地会话。适合谁来参考如果你正在用 SpringBoot 做 Web 项目想加一个第三方登录入口又不想被各种 OAuth 概念绕晕那这篇就是给你写的。我会把整个流程拆开从申请应用、配置参数、写回调接口到踩过的坑全部讲清楚。整个方案基于 SpringBoot 2.x RestTemplate 手写调用不依赖任何第三方 OAuth 封装库这样你能看清每一步到底发生了什么而不是被框架的黑盒遮住。需要提前说明的是OAuth2 的授权码模式Authorization Code是第三方登录里最标准、最安全的做法它把令牌交换放在服务端完成前端只负责跳转和拿 code避免了令牌暴露在浏览器里。这个 Demo 就采用这个模式。2. 整体设计思路与方案选型拆解2.1 授权码模式到底在解决什么问题很多人第一次接触 OAuth 会懵为什么不能直接让用户把码云账号密码给我答案很简单——你永远不应该拿到用户的密码。授权码模式的核心思想是用户是在码云的页面上完成身份确认的你的系统从头到尾只拿到一个临时的、一次性的授权码code然后你拿着这个 code 加上自己的应用密钥去码云后台换一个访问令牌access_token再用令牌去读用户的基本信息。打个生活化的比方你去酒店入住前台不会把万能房卡给你而是给你一张只能开你房间、且当天有效的房卡。授权码就是那张临时房卡access_token 是正式房卡而你的应用密钥client_secret就是酒店和发卡系统之间的暗号绝不能给到用户手里。整个流程分四步用户点击码云登录浏览器跳转到码云的授权页带上你的 client_id 和回调地址。用户在码云页面确认授权码云把浏览器重定向回你的回调地址并附带一个 code。你的服务端拿 code client_id client_secret 回调地址向码云令牌接口换取 access_token。你的服务端拿 access_token 调用码云用户信息接口拿到用户唯一标识id、login 等在本地建立会话或注册新用户。2.2 为什么选择手写 RestTemplate 而不是用封装库市面上有 JustAuth 这类封装库一行代码就能搞定。但我这个 Demo 故意不用原因有三个。第一排查问题方便。封装库把请求参数、响应结构都藏起来了一旦回调失败你根本不知道是 code 过期、回调地址不匹配还是密钥错了。手写的话每一步的请求和响应你都能打日志。第二理解成本低。OAuth 本身不复杂被库封装后反而显得神秘手写一遍你就彻底懂了。第三依赖少。内部工具站不想引入额外依赖RestTemplate 是 SpringBoot 自带的够用。当然如果你项目里已经用了 JustAuth那也没问题原理是一样的只是它帮你把 HTTP 调用和参数拼装做了。我这里讲的是裸奔版理解了它用任何库都能看穿。2.3 关键参数与安全考量在动手之前有几个参数必须先想清楚它们直接决定安全性和可用性参数作用安全要点client_id应用唯一标识可公开出现在跳转 URL 里client_secret应用密钥绝对保密只存服务端绝不进前端redirect_uri回调地址必须与码云后台配置完全一致否则报错state防 CSRF 随机串每次请求随机生成回调时校验scope申请权限范围一般用 user_info 即可其中state 参数是最容易被忽略的安全点。它的作用是防止跨站请求伪造你发起授权时生成一个随机字符串存进 session回调时比对不一致就拒绝。没有它攻击者可以伪造回调请求把你的账号和自己的码云账号绑定。这个 Demo 会把它加上。注意client_secret 一旦泄露别人就能冒充你的应用去换令牌。所以它只能放在服务端配置文件里且不要提交到代码仓库用环境变量或配置中心注入。3. 核心细节解析与实操要点3.1 在码云上创建应用并拿到凭证第一步是去码云后台创建第三方应用。登录码云后进入个人设置里的第三方应用或应用管理入口创建一个新应用。需要填写几个关键信息应用名称随便起用户授权页会显示。应用主页你的站点首页地址。回调地址这是重中之重必须和你代码里用的 redirect_uri 一模一样包括协议、域名、端口、路径。本地开发一般填http://localhost:8080/oauth/gitee/callback。权限范围勾选读取用户基本信息即可。创建完成后你会拿到Client ID和Client Secret。把它们记下来后面配置用。这里有个坑回调地址如果填了http://localhost有些平台会要求必须是 https码云对 localhost 是放行的但如果你用127.0.0.1可能就不认建议统一用 localhost。3.2 配置文件与依赖准备SpringBoot 项目里先在pom.xml确认有 web 依赖RestTemplate 是 spring-web 自带的不用额外引。然后在application.yml里加一段配置gitee: oauth: client-id: 你的client_id client-secret: 你的client_secret redirect-uri: http://localhost:8080/oauth/gitee/callback authorize-url: https://gitee.com/oauth/authorize token-url: https://gitee.com/oauth/token user-info-url: https://gitee.com/api/v5/user把 URL 也做成配置项好处是以后要换平台或者改地址不用改代码。然后用一个ConfigurationProperties类把它绑定成对象方便注入使用。3.3 授权跳转接口的实现要点用户点击登录按钮后请求打到你的/oauth/gitee/authorize接口。这个接口要做三件事生成随机 state、存进 session、拼装跳转 URL 并重定向。拼装的 URL 长这样https://gitee.com/oauth/authorize?client_idxxxredirect_urixxxresponse_typecodestatexxx注意response_type固定是code这是授权码模式的标志。state 用UUID.randomUUID()生成即可。存 session 时建议加个前缀比如gitee_state避免和其他业务冲突。实操心得state 存 session 后如果用户开了多个标签页同时发起登录后发起的会覆盖先前的 state导致先前的回调校验失败。内部工具站用户量小这个问题不明显但如果要做严谨可以把 state 存进一个带过期时间的缓存key 用 state 本身回调时按 key 取。3.4 回调接口与令牌交换回调接口/oauth/gitee/callback是整个流程的核心。它接收码云重定向回来的code和state。处理顺序很重要先校验 state从 session 取出之前存的和回调带来的比对不一致直接拒绝。校验 code 是否存在为空说明用户取消了授权。用 code 换 access_token这是服务端到服务端的 POST 请求。用 access_token 拉用户信息。本地建立会话或注册用户。换令牌的请求是 POST参数放在表单体里application/x-www-form-urlencoded包括grant_typeauthorization_code、code、client_id、client_secret、redirect_uri。这里有个细节redirect_uri 必须和第一步跳转时用的完全一致否则码云会拒绝。响应是一个 JSON里面有access_token。拉用户信息时把 access_token 作为查询参数拼到 URL 上GET 请求即可。返回的 JSON 里id是用户的唯一数字标识login是用户名name是昵称avatar_url是头像。本地建用户时一定要用 id 作为唯一键因为 login 和 name 用户是可以改的id 不会变。4. 完整实操过程与关键代码实现4.1 项目结构与依赖梳理整个 Demo 的目录结构很清爽核心就三个类加一个配置src/main/java/com/example/demo/ ├── config/ │ └── GiteeOauthProperties.java // 配置绑定 ├── controller/ │ └── GiteeOauthController.java // 授权与回调接口 ├── service/ │ └── GiteeOauthService.java // 令牌交换与用户信息获取 └── DemoApplication.javapom.xml里只需要spring-boot-starter-webRestTemplate 用它自带的。如果你要用 JSON 解析Jackson 也是 web starter 自带的不用额外引。这种极简依赖的好处是启动快、排查问题路径短。4.2 配置绑定类的写法ConfigurationProperties(prefix gitee.oauth) public class GiteeOauthProperties { private String clientId; private String clientSecret; private String redirectUri; private String authorizeUrl; private String tokenUrl; private String userInfoUrl; // getter / setter 省略 }在启动类上加EnableConfigurationProperties(GiteeOauthProperties.class)让它生效。这样配置和代码解耦改地址不用动 Java 代码。4.3 授权跳转接口的完整实现GetMapping(/oauth/gitee/authorize) public void authorize(HttpServletResponse response) throws IOException { String state UUID.randomUUID().toString().replace(-, ); request.getSession().setAttribute(gitee_state, state); String url UriComponentsBuilder .fromHttpUrl(properties.getAuthorizeUrl()) .queryParam(client_id, properties.getClientId()) .queryParam(redirect_uri, properties.getRedirectUri()) .queryParam(response_type, code) .queryParam(state, state) .build().toUriString(); response.sendRedirect(url); }这里用UriComponentsBuilder拼参数比手写字符串拼接安全它会自动做 URL 编码。redirect_uri 里如果有特殊字符手拼很容易出错用这个工具类省心。4.4 回调接口与令牌交换的完整实现回调接口是重头戏我把它拆成校验、换令牌、拉信息、建会话四段GetMapping(/oauth/gitee/callback) public String callback(RequestParam(required false) String code, RequestParam(required false) String state, HttpServletRequest request) { // 1. 校验 state String savedState (String) request.getSession().getAttribute(gitee_state); if (savedState null || !savedState.equals(state)) { return state 校验失败疑似 CSRF 攻击; } // 2. 校验 code if (code null || code.isEmpty()) { return 用户取消了授权; } // 3. 换令牌 String accessToken giteeOauthService.getAccessToken(code); // 4. 拉用户信息 GiteeUser user giteeOauthService.getUserInfo(accessToken); // 5. 本地建会话这里简化为存 session request.getSession().setAttribute(loginUserId, user.getId()); return 登录成功欢迎 user.getName(); }换令牌的 service 方法public String getAccessToken(String code) { MultiValueMapString, String params new LinkedMultiValueMap(); params.add(grant_type, authorization_code); params.add(code, code); params.add(client_id, properties.getClientId()); params.add(client_secret, properties.getClientSecret()); params.add(redirect_uri, properties.getRedirectUri()); HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntityMultiValueMapString, String entity new HttpEntity(params, headers); ResponseEntityMap resp restTemplate.postForEntity( properties.getTokenUrl(), entity, Map.class); return (String) resp.getBody().get(access_token); }拉用户信息public GiteeUser getUserInfo(String accessToken) { String url properties.getUserInfoUrl() ?access_token accessToken; ResponseEntityGiteeUser resp restTemplate.getForEntity(url, GiteeUser.class); return resp.getBody(); }GiteeUser是个简单的 POJO字段用JsonProperty映射码云返回的 JSON key比如id、login、name、avatar_url。4.5 参数计算与超时配置RestTemplate 默认没有超时网络一卡线程就挂住。生产环境必须配超时。我一般设连接超时 3 秒、读取超时 5 秒Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(3000); factory.setReadTimeout(5000); return new RestTemplate(factory); }为什么是这两个值连接超时 3 秒是因为码云的授权服务器响应很快超过 3 秒基本是网络问题没必要等。读取超时 5 秒是给令牌交换留的余量因为那一步涉及服务端处理。这两个值可以根据你的网络环境微调但千万不要不设超时这是新手最容易犯的错。5. 常见问题与排查技巧实录5.1 回调报错排查速查表实际接入过程中报错五花八门我整理了一张速查表基本覆盖 90% 的情况现象可能原因排查方向跳转后提示 redirect_uri 不匹配回调地址与后台配置不一致逐字符比对注意 http/https、端口、末尾斜杠回调后 state 校验失败session 丢失或 state 被覆盖检查 session 是否跨域、是否多标签页冲突换令牌返回 401client_secret 错误或 code 已用过确认密钥、确认 code 只换一次换令牌返回 400redirect_uri 与跳转时不一致两处必须完全相同拉用户信息返回 401access_token 过期或无效令牌有效期通常几小时重新授权用户 id 为 nullJSON 字段名映射错误打印原始响应核对字段名5.2 几个我踩过的坑第一个坑code 只能用一次。我一开始在回调里既换令牌又打日志结果调试时刷新页面第二次就报 code 无效。原因是授权码是一次性的换过就作废。所以调试时不要反复刷新回调页要么重新走授权要么把换到的令牌缓存起来。第二个坑session 跨域丢失。如果你的前端和后端不同域session 的 cookie 可能带不过去导致 state 校验永远失败。解决办法是前后端同域部署或者用带过期时间的服务端缓存存 state不依赖 session。第三个坑redirect_uri 末尾斜杠。后台配置http://localhost:8080/oauth/gitee/callback代码里写成.../callback/多一个斜杠就报错。这种问题最气人因为肉眼看不出。建议把 redirect_uri 只在一处定义其他地方引用同一个配置项。第四个坑用户信息字段名。码云返回的头像字段是avatar_url不是avatarUrlJava 里如果直接映射会拿到 null。用JsonProperty(avatar_url)显式指定别偷懒。5.3 安全加固的几个建议Demo 跑通只是第一步真要上线还得加固。第一state 必须校验这是防 CSRF 的底线。第二client_secret 不进代码仓库用环境变量注入。第三令牌不要存前端全程服务端持有。第四本地用户表用码云 id 做唯一键并加唯一索引防止并发注册产生重复用户。第五回调接口做频率限制防止有人拿 code 暴力试探。提示如果你要做绑定已有账号而不是直接注册新账号那在拿到码云 id 后先查本地是否已有绑定关系没有的话引导用户登录已有账号再绑定避免一个码云账号对应多个本地账号。6. 从 Demo 到可用系统的扩展思路6.1 会话管理怎么做得更稳Demo 里我用 session 存登录态简单直接。但如果你的系统要做分布式部署session 就得集中存储比如放 Redis。做法是把loginUserId写进 Rediskey 是随机生成的 token然后把 token 通过 cookie 或响应头返回给前端。这样多台服务器共享登录态用户换机器也不用重新登录。另一个思路是用 JWT。把用户 id 和过期时间签进令牌服务端不存状态验证时解签即可。好处是无状态、易扩展坏处是注销麻烦令牌签发后在过期前一直有效。内部工具站用 session 就够了对外系统建议 JWT 短期过期 刷新令牌的组合。6.2 多平台登录的统一抽象如果你以后还想接入其他平台的授权登录别把代码写死。可以抽象一个OAuthProvider接口定义getAuthorizeUrl、getAccessToken、getUserInfo三个方法每个平台一个实现类。回调接口根据路径里的平台标识路由到对应实现。这样加新平台只需加一个实现类不用改主流程。这个抽象我在另一个项目里用过接入三个平台时代码量减少了一半。6.3 用户体验上的小优化授权跳转前可以在前端加个 loading因为跳转到码云页面有网络延迟。回调成功后别只返回一句登录成功最好重定向回用户原本想访问的页面。做法是在发起授权时把目标地址存进 state 或 session回调成功后跳回去。另外如果用户之前授权过码云可能直接跳过确认页体验会更顺滑这是平台行为不用你处理。我个人在实际操作中的体会是第三方登录这东西第一次接会觉得概念多、参数杂但真正跑通一遍之后你会发现它的骨架非常清晰跳转拿 code、服务端换令牌、拉信息建会话三步走。真正花时间的不是写代码而是排查那些配置不一致的小问题。所以我的建议是把 redirect_uri、client_id、client_secret 这三个值只在一处定义其他地方全部引用能省掉一大半的调试时间。另外日志一定要打全尤其是换令牌那一步的请求参数和响应体出问题时一眼就能看出是哪个参数不对。这套 Demo 我在两个内部项目里都用过稳定跑了半年多没出过授权相关的问题你可以放心参考。

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

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

免费获取报价 →
↑