OAuth2 GitHub 登录实现
OAuth2 GitHub 登录实现文章目录OAuth2 GitHub 登录实现1. 概述整体流程2. 配置2.1 application.yml2.2 GitHub OAuth App 配置3. 后端核心实现3.1 SecurityConfig — OAuth2 登录配置3.2 UserDetailsServiceImpl — 双重角色3.3 OAuth2 成功处理器3.4 GitHub 用户属性映射3.5 本地用户创建3.6 数据库表结构4. 前端实现4.1 登录页面 —— 触发 GitHub 登录4.2 OAuth2 回调页面 —— 接收 Token5. 完整数据流6. 安全性分析7. 注意事项1. 概述本系统使用Spring Security OAuth2 Client实现 GitHub 第三方登录。用户通过 GitHub 授权后系统自动拉取 GitHub 用户信息转换为本地用户并签发 JWT 双 Token 供后续鉴权。整体流程┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌───────────┐ │ 前端 │ │ 后端 │ │ GitHub │ │ 数据库 │ │(Vue 3) │ │(Spring Boot) │ │ │ │ (MySQL) │ └────┬─────┘ └──────┬───────┘ └────┬─────┘ └─────┬─────┘ │ │ │ │ │ 1. 点击 GitHub │ │ │ │ 登录按钮 │ │ │ │────────────────│ │ │ │ │ │ │ │ 2. 重定向到 │ │ │ │ GitHub 授权页│ │ │ │────────────────│ │ │ │ │ │ │ │ 3. 用户授权 │ │ │ │──────────────────────────────────│ │ │ │ │ │ │ 4. GitHub 回调 │ │ │ │ 带上授权码 │ │ │ │────────────────│ │ │ │ │ │ │ │ │ 5. 用授权码 │ │ │ │ 换取 Access │ │ │ │ Token │ │ │ │────────────────│ │ │ │ │ │ │ │ 6. 返回用户信息 │ │ │ │────────────────│ │ │ │ │ │ │ │ 7. 查找/创建 │ │ │ │ 本地用户 │ │ │ │────────────────│ │ │ │ │ │ │ │ 8. 签发 JWT │ │ │ │ 重定向前端 │ │ │ 9. 前端收到 │ │ │ │ Token, 保存 │ │ │ │ 跳转主页 │ │ │ │────────────────│ │ │2. 配置2.1 application.ymlspring:security:oauth2:client:registration:github:client-id:Ov23liRI4TMbCclient-secret:c0a2194994743d17d856b5ee49scope:read:user,user:email配置项说明获取方式client-idGitHub OAuth App 的 Client IDGitHub Settings → Developer settings → OAuth Appsclient-secretGitHub OAuth App 的 Client Secret同上scope请求的权限范围read:user获取用户基本信息,user:email获取邮箱2.2 GitHub OAuth App 配置在 GitHub 上创建 OAuth App 时Authorization callback URL必须设置为http://localhost:8080/login/oauth2/code/github这是 Spring Security OAuth2 Client 的默认回调路径由框架自动处理无需手动实现。3. 后端核心实现3.1 SecurityConfig — OAuth2 登录配置BeanpublicSecurityFilterChainsecurityFilterChain(HttpSecurityhttp)throwsException{http// ... 其他配置 ....oauth2Login(oauth2-oauth2.userInfoEndpoint(userInfo-userInfo.userService(userDetailsService)// ← 自定义 OAuth2UserService).successHandler(this::oauth2SuccessHandler)// ← 自定义成功处理器)// ...}配置说明方法作用.userInfoEndpoint().userService()指定自定义的OAuth2UserService用于处理 GitHub 返回的用户信息.successHandler()自定义登录成功后的处理逻辑签发 JWT 重定向前端3.2 UserDetailsServiceImpl — 双重角色ServicepublicclassUserDetailsServiceImplextendsDefaultOAuth2UserServiceimplementsUserDetailsService{// 角色1: UserDetailsService — 用户名密码登录时使用OverridepublicUserDetailsloadUserByUsername(Stringusername){// 从数据库查询本地用户}// 角色2: OAuth2UserService — OAuth2 登录时使用OverridepublicOAuth2UserloadUser(OAuth2UserRequestuserRequest){OAuth2UseroAuth2Usersuper.loadUser(userRequest);// 调用 GitHub API// 包装为 DefaultOAuth2User指定用户名字段为 idreturnnewDefaultOAuth2User(Collections.singletonList(newSimpleGrantedAuthority(ROLE_USER)),oAuth2User.getAttributes(),id// nameAttributeKey GitHub 用户 ID);}}为什么继承DefaultOAuth2UserServiceSpring Security OAuth2 Client 通过OAuth2UserService使用 GitHub 返回的 Access Token 调用 GitHub 用户 API (https://api.github.com/user)获取详细的用户信息。继承DefaultOAuth2UserService复用这个默认行为super.loadUser(userRequest)内部完成了用授权码换取 Access Token由OAuth2AuthorizedClientService自动完成用 Access Token 调用 GitHub API 获取用户信息返回包含完整用户属性的OAuth2User对象nameAttributeKey的作用DefaultOAuth2User构造函数需要指定一个属性名作为用户的唯一标识。GitHub 的id字段是数字类型唯一且不可变适合作为nameAttributeKey。之后通过oAuth2User.getName()即可获取该值。3.3 OAuth2 成功处理器privatevoidoauth2SuccessHandler(HttpServletRequestrequest,HttpServletResponseresponse,Authenticationauthentication)throwsIOException{OAuth2Useroauth2User(OAuth2User)authentication.getPrincipal();// 1. 提取 GitHub 用户信息StringproviderGITHUB;StringproviderIdoauth2User.getAttribute(id).toString();Stringusernameoauth2User.getAttribute(login);Stringemailoauth2User.getAttribute(email);Stringavataroauth2User.getAttribute(avatar_url);// 2. 查找本地用户根据 provider providerIdvaruseruserService.findByProviderAndProviderId(provider,providerId);if(usernull){// 3. 首次登录 → 自动创建本地用户useruserService.createOAuth2User(username,email,avatar,provider,providerId);}// 4. 签发 JWT 双 TokenStringaccessTokenjwtTokenProvider.generateAccessToken(user.getUsername(),user.getEmail(),user.getAvatar());StringrefreshTokenjwtTokenProvider.generateRefreshToken(user.getUsername(),true);// 5. 重定向到前端Token 放在 URL 参数中response.sendRedirect(http://localhost:5173/oauth2/callback?tokenaccessTokenrefreshTokenrefreshToken);}成功处理器的关键步骤步骤说明提取用户信息从OAuth2User.getAttributes()中获取 GitHub 返回的用户属性查找/创建本地用户通过providerproviderId这一对唯一组合确定用户身份签发 JWT生成 Access Token Refresh Token重定向前端将 Token 以 URL 参数形式传给前端OAuth2Callback页面3.4 GitHub 用户属性映射GitHub 属性类型映射到本地字段说明idIntegerprovider_idGitHub 用户唯一 IDloginStringusernameGitHub 用户名emailStringemail用户邮箱可能为 nullavatar_urlStringavatar用户头像 URLprovider固定值provider固定为GITHUB3.5 本地用户创建publicUsercreateOAuth2User(Stringusername,Stringemail,Stringavatar,Stringprovider,StringproviderId){UserusernewUser();user.setUsername(username);user.setPassword(passwordEncoder.encode(oauth2_user_System.currentTimeMillis()));// 随机密码user.setEmail(email);user.setAvatar(avatar);user.setProvider(provider);user.setProviderId(providerId);user.setEnabled(true);user.setCreatedAt(LocalDateTime.now());userMapper.insert(user);returnuser;}OAuth2 用户与本地注册用户的区别对比项本地注册用户OAuth2 用户providerLOCALGITHUBprovider_idnullGitHub 用户 IDpasswordBCrypt 加密随机字符串不可用于登录avatar可选GitHub 头像 URL3.6 数据库表结构CREATETABLEIFNOTEXISTSusers(idBIGINTNOTNULLAUTO_INCREMENTPRIMARYKEY,usernameVARCHAR(255)NOTNULLUNIQUE,passwordVARCHAR(255)NOTNULL,emailVARCHAR(255)DEFAULTNULL,avatarVARCHAR(255)DEFAULTNULL,providerVARCHAR(50)NOTNULLDEFAULTLOCAL,-- LOCAL / GITHUBprovider_idVARCHAR(255)DEFAULTNULL,-- GitHub 用户 IDenabledTINYINT(1)NOTNULLDEFAULT1,created_atDATETIMENOTNULLDEFAULTCURRENT_TIMESTAMP)ENGINEInnoDBDEFAULTCHARSETutf8mb4;关键索引(provider, provider_id)作为 OAuth2 用户的唯一标识对应查询SELECT*FROMusersWHEREproviderGITHUBANDprovider_id12345678;4. 前端实现4.1 登录页面 —— 触发 GitHub 登录// LoginPage.vuefunctionhandleGithubLogin(){window.location.hrefhttp://localhost:8080/oauth2/authorization/github}直接跳转到 Spring Security 的 OAuth2 授权端点后端自动处理授权流程。4.2 OAuth2 回调页面 —— 接收 Token// OAuth2Callback.vueonMounted(async(){consttokenroute.query.tokenasstringconstrefreshTokenroute.query.refreshTokenasstringif(tokenrefreshToken){authStore.setTokens(token,refreshToken)// 保存双 Tokentry{constresawaitgetUserInfo()// 获取用户信息const{username,avatar}res.data.data authStore.setUser(username,avatar)router.push(/dashboard)}catch{router.push(/dashboard)// 即使获取用户信息失败也跳转}}else{ElMessage.error(登录失败未获取到认证信息)router.push(/login)}})为什么回调页面需要调用/api/auth/user后端 OAuth2 成功处理器在重定向时没有返回用户信息用户名、头像因为 URL 长度有限且安全考虑。前端需要用自己的 Access Token 调用/api/auth/user获取用户信息然后存入 Pinia Store 和 localStorage。5. 完整数据流以下是一个用户首次使用 GitHub 登录的完整请求链路Step 1: 前端跳转 GitHub 授权 GET http://localhost:8080/oauth2/authorization/github → 302 重定向到 https://github.com/login/oauth/authorize?client_idxxxredirect_urihttp://localhost:8080/login/oauth2/code/github Step 2: 用户同意授权 → GitHub 302 重定向到 http://localhost:8080/login/oauth2/code/github?codexxx Step 3: 后端用 code 换取 access_token → POST https://github.com/login/oauth/access_token Step 4: 后端用 access_token 获取用户信息 → GET https://api.github.com/user ← { id: 12345678, login: zhangsan, email: zhangsanexample.com, avatar_url: https://... } Step 5: 查找/创建本地用户 → SELECT * FROM users WHERE provider GITHUB AND provider_id 12345678 → 未找到 → INSERT INTO users ... Step 6: 签发 JWT 并重定向前端 → 302 重定向到 http://localhost:5173/oauth2/callback?tokenxxxrefreshTokenyyy Step 7: 前端保存 Token 并获取用户信息 → GET /api/auth/user (Authorization: Bearer xxx) ← { username: zhangsan, email: ..., avatar: ... } Step 8: 跳转仪表盘 → router.push(/dashboard)6. 安全性分析风险点防护措施授权码泄露授权码一次性使用有效期短秒级GitHub Access Token 泄露仅后端持有不经过前端不会泄露CSRF 攻击Spring Security OAuth2 Client 默认使用 state 参数防 CSRF重定向 URL 篡改GitHub 会验证 redirect_uri 与注册时一致伪造回调后端用 code client_secret 向 GitHub 换取 token伪造无效7. 注意事项GitHub 邮箱可能为空如果用户在 GitHub 上设置了隐私模式隐藏邮箱email字段可能为null。此时user.getEmail()返回nullJWT claims 中 email 会设为空字符串。用户名冲突如果已存在同名的本地注册用户GitHub 登录的login字段会因UNIQUE约束而插入失败。实际中需考虑用户名冲突处理策略如添加后缀或前缀。OAuth2 状态管理当前实现未要求自定义loginPage使用 Spring Security 默认的 OAuth2 端点路径避免了自定义路径与内置回调路径冲突导致的循环重定向。Session 管理虽然设置了SessionCreationPolicy.STATELESS但 OAuth2 流程中 Spring Security 需要在 Session 中临时保存OAuth2AuthorizationRequest含 state 参数因此 OAuth2 流程会短暂使用 Session流程完成后后续请求通过 JWT 鉴权。注册 GitHub OAuth App 时的注意事项Homepage URL 填http://localhost:5173Authorization callback URL 填http://localhost:8080/login/oauth2/code/github生产环境需要将localhost替换为实际域名