用php做的网站有哪些网站开发用哪种语言做好
2025/12/31 21:25:35 网站建设 项目流程
用php做的网站有哪些,网站开发用哪种语言做好,烟台广告公司南网站建设评价,网页怎么截图前言 第三方登录#xff08;OAuth2#xff09;看似简单#xff1a;用户扫码#xff0c;登录成功。但在实际工程落地中#xff0c;涉及到底层网络代理、参数配置管理、安全校验#xff08;State#xff09;、以及**“未绑定账号如何优雅处理”**等复杂的业务逻辑。 本文…前言第三方登录OAuth2看似简单用户扫码登录成功。但在实际工程落地中涉及到底层网络代理、参数配置管理、安全校验State、以及**“未绑定账号如何优雅处理”**等复杂的业务逻辑。本文将基于JustAuth开源库与Yudao框架的源码实现抽丝剥茧还原一个企业级第三方登录的全流程。流程图流程图方便配合后面具体的文字细节帮助理解情况一用户已将第三方与本系统账户绑定情况二用户未将第三方与本系统账户绑定流程细节第一阶段发起授权The Request核心逻辑前端索要“通行证”后端组装“申请单”。1. 前端发起请求用户点击“GitHub 登录”前端向后端接口/social-auth-redirect发起请求。关键参数type20标识社交平台类型如 GitHub。redirectUri回调地址。这是 GitHub 办完事后要把用户送回来的地址必须与 GitHub 后台白名单一致。2. 后端构建 AuthRequest核心工厂模式后端接收到请求后通过AuthRequestFactory构建核心请求对象。这里采用了一个混合配置策略Step 2.1加载基础架构配置YAML通过JustAuthProperties读取application.yaml中的配置。作用初始化HTTP 代理Proxy和超时时间。这是为了解决国内服务器访问 GitHub 经常超时的问题。Step 2.2加载业务配置Database根据type查询数据库social_client表。作用获取最新的ClientId和ClientSecret。设计亮点利用反射机制将 DB 中的业务参数覆盖到 YAML 的基础配置对象中。这样既保证了网络连通性又实现了业务参数的动态热更新换 ID 无需重启服务。3. 生成 State 与 授权 URL生成防伪印章State后端调用AuthStateUtils.createState()生成随机字符串。缓存 StateRedis通过 IOC 注入的RedisStateCache将state存入 Redis设置短暂过期时间如 3 分钟。目的防止 CSRF 攻击。组装 URL调用AuthRequest.authorize(state)(缓存state其实是在这个函数内被执行的)生成最终的跳转链接https://github.com/login/oauth/authorize?client_id...redirect_uri...statexyz...返回前端后端将这个长链接直接返回给前端。第二阶段用户交互The Interaction核心逻辑用户在第三方平台操作系统完全不可见。浏览器跳转前端拿到 URL执行window.location.href跳转。用户授权用户在 GitHub 页面输入账号密码或扫码点击“同意授权”。携带信物返回GitHub 生成一个临时的code并将浏览器重定向回前端传入的redirectUri地址变为/social-login?codetemp_code_123statexyz...第三阶段后端回调与分流策略核心逻辑优先查库“复用”兜底请求“换票”。前端监听到回调路由提取 URL 中的code和state调用后端登录接口。后端authSocialUser方法执行以下严格顺序1. 优先查库后端首先根据codestate查询social_user表。这是为了处理“账号绑定”等需要复用 Code 的场景。分支 A命中缓存查到了场景用户处于“绑定账号”的第二步操作或者是极短时间内的重试。逻辑既然数据库里有记录说明该请求在不久前已经通过了第三方校验。直接返回数据库中的SocialUserDO信息。结果跳过Redis 校验跳过GitHub 请求因为 Code 已失效直接进入绑定或登录流程。分支 B未命中没查到场景这是用户第一次发起登录或者是全新的请求。逻辑代码继续向下执行调用 JustAuth 的核心方法authRequest.login(callback)。2. 标准校验与换票进入分支 B 后authRequest.login()内部会自动执行标准流程验明正身 (State Check)SDK 内部自动调用RedisStateCache查找state。如果 Redis 中不存在或已过期抛出异常拦截攻击。以码换人 (Exchange)校验通过后请求 GitHub 接口换取 AccessToken 和 UserInfo。数据落库 (Snapshot)Yudao 获取到 GitHub 信息后立即将其插入/更新到social_user表包含code和state。目的这就是为了让下一次请求能进入“分支 A”。3. 业务结果判定的后续处理核心逻辑基于关联表的三岔路口——是直接放行还是通过异常机制触发“填表流程”当后端经过第二步的分支A或B拿到SocialUserDO包含 OpenID 等社交信息后认证流程并未结束反而刚刚开始。系统需要判断“这个拿着 GitHub 身份证的人到底是不是我公司的员工”这一步的逻辑严格分为两个分支Step 3.1跨表查询关联关系后端使用socialType社交平台类型和socialUserId即刚才获取到的SocialUserDO的主键或 OpenID去查询sys_social_user_bind关联表。SELECTuser_idFROMsys_social_user_bindWHEREsocial_type20ANDsocial_user_id{当前GitHub用户的ID}Step 3.2分支 A —— 已绑定老用户丝滑入境如果查询到了关联记录说明该 GitHub 账号已经与本系统的一个UserId例如1024绑定过了。查询主账号根据查到的user_id去sys_user表查询系统用户详情。状态校验检查该用户是否被禁用、是否离职等安全风控。记录日志写入登录日志表记录“用户通过 GitHub 登录成功”。发放令牌基于UserId创建LoginUser对象。生成系统的AccessToken和RefreshToken。返回结果直接返回 HTTP 200 和 Token前端收到后直接跳转首页。用户感知扫码后屏幕一闪直接进系统无感知。Step 3.3分支 B —— 未绑定触发“中断”机制如果sys_social_user_bind表里查不到记录说明这是一个“熟悉的陌生人”——我们认识这个 GitHub 账号sys_social_user里有但他还没关联到任何内部账号。此时后端绝对不能返回成功但也不能返回普通的 500 错误。定义异常类型后端会抛出一个特定的业务异常例如ServiceException并携带一个特殊的错误码例如AUTH_THIRD_PARTY_NOT_BIND。中断流程该异常会打断当前的登录链路Token 生成步骤不会执行。返回给前端前端收到这个异常响应解析错误码。关键动作前端识别出“未绑定”信号自动跳转到**“账号绑定/注册页面”**。携带上下文抛出异常时不需要返回具体的 OpenID因为前端手里还捏着那个code和state或者是后端的reqId这将在下一步绑定时作为凭证。给读者的提示 源码划重点在部分实现中并不是先校验 State。这是因为social_user表充当了“持久化的 State 缓存”。只有当 DB 里没有记录时才会回退到标准的 OAuth2 流程校验 Redis - 请求 GitHub。这种设计完美兼容了“Code 一次性”与“业务多步走”的矛盾。第四阶段异常处理与账号绑定The Binding核心逻辑复用失效的 Code完成逻辑闭环。1. 前端处理异常前端捕获到“未绑定异常”跳转到“账号绑定/登录页”提示用户输入本系统的账号密码。2. 发起绑定请求用户点击“绑定并登录”前端发送usernamepasswordcode(复用之前的参数)。3. 后端“偷天换日”后端再次接收到带有code的请求再次查库根据codestate查询social_user表。命中缓存由于第三阶段已经落库这次查到了记录直接返回代码直接返回数据库中的SocialUser对象里面包含 OpenID跳过了向 GitHub 请求的步骤。妙处规避了“Code 已失效”的错误利用本地数据完成了逻辑接力。完成绑定验证账号密码正确。在social_user_bind表中插入UserId与SocialUserId的关联。生成 JWT Token返回给前端。关键变量数据流向变量流程图具体流向在 OAuth2 的复杂的交互中有 4 个关键变量贯穿始终。理解它们的流转路径是掌握整个安全体系的钥匙。1.ClientIdClientSecret(身份凭证)角色应用的“身份证”和“密码”。来源静态application.yaml(兜底配置)。动态MySQLsocial_client表 (业务配置优先级更高)。流向配置加载后端启动或请求时通过AuthRequestFactory从 DB 读取。分道扬镳ClientId拼接到授权 URL 中 -暴露给前端/浏览器- 发送给 GitHub。ClientSecret严禁泄露给前端。它只停留在后端服务器由后端通过 HTTP 请求直接发给 GitHub 用于换取 Token。2.State(防伪印章 联合主键)在 部分系统 的设计中state拥有双重身份前半生是CSRF 令牌后半生是本地业务索引键。1. 诞生与缓存 (Creation Cache)执行位置AuthController-AuthRequestFactory-AuthRequest.authorize()代码细节调用AuthStateUtils.createState()生成一个 32 位随机字符串例如uuid-1234。AuthRequest内部调用RedisStateCache.cache(state)。物理存储位置Redis 服务器。Key 格式justauth:state:{socialType}:{state值}(例如justauth:state:20:uuid-1234)。TTL默认为 3 分钟auth.cache.timeout配置。作用此时它是“门票存根”证明请求是由本系统发出的。2. 漂流 (Transmission)载体HTTP 302 重定向 URL。形态https://github.com/login...?stateuuid-1234。流转从服务器内存-用户浏览器地址栏-GitHub 服务器。注意GitHub 服务器仅仅是“保管”一下这个参数回调时原样退回。3. 验证与销毁 (Verify Delete)执行位置SocialUserServiceImpl-AuthRequest.login()代码细节SDK 内部调用RedisStateCache.get(state)。比对Redis 里有没有这个 Key有验证通过并立即执行redisTemplate.delete(key)防止重放攻击。无抛出AuthException(Illegal state)。状态变更至此state在 Redis 中的使命结束。4. 持久化与转生 (Persistence)执行位置SocialUserServiceImpl.authSocialUser代码细节socialUserMapper.insert(newSocialUserDO().setState(state)// state 被写入 MySQL);物理存储MySQLsystem_social_user表的state字段。作用变更此时state不再用于防 CSRF而是和code一起组成了本次社交会话的唯一标识。2.Code(一次性令牌 绑定凭证)code是 OAuth2 的核心1. 诞生 (Generation)执行位置第三方服务器GitHub/DingTalk。触发时机用户在第三方页面点击“同意授权”的一瞬间。性质极其短暂的有效期通常 1-5 分钟且只能交换一次 Token。2. 漂流 (Transmission)载体HTTP 回调 URLCallback URL。形态http://yoursite.com/social-login?codegh_xc9...state...。流转GitHub 服务器-用户浏览器-Nginx-Spring Boot Controller。3. 消费 (Consumption)执行位置AuthRequest.login()-AuthGithubRequest.getAccessToken()动作后端构造 HTTP POST 请求发送给 GitHub。参数包含client_id,client_secret,code。结果成功换回access_token和用户信息。GitHub 端状态该code在 GitHub 侧被标记为**“已失效”**。4. 落库 (Persistence - 关键一步)执行位置SocialUserServiceImpl.authSocialUser代码细节// 即使 code 已经在 GitHub 失效了我们依然把它存进数据库socialUser.setCode(code);socialUserMapper.insertOrUpdate(socialUser);物理存储MySQLsystem_social_user表的code字段。战略意义这相当于给这次“登录行为”拍了一张快照。5. 复用 (Reuse - 绑定阶段)场景用户在前端“绑定账号”页面点击“绑定”。入参前端将之前的code和state再次传给后端接口/auth/login。执行位置SocialUserServiceImpl.authSocialUser第一行代码。逻辑// 拿着前端传来的失效 Code去 MySQL 里找快照selectByTypeAndCodeAnState(type,code,state);结果查到了之前存的SocialUserDO里面含有 OpenID。后端假装Code 还没失效直接返回了用户信息从而绕过了 GitHub 的校验。实现代码1. 接口层 (Controller)提供两个核心入口social-login用于已绑定用户的直接登录login用于常规登录或未绑定用户的“账号绑定登录”。// AuthController.javaRestControllerRequestMapping(/system/auth)publicclassAuthController{ResourceprivateAuthServiceauthService;/** * 场景A社交快捷登录 * 适合已绑定社交账号的老用户前端直接携带 code state 请求 */PostMapping(/social-login)PermitAllpublicCommonResultAuthLoginRespVOsocialQuickLogin(RequestBodyValidAuthSocialLoginReqVOreqVO){returnsuccess(authService.socialLogin(reqVO));}/** * 场景B账号密码登录兼具绑定功能 * 1. 常规登录只传 username password * 2. 绑定并登录传 username password socialType code state */PostMapping(/login)PermitAllpublicCommonResultAuthLoginRespVOlogin(RequestBodyValidAuthLoginReqVOreqVO){returnsuccess(authService.login(reqVO));}}2. 登录业务层这里处理登录的分流逻辑是直接颁发 Token还是抛出异常引导绑定亦或是处理绑定逻辑。// AdminAuthServiceImpl.javaServicepublicclassAdminAuthServiceImplimplementsAuthService{OverridepublicAuthLoginRespVOsocialLogin(AuthSocialLoginReqVOreqVO){// 1. 使用 code 换取社交用户信息// 注意底层 authSocialUser 会自动处理 code 的持久化实现“一次性 code”的复用准备SocialUserRespDTOsocialUsersocialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(),reqVO.getType(),reqVO.getCode(),reqVO.getState());// 2. 关键判断如果该社交账号未绑定系统用户抛出特定异常if(socialUsernull||socialUser.getUserId()null){// 前端捕获此异常后跳转到绑定页面throwexception(AUTH_THIRD_LOGIN_NOT_BIND);}// 3. 如果已绑定直接获取系统用户并登录AdminUserDOuseruserService.getUser(socialUser.getUserId());returncreateTokenAfterLoginSuccess(user.getId(),user.getUsername(),LoginLogTypeEnum.LOGIN_SOCIAL);}OverridepublicAuthLoginRespVOlogin(AuthLoginReqVOreqVO){// 1. 账号密码常规验证AdminUserDOuserauthenticate(reqVO.getUsername(),reqVO.getPassword());// 2. 绑定逻辑如果请求中携带了社交参数 (socialType 非空)if(reqVO.getSocialType()!null){// 执行绑定内部会复用之前存储在 DB 中的 code/state 记录无需再次请求第三方socialUserService.bindSocialUser(newSocialUserBindReqDTO(user.getId(),getUserType().getValue(),reqVO.getSocialType(),reqVO.getSocialCode(),reqVO.getSocialState()));}// 3. 创建 Token 返回returncreateTokenAfterLoginSuccess(user.getId(),reqVO.getUsername(),LoginLogTypeEnum.LOGIN_USERNAME);}}3. 核心业务层这是整个架构中最精髓的部分。authSocialUser方法通过优先查询数据库实现了对 OAuth2 Code 的“拦截与复用”。// SocialUserServiceImpl.javaServicepublicclassSocialUserServiceImplimplementsSocialUserService{/** * 核心方法获取社交用户信息 * 策略DB 缓存优先 - 兜底请求第三方 - 落库保存 */NotNullpublicSocialUserDOauthSocialUser(IntegersocialType,IntegeruserType,Stringcode,Stringstate){// 1. 【优先查库】尝试从 DB 获取解决 Code 二次使用问题// 如果用户是在“绑定”阶段前端传来的 code 已经在之前的请求中落库了这里直接返回避免报错SocialUserDOsocialUsersocialUserMapper.selectByTypeAndCodeAnState(socialType,code,state);if(socialUser!null){returnsocialUser;}// 2. 【请求第三方】如果 DB 没查到说明是新请求。调用 JustAuth 请求第三方平台// 内部会校验 Redis State通过 code 换取 access_token 和 openidAuthUserauthUsersocialClientService.getAuthUser(socialType,userType,code,state);// 3. 【落库保存】将第三方返回的信息OpenID, Token以及本次的 Code State 持久化到 DB// 这一步是后续能够复用 Code 的关键socialUsersocialUserMapper.selectByTypeAndOpenid(socialType,authUser.getUuid());if(socialUsernull){socialUsernewSocialUserDO();socialUserMapper.insert(socialUser.setType(socialType).setCode(code).setState(state).setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()));}else{// 更新 Code 和 State确保最新的请求也能被缓存socialUserMapper.updateById(socialUser.setCode(code).setState(state).setToken(authUser.getToken().getAccessToken()));}returnsocialUser;}/** * 绑定逻辑 */publicStringbindSocialUser(SocialUserBindReqDTOreqDTO){// 1. 获取社交用户复用 authSocialUser 逻辑此时走 DB 缓存分支SocialUserDOsocialUserauthSocialUser(reqDTO.getSocialType(),reqDTO.getUserType(),reqDTO.getCode(),reqDTO.getState());// 2. 清理旧绑定关系防止脏数据socialUserBindMapper.deleteByUserTypeAndSocialUserId(reqDTO.getUserType(),socialUser.getId());// 3. 插入新的绑定关系SocialUserBindDOsocialUserBindSocialUserBindDO.builder().userId(reqDTO.getUserId()).userType(reqDTO.getUserType()).socialUserId(socialUser.getId()).socialType(socialUser.getType()).build();socialUserBindMapper.insert(socialUserBind);returnsocialUser.getOpenid();}}

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

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

立即咨询