尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

uni-app微信登录全链路实现:Spring Boot后端与小程序前端整合指南

uni-app微信登录全链路实现:Spring Boot后端与小程序前端整合指南 1. 项目概述为什么选择 uni-app 实现微信登录在移动应用开发领域跨端框架的兴起极大地提升了开发效率。uni-app 作为其中的佼佼者凭借“一套代码发布到多个平台”的能力成为了许多开发者的首选。当我们谈论一个应用的用户体系时登录功能是基石而微信登录因其庞大的用户基数和便捷的授权体验几乎成为了国内移动应用的标配。将这两者结合——在 uni-app 项目中集成微信登录就成为了一个高频且核心的开发需求。这个需求看似简单无非是前端调用微信的 SDK 获取一个临时凭证然后传给后端去交换用户信息。但实际操作起来你会发现它横跨了前端、后端、微信开放平台/小程序后台等多个环节涉及 OAuth2.0 授权、会话管理、用户信息同步等一系列技术点。任何一个环节的疏漏都可能导致登录失败、用户体验不佳甚至安全风险。因此深入理解其背后的流程和细节对于构建一个稳定、安全、用户体验良好的登录系统至关重要。本文将从零开始以一个典型的“uni-app 微信小程序 Spring Boot 后端”架构为例拆解微信登录的完整实现链路并分享我在多个项目中积累的实操经验和避坑指南。2. 核心流程与架构设计解析微信登录的流程本质上是标准的 OAuth2.0 授权码模式的一个变种。对于小程序和 App微信提供了略有差异的接口但其核心思想一致应用不直接获取用户的微信密码而是通过微信的授权页面获得一个代表用户身份的“令牌”再用这个令牌去换取用户的基本信息。2.1 微信登录的三种主要场景在开始之前我们必须明确 uni-app 项目可能运行的平台因为不同平台的实现方式有显著区别微信小程序这是最直接、最标准的场景。uni-app 编译为微信小程序代码后可以直接使用微信小程序原生的wx.login()API 获取code。整个授权流程在微信客户端内完成用户体验最流畅。微信内嵌网页H5当 uni-app 运行在微信浏览器内时需要使用微信的“网页授权”流程。这需要引导用户跳转到微信的授权页面用户确认后微信会带着code重定向回我们指定的前端页面。App非微信环境如果 uni-app 打包成 App并且希望在 App 内实现微信登录则需要集成微信开放平台的移动应用 SDK。用户点击登录时会唤起微信 App 进行授权授权完成后跳转回我们的 App。由于“微信小程序”是 uni-app 最主流、最典型的场景且相关热搜词也集中于此本文将以此为重点进行详细阐述。H5 和 App 的场景会作为扩展思路在注意事项中提及。2.2 整体架构与数据流一个健壮的微信登录系统通常涉及以下组件和数据流uni-app 前端 (小程序) - Spring Boot 后端服务器 - 微信接口服务器具体流程可以分为以下五个关键步骤前端发起登录用户在小程序中点击“微信登录”按钮前端调用uni.login()其底层会调用微信的wx.login()获取一个临时登录凭证code。这个code有效期仅 5 分钟且每次调用都会刷新。前端发送 Code 至后端前端将获取到的code连同可能需要的小程序appid可从 manifest.json 读取或后端配置通过 HTTPS 请求发送给自己的 Spring Boot 后端服务器。后端凭证校验与服务端通信后端接收到code后需要做两件核心事情用 code 换 session_key 和 openid后端携带小程序的appid、secret在小程序管理后台获取必须严格保密以及前端传来的code调用微信的auth.code2Session接口。微信服务器会返回openid用户在当前小程序下的唯一标识和session_key会话密钥。生成自定义登录态为了安全和不频繁调用微信接口后端需要生成自己的会话标识。通常我们会用openid查询或创建本地用户记录然后生成一个自定义的token如 JWT或sessionId并将其与openid、session_key的关联关系存储在缓存如 Redis中。后端返回自定义登录态后端将生成的token返回给前端。前端维持登录状态前端收到token后将其存储在本地如uni.setStorageSync。后续所有需要认证的 API 请求都在请求头如Authorization: Bearer token中携带此token。后端拦截器会校验token的有效性并获取对应的用户信息。注意session_key是微信返回的敏感信息绝对不应该通过网络传输给前端。它主要用于后端进行数据解密如获取加密的手机号和签名验证。前端只需要关心code和token。3. 前端 (uni-app) 实现详解前端的工作相对清晰核心是获取code并安全地传递给后端。3.1 获取登录凭证 Code在 uni-app 中我们使用uni.login方法来获取 code。为了更好的兼容性和错误处理建议进行封装。// utils/login.js export const wxLogin () { return new Promise((resolve, reject) { uni.login({ provider: weixin, // 使用微信登录提供商 success: (loginRes) { if (loginRes.code) { // 成功获取到 code resolve(loginRes.code); } else { reject(new Error(微信登录失败 loginRes.errMsg)); } }, fail: (err) { reject(new Error(调用登录接口失败 err.errMsg)); } }); }); };关键点解析provider: weixin明确指定微信登录提供商。在 uni-app 中这能确保编译到不同平台时调用正确的底层 API。异步处理使用 Promise 封装便于在 Vue 组件中使用async/await让逻辑更清晰。错误处理必须处理success中但code为空以及fail的情况。网络问题、用户快速取消等都可能导致失败。3.2 发送请求并处理登录响应获取code后需要将其发送到后端。这里涉及网络请求和状态管理。template button clickhandleLogin微信一键登录/button /template script import { wxLogin } from /utils/login.js; import { loginByWeixin } from /api/user.js; // 假设封装了后端的登录接口 export default { methods: { async handleLogin() { uni.showLoading({ title: 登录中..., mask: true }); try { // 1. 获取微信 code const code await wxLogin(); // 2. 调用后端登录接口传递 code const res await loginByWeixin({ code: code }); // 3. 假设后端返回了 token 和用户基础信息 const { token, userInfo } res.data; // 4. 存储登录态 uni.setStorageSync(token, token); uni.setStorageSync(userInfo, userInfo); // 5. 更新 Vuex 状态如果使用 this.$store.commit(user/SET_TOKEN, token); this.$store.commit(user/SET_USER_INFO, userInfo); // 6. 登录成功反馈 uni.showToast({ title: 登录成功, icon: success }); // 7. 跳转或返回 const pages getCurrentPages(); if (pages.length 1) { uni.navigateBack(); } else { uni.switchTab({ url: /pages/home/index }); // 跳转到首页 } } catch (error) { uni.showToast({ title: error.message || 登录失败, icon: none }); } finally { uni.hideLoading(); } } } } /scriptAPI 请求封装示例 (api/user.js)import request from /utils/request; // 基于 uni.request 封装的请求库 export function loginByWeixin(data) { return request({ url: /api/auth/wx-login, method: POST, data }); }3.3 前端登录状态维护登录成功后如何维持状态是整个应用的关键。Token 存储使用uni.setStorageSync存储token。Sync方法是同步的确保在后续请求发出前 token 已就位。对于敏感信息可以考虑使用uni.setStorage异步方法。请求拦截器在封装的request工具中需要自动为每个请求添加Authorization头。// utils/request.js const service uni.request; // 请求拦截 const request (options) { const token uni.getStorageSync(token); if (token) { options.header { ...options.header, Authorization: Bearer ${token} }; } return new Promise((resolve, reject) { service({ ...options, success: (res) resolve(res.data), fail: (err) reject(err) }); }); }; export default request;登录检查与拦截在需要登录的页面如“个人中心”可以在onLoad或onShow生命周期中检查 token 是否存在若不存在则跳转到登录页。onShow() { const token uni.getStorageSync(token); if (!token) { uni.redirectTo({ url: /pages/login/index }); return; } // ... 加载页面数据 }4. 后端 (Spring Boot) 实现详解后端是微信登录逻辑的核心负责与微信服务器通信、业务逻辑处理和会话管理。我们将使用 Spring Boot MyBatis-Plus 来实现。4.1 环境准备与依赖首先在pom.xml中添加必要依赖dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 用于HTTP请求调用微信接口 -- dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId scopetest/scope /dependency !-- 推荐使用 OkHttp 或 RestTemplate这里以OkHttp为例 -- dependency groupIdcom.squareup.okhttp3/groupId artifactIdokhttp/artifactId version4.10.0/version /dependency !-- MyBatis-Plus -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- 数据库驱动以MySQL为例 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- Redis 用于缓存 session_key 和 token -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- JWT 生成 token -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies在application.yml中配置微信小程序信息和 Rediswechat: mp: app-id: your-appid # 小程序AppID secret: your-secret # 小程序AppSecret spring: redis: host: localhost port: 6379 password: database: 0 datasource: url: jdbc:mysql://localhost:3306/your_db?useUnicodetruecharacterEncodingutf8 username: root password: your_password4.2 核心实体与 DTO 设计我们需要设计用户表以及用于接收请求和响应的数据传输对象。1. 用户实体 (User)import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; Data TableName(t_user) public class User { TableId(type IdType.AUTO) private Long id; private String openid; // 微信开放平台唯一标识小程序用 private String unionid; // 微信开放平台统一标识同主体下唯一 private String nickname; private String avatarUrl; private Integer gender; private String country; private String province; private String city; private String language; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }2. 微信登录请求 DTOimport lombok.Data; import javax.validation.constraints.NotBlank; Data public class WxLoginDTO { NotBlank(message code不能为空) private String code; // 如果需要可以传递用户非敏感信息如昵称、头像由前端在登录后上传 // private String rawData; // private String signature; }3. 微信code2Session接口响应封装import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; Data public class WxSessionResult { JsonProperty(openid) private String openid; JsonProperty(session_key) private String sessionKey; JsonProperty(unionid) private String unionid; JsonProperty(errcode) private Integer errcode; JsonProperty(errmsg) private String errmsg; public boolean isSuccess() { return errcode null || errcode 0; } }4.3 服务层与微信服务器交互创建一个服务来处理与微信服务器的通信逻辑。import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import okhttp3.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.IOException; Slf4j Service public class WeChatService { Value(${wechat.mp.app-id}) private String appId; Value(${wechat.mp.secret}) private String secret; private static final String CODE2SESSION_URL https://api.weixin.qq.com/sns/jscode2session; private final OkHttpClient client new OkHttpClient(); private final ObjectMapper objectMapper new ObjectMapper(); /** * 根据前端传来的 code调用微信接口获取 session_key 和 openid * param code 登录凭证 * return WxSessionResult */ public WxSessionResult getSessionInfo(String code) { String url String.format(%s?appid%ssecret%sjs_code%sgrant_typeauthorization_code, CODE2SESSION_URL, appId, secret, code); Request request new Request.Builder().url(url).get().build(); try (Response response client.newCall(request).execute()) { if (!response.isSuccessful()) { log.error(请求微信接口失败 code: {}, 响应码: {}, code, response.code()); throw new RuntimeException(微信服务暂时不可用); } String body response.body().string(); log.debug(微信接口返回: {}, body); WxSessionResult result objectMapper.readValue(body, WxSessionResult.class); if (!result.isSuccess()) { log.error(微信接口返回错误code: {}, errcode: {}, errmsg: {}, code, result.getErrcode(), result.getErrmsg()); // 可以根据不同的 errcode 抛出更具体的业务异常 throw new RuntimeException(登录凭证无效或已过期); } return result; } catch (IOException e) { log.error(调用微信接口网络异常, e); throw new RuntimeException(网络异常请稍后重试); } } }4.4 业务逻辑层登录与用户管理这是最核心的业务层负责整合微信服务、用户数据库操作和 Token 生成。import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; import java.util.concurrent.TimeUnit; Slf4j Service RequiredArgsConstructor public class AuthService { private final WeChatService weChatService; private final UserService userService; // MyBatis-Plus 的 Service private final JwtTokenUtil jwtTokenUtil; // 自定义的JWT工具类 private final StringRedisTemplate redisTemplate; private static final String REDIS_SESSION_KEY_PREFIX wx:session:; private static final long SESSION_EXPIRE_HOURS 72; // session_key 缓存时间 Transactional public LoginResult wxLogin(String code) { // 1. 用 code 换取 session_key 和 openid WxSessionResult wxSession weChatService.getSessionInfo(code); String openid wxSession.getOpenid(); String sessionKey wxSession.getSessionKey(); String unionid wxSession.getUnionid(); // 2. 根据 openid 查找或创建用户 User user userService.getOne(new LambdaQueryWrapperUser().eq(User::getOpenid, openid)); boolean isNewUser false; if (user null) { user new User(); user.setOpenid(openid); user.setUnionid(unionid); // 新用户可以设置默认昵称和头像或者等待前端上传 user.setNickname(微信用户_ openid.substring(0, 8)); userService.save(user); isNewUser true; log.info(新用户注册openid: {}, openid); } // 3. 生成自定义登录态 Token (这里以JWT为例) String token jwtTokenUtil.generateToken(user.getId().toString(), user.getOpenid()); // 4. 将 session_key 存入 Redis (用于后续解密手机号等敏感信息) // KEY 的设计要能关联到用户这里用 openid 作为一部分 String sessionRedisKey REDIS_SESSION_KEY_PREFIX openid; redisTemplate.opsForValue().set(sessionRedisKey, sessionKey, SESSION_EXPIRE_HOURS, TimeUnit.HOURS); // 5. 返回结果 LoginResult result new LoginResult(); result.setToken(token); result.setUserInfo(convertToVO(user)); // 将User实体转为前端需要的VO对象 result.setIsNewUser(isNewUser); return result; } // 一个简单的 JWT 工具类示例 Component public static class JwtTokenUtil { Value(${jwt.secret:your-secret-key}) // 从配置读取 private String secret; private long expiration 86400L; // 24小时 public String generateToken(String userId, String openid) { MapString, Object claims new HashMap(); claims.put(sub, userId); claims.put(openid, openid); claims.put(created, new Date()); return Jwts.builder() .setClaims(claims) .setExpiration(new Date(System.currentTimeMillis() expiration * 1000)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // ... 其他校验和解析方法 } }4.5 控制层提供 API 接口最后创建一个 RESTful 接口供前端调用。import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/auth) RequiredArgsConstructor public class AuthController { private final AuthService authService; PostMapping(/wx-login) public ResultLoginResult wxLogin(RequestBody Validated WxLoginDTO dto) { LoginResult loginResult authService.wxLogin(dto.getCode()); return Result.success(loginResult); } // 统一的返回结果封装 Data public static class ResultT { private int code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(success); result.setData(data); return result; } } }5. 安全增强与最佳实践实现基础功能后我们必须考虑安全性这是生产环境不可或缺的一环。5.1 防范 Code 被恶意刷取code的有效期很短5分钟且一个code只能兑换一次session_key。但恶意用户仍可能通过脚本快速获取大量code来请求你的后端造成不必要的微信接口调用和服务器压力。解决方案前端增加人机验证或请求频率限制。图形验证码在点击“微信登录”前先要求用户完成一个简单的图形验证码。虽然对用户体验有轻微影响但能有效阻止脚本。请求签名前端在发送code时同时发送一个基于时间戳和固定盐值的签名。后端校验签名是否有效且时间戳在合理窗口内如5分钟。// 前端生成签名 const timestamp Date.now(); const salt your-frontend-salt; // 可以写死在前端虽然不完全安全但增加破解成本 const sign md5(code${code}t${timestamp}salt${salt}); // 使用简单的MD5或更安全的算法 // 将 code, timestamp, sign 一起发送给后端IP 频率限制在 Spring Boot 后端使用拦截器或过滤器结合 Redis 对同一 IP 在短时间内调用登录接口的次数进行限制。5.2 Session_Key 的安全存储与使用session_key是解密用户加密数据如getPhoneNumber获取的手机号的钥匙。必须安全存储。存储位置如上述代码所示存储在 Redis 中并设置合理的过期时间建议与微信的session_key有效期一致或略短。Key 设计使用与用户强关联的 Key如wx:session:${openid}。这样在需要解密时能快速定位。绝不网络传输再次强调session_key不能返回给前端也不能在日志中明文打印。5.3 用户信息更新与同步首次登录时我们可能只有openid。通常我们需要用户的昵称、头像等信息。方案一登录后立即获取推荐前端在登录成功后立即调用uni.getUserProfile注意2022年后已调整需用户主动触发按钮获取用户信息并上传至后端更新。button open-typegetUserInfo getuserinfoonGetUserInfo授权用户信息/button在onGetUserInfo回调中将加密的encryptedData和iv发送给后端后端用存储的session_key解密然后更新用户表。方案二使用时再获取将获取用户信息的逻辑放在“个人资料页”等需要展示的地方按需拉取和更新。5.4 Token 过期与刷新JWT Token 有过期时间。前端需要在请求接口时处理401 Unauthorized错误。前端拦截 401在全局请求拦截器中当响应状态码为 401 时尝试使用refresh_token如果有或直接引导用户重新登录。后端提供刷新接口可以设计一个/api/auth/refresh接口接受一个未过期的refresh_token另一种长效令牌来换取新的access_token即之前的 JWT。无感刷新在发起业务请求前先检查access_token是否即将过期如剩余时间小于5分钟。如果是则在后台静默调用刷新接口获取新 token然后重试原请求。这个过程对用户透明。6. 常见问题排查与实战技巧在实际开发中你一定会遇到各种问题。这里记录了一些典型问题和解决方法。6.1 前端常见问题Q1:uni.login失败返回errMsg: “login:fail”可能原因网络问题、小程序基础库版本过低、或微信客户端临时故障。排查检查手机网络。在真机上调试查看 console 更详细的错误信息。确保manifest.json中已正确配置微信小程序的appid。尝试重启微信或小程序。Q2: 登录成功后后续请求后端仍返回未登录可能原因Token 未正确携带检查请求拦截器是否工作正常Authorization头是否正确添加。Token 存储失败检查uni.setStorageSync是否在异步操作完成前被调用例如在success回调外。跨域问题仅H5确保后端配置了正确的 CORS 策略。Q3: 如何获取用户手机号方法使用button open-type”getPhoneNumber”。用户点击授权后在getphonenumber回调中会获得一个加密数据encryptedData和初始向量iv。后端解密将这两个参数和当前用户的openid从已登录的 token 中解析发送到后端。后端根据openid从 Redis 中取出对应的session_key然后使用 AES-128-CBC 算法进行解密即可获得手机号。// 解密工具方法示例 public String decryptPhoneNumber(String encryptedData, String iv, String sessionKey) throws Exception { byte[] keyBytes Base64.getDecoder().decode(sessionKey); byte[] ivBytes Base64.getDecoder().decode(iv); byte[] dataBytes Base64.getDecoder().decode(encryptedData); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted cipher.doFinal(dataBytes); String result new String(decrypted, StandardCharsets.UTF_8); // result 是一个 JSON 字符串包含 phoneNumber 等字段 return JSON.parseObject(result).getString(phoneNumber); }6.2 后端常见问题Q1: 调用code2Session接口返回errcode: 40029含义code无效已过期或被使用过。解决确保前端传递的code是新鲜的、未被使用过的。检查前端是否重复发送了同一个code。Q2: 调用code2Session接口返回errcode: 40163含义code已被使用。解决同上。确保你的后端逻辑对同一个code只处理一次。可以在 Redis 中为每个成功兑换的code设置一个短期标记如1分钟防止重放攻击。Q3: 解密用户信息或手机号失败可能原因session_key 不匹配解密用的session_key和加密时的session_key不是同一个。确保解密时使用的是该用户最近一次登录时兑换的session_key。数据被篡改encryptedData或iv在传输过程中出错。可以尝试让前端重新获取。编码问题确保 Base64 解码正确。Q4: 如何在高并发下保证登录性能缓存策略用户信息、session_key务必使用 Redis 缓存避免频繁查询数据库。异步处理登录成功后更新用户最后登录时间等非关键操作可以放入消息队列异步执行。数据库优化在openid字段上建立唯一索引加快查询速度。6.3 部署与上线检查清单项目上线前请逐一核对以下事项检查项前端后端配置manifest.json中的微信小程序appid已改为正式环境的。application-prod.yml中的appid和secret已替换为正式环境的。域名业务域名、服务器域名已在微信小程序后台正确配置包括request合法域名。服务器已备案域名解析正确SSL 证书已部署必须 HTTPS。安全已考虑对登录按钮做防重复点击处理。secret已妥善保管未提交到代码仓库。接口已做频率限制和签名验证。监控已加入关键步骤的日志上报或异常监控。关键接口登录、解密已打点监控错误日志已接入告警。兼容已测试基础库最低版本兼容性。已进行压力测试确保登录接口能承受预期流量。7. 扩展思考从登录到用户体系实现微信登录只是第一步。一个完整的用户体系还需要考虑更多绑定手机号微信登录获取的手机号是虚拟的部分情况。对于需要实名或强联系的应用需要引导用户绑定真实手机号。账号合并同一个用户可能通过不同小程序或公众号登录产生多个openid。如果应用下有多个关联产品需要使用unionid来识别同一用户并在后台进行账号合并。权限与角色基于 JWT 的token可以在 payload 中嵌入用户角色信息后端通过拦截器实现接口级别的权限控制。退出登录前端清除本地token和用户信息。后端可以将该token加入黑名单Redis在有效期内拒绝其访问。微信登录是一个入口它为我们带来了便捷的授权和用户标识。但构建一个健壮、可扩展、安全的用户中心还需要我们在业务逻辑、数据设计和系统架构上投入更多思考。希望这篇从原理到实践从代码到避坑的详细解析能帮助你在下一个 uni-app 项目中稳稳地搞定微信登录。
返回列表