微信小程序开发(三)对接登录
微信小程序开发(三)对接登录
简要
微信小程序登录:并不是我们自己去实现微信登录。
而是我们通过api去判断是否微信有没有登录,然后再微信登录的情况下,我们才能获取到用户信息,从而存储用户,进而让该用户访问我们的发布的微信小程序。
本地开发调试前提
要把 “不校验合法域名、web-view(业务域名)、TLS版本以及 HTTPS 证书” 这个选项给勾选上

一、微信小程序获取用户信息 (需了解的)
目前微信用户的真实信息在某些高版本的情况下已经无法获取了。
基础库版本查看位置
api 说明 游客身份是否可触发 备注 wx.getSetting 获取用户当前的授权状态 不可触发 wx.getUserInfo 获取用户信息 可以触发 预计自2021年4月13日起,getUserInfo将不再弹出弹窗,并直接返回匿名的用户个人信息 wx.getUserProfile 获取用户信息 不可以触发 替代 wx.getUserInfo
弹窗:基础库 2.26.2 以上不支持弹窗
用户信息:弹窗允许后,就会获取到用户信息(基础库 2.26.2 以上被匿名了)
官网简直整幺儿子😥以下这个就是 弹窗:
- wx.getSetting 与 wx.getUserInfo 一起用的情形(官网提供的)
<!-- 如果只是展示用户头像昵称,可以使用 <open-data /> 组件 --> <open-data type="userAvatarUrl"></open-data> <open-data type="userNickName"></open-data> <!-- 需要使用 button 来授权登录 --> <button wx:if="{{canIUse}}" open-type="getUserInfo" bindgetuserinfo="bindGetUserInfo">授权登录</button> <view wx:else>请升级微信版本</view>Page({ data: { canIUse: wx.canIUse('button.open-type.getUserInfo') }, onLoad: function() { // 查看是否授权 wx.getSetting({ success (res){ if (res.authSetting['scope.userInfo']) { // 已经授权,可以直接调用 getUserInfo 获取头像昵称 wx.getUserInfo({ success: function(res) { console.log(res.userInfo) } }) } } }) }, bindGetUserInfo (e) { console.log(e.detail.userInfo) } })
- wx.getSetting 与 wx.getUserProfile一起用的情形(官网提供的)
<view class="container"> <view class="userinfo"> <block wx:if="{{!hasUserInfo}}"> <button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button> <button wx:else open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button> </block> <block wx:else> <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image> <text class="userinfo-nickname">{{userInfo.nickName}}</text> </block> </view> </view>Page({ data: { userInfo: {}, hasUserInfo: false, canIUseGetUserProfile: false, }, onLoad() { if (wx.getUserProfile) { this.setData({ canIUseGetUserProfile: true }) } }, getUserProfile(e) { // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认 // 开发者妥善保管用户快速填写的头像昵称,避免重复弹窗 wx.getUserProfile({ desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写 success: (res) => { this.setData({ userInfo: res.userInfo, hasUserInfo: true }) } }) }, getUserInfo(e) { // 不推荐使用getUserInfo获取用户信息,预计自2021年4月13日起,getUserInfo将不再弹出弹窗,并直接返回匿名的用户个人信息 this.setData({ userInfo: e.detail.userInfo, hasUserInfo: true }) }, })
现阶段情况:
微信这个接口一直在改,现在(2023.11.06)对于引用的微信库版本>2.27.1 时, wx.getUserProfile不再返回真实用户信息了并且也不再弹出授权框(详见微信官方公告)
总结得知:
- wx.getUserProfile 和 wx.getUserInfo 都不提供真实的用户信息了。
包括从它们两者提供 encryptedData 中解密出来的用户数据,也是不含的真实的用户信息。- 用户微信小程序登录成功后,让用户自己改一下微信小程序里面的个人的昵称和头像吧
二、小程序登录接入方式一: wx.login()
重点:微信小程序登录:并不是我们自己去实现微信登录。
而是我们通过api去判断是否微信有没有登录,然后再微信登录的情况下,我们才能获取到用户信息,从而存储用户,进而让该用户访问我们的发布的微信小程序。
个人理解步骤:
步骤一:首先微信小程序调用wx.login() 获取code
步骤二:把获取到的code传给我们自己的SpringBoot后端,由我们后端向微信接口服务(https://api.weixin.qq.com/sns/jscode2session)发送请求。
/**
appid 为 小程序 appId
secret 为 小程序 appSecret
js_code 为 前端给过来的 获取code
grant_type 是固定值 authorization_code
*/
String url = "https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code";
String replaceUrl = url.replace("{0}", appid).replace("{1}", secret).replace("{2}", code);
String res = HttpUtil.get(replaceUrl);
后端发送请求后获取到的返回信息如下:
{"session_key":"aF3jOYTs5a/iRzctAP98FA==","openid":"oyxuR7dwHBu9-6vHPdSasUeoilJ0"}
- session_key : 会话密钥
描述:微信会根据用户使用小程序的行为对session_key进行续期,用户在一段时间内没有使用小程序,或者小程序被关闭、重启等,session_key可能会失效
作用:可以解密用户信息用,看你需要是否进行存储 - openid:用户唯一标识
作用:常用来配合jwt生成自定义登录态
按照官方文档所讲:之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
所以就可以使用 jwt方式生成Token返回给前端,让前端下次请求时能够带上,就能允许他们访问了。
// 生成自定义登录态token
String token = jwtUtil.generateToken(openid);
同时此处应该把刚刚登录进来的用户数据存到库里面,没有即新增,有即修改
步骤三:微信小程序拿到这个jwt格式的token 以后,就通过setStorageSync进行存储
//持久化存储:数据会存储在本地,即使小程序被关闭或重启,数据仍然可以访问。
wx.setStorageSync('token', result.data.token);
同时 显示授权弹窗,让用户手动触发获取用户信息,然后把用户信息显示在小程序的页面上面(这一步操作可以不要了,因为微信用户信息都没被匿名了,没有任何意义,除非你的基础版本<=2.26.2),同时后端进行用户信息更新操作。
步骤四:后续的调用其他接口时,携带token进行访问。
随即完成整个登录流程操作。
以下是部分代码参考

前端(微信小程序)
- login.js
Page({
data: {
loading: false
},
// 处理微信登录
handleWxLogin() {
if(this.data.loading) return
this.setData({ loading: true })
wx.login({
success: (res) => {
if (res.code) {
console.log('code:', res.code)
// 发送 code 到后台换取 token
wx.request({
url: 'http://127.0.0.1:8080/api/auth/wx-login',
method: 'GET',
data: { code: res.code },
success: (result) => {
console.log('result:', result)
if(result.data.data.token) {
// 保存token
wx.setStorageSync('token', result.data.data.token)
// 保存用户信息
//wx.setStorageSync('userInfo', result.data.data.userInfo)
// 更新全局数据
//getApp().globalData.userInfo = result.data.data.userInfo
// 跳转到首页
wx.reLaunch({
url: '/pages/message/message'
})
} else {
wx.showToast({
title: '登录失败',
icon: 'none'
})
}
},
fail: () => {
wx.showToast({
title: '登录失败',
icon: 'none'
})
},
complete: () => {
this.setData({ loading: false })
}
})
}
},
fail: () => {
wx.showToast({
title: '微信登录失败',
icon: 'none'
})
this.setData({ loading: false })
}
})
}
})
- login.json
{
"navigationBarTitleText": "登录"
}
- login.wxml
<view class="container">
<view class="logo">
<image src="/images/logo.png" mode="aspectFit"></image>
</view>
<view class="login-btn">
<button
type="primary"
loading="{{loading}}"
bindtap="handleWxLogin"
>微信一键登录</button>
</view>
</view>
- login.wxss
.container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 32rpx;
}
.logo {
margin-bottom: 100rpx;
}
.logo image {
width: 200rpx;
height: 200rpx;
}
.login-btn {
width: 100%;
}
.login-btn button {
width: 100%;
}
- message.js
Page({
data: {
token: ''
},
onLoad() {
const token = wx.getStorageSync('token')
this.setData({ token })
},
// 退出登录
handleLogout() {
// 清空本地存储
wx.clearStorageSync()
// 返回登录页
wx.reLaunch({
url: '/pages/login/login'
})
}
})
- message.json
{
"navigationBarTitleText": "消息"
}
- message.wxml
<view class="container">
<view class="success-text">登录成功!</view>
<view class="token-text">Token: {{token}}</view>
<view class="logout-btn">
<button type="warn" bindtap="handleLogout">退出登录</button>
</view>
</view>
- message.wxss
.container {
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 32rpx;
}
.success-text {
font-size: 40rpx;
font-weight: bold;
color: #07c160;
margin-bottom: 30rpx;
}
.token-text {
font-size: 28rpx;
color: #666;
margin-bottom: 100rpx;
word-break: break-all;
text-align: center;
}
.logout-btn {
width: 100%;
}
.logout-btn button {
width: 100%;
}
数据库脚本
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`openid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '微信openid',
`unionid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '微信unionid',
`phone` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
`password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
`nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户昵称',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户头像URL',
`gender` tinyint NULL DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '国家',
`province` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '省份',
`city` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '城市',
`language` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '语言',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`last_login_time` datetime NULL DEFAULT NULL COMMENT '最后登录时间',
`deleted` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除 0-未删除 1-已删除',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_openid`(`openid` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'oyxuR7dwHBu9-6vHPdSasUeoilJ0', NULL, NULL, NULL, '项目876', 'http://localhost:8080/api/static/avatar/af3a658a-c6e6-4019-8037-a16428b413db.jpg', 2, NULL, NULL, NULL, NULL, '2024-11-11 14:51:57', '2024-11-11 18:49:49', '2024-11-11 18:46:09', 0);
SET FOREIGN_KEY_CHECKS = 1;
后端
- Controller
/**
* 认证控制器
*/
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
private final JwtUtil jwtUtil;
private final StringRedisTemplate redisTemplate;
private final ObjectMapper objectMapper;
private static final Logger log = LoggerFactory.getLogger(AuthController.class);
/**
* 微信登录
* @param code 微信登录code
* @return token
*/
@GetMapping("/wx-login")
public Result<LoginVO> wxLogin(@RequestParam String code) {
log.info("微信登录请求参数: {}", code);
LoginVO loginVO = userService.loginByWechat(code);
return Result.success(loginVO);
}
/**
* 退出登录
* @param token 用户token
* @return 退出登录结果
*/
@PostMapping("/logout")
public Result<Void> logout(@RequestHeader("Authorization") String token) {
try {
String actualToken = token.substring(7); // 去掉 "Bearer "
// 获取token中的openid
String openid = jwtUtil.parseToken(actualToken).getSubject();
// 只清除Redis中的session_key和unionid
String sessionKey = String.format("wx:session:%s", openid);
String unionidKey = String.format("wx:unionid:%s", openid);
redisTemplate.delete(sessionKey);
redisTemplate.delete(unionidKey);
return Result.success(null);
} catch (Exception e) {
log.error("退出登录失败", e);
return Result.error("退出登录失败");
}
}
/**
* 获取用户信息
*/
@GetMapping("/user/info")
public Result<UserVO> getUserInfo(@RequestHeader("Authorization") String token) {
try {
String openid = jwtUtil.parseToken(token.substring(7)).getSubject();
UserVO userVO = userService.getUserInfo(openid);
return Result.success(userVO);
} catch (Exception e) {
log.error("获取用户信息失败", e);
return Result.error("获取用户信息失败");
}
}
/**
* 更新用户信息
*/
@PostMapping("/user/update")
public Result<Void> updateUserInfo(
@RequestHeader("Authorization") String token,
@RequestBody UserVO userVO) {
try {
String openid = jwtUtil.parseToken(token.substring(7)).getSubject();
userService.updateUserInfo(openid, userVO);
return Result.success(null);
} catch (Exception e) {
log.error("更新用户信息失败", e);
return Result.error("更新用户信息失败");
}
}
}
- UserService
public interface UserService {
/**
* 微信登录
* @param code 微信登录code
* @return 登录结果
*/
LoginVO loginByWechat(String code);
}
- UserServiceImpl
@Slf4j
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final RestTemplate restTemplate;
private final StringRedisTemplate redisTemplate;
private final JwtUtil jwtUtil;
private final ObjectMapper objectMapper;
private final UserMapper userMapper;
@Value("${wx.appid}")
private String appid;
@Value("${wx.secret}")
private String secret;
private static final String CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
@Override
@Transactional(rollbackFor = Exception.class)
public LoginVO loginByWechat(String code) {
try {
// 1. 构建请求URL
String url = String.format("%s?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
CODE2SESSION_URL, appid, secret, code);
// 2. 发送请求到微信服务器
String response = restTemplate.getForObject(url, String.class);
log.info("微信登录接口返回: {}", response);
// 3. 解析返回结果
JsonNode jsonNode = objectMapper.readTree(response);
// 4. 检查是否有错误
if (jsonNode.has("errcode") && jsonNode.get("errcode").asInt() != 0) {
log.error("微信登录失败: code={}, errcode={}, errmsg={}",
code, jsonNode.get("errcode").asText(), jsonNode.get("errmsg").asText());
throw new RuntimeException("微信登录失败: " + jsonNode.get("errmsg").asText());
}
// 5. 获取openid和session_key
String openid = jsonNode.get("openid").asText();
String sessionKey = jsonNode.get("session_key").asText();
String unionid = jsonNode.has("unionid") ? jsonNode.get("unionid").asText() : null;
// 6. 查询用户是否存在
User user = userMapper.selectOne(
new LambdaQueryWrapper<User>()
.eq(User::getOpenid, openid)
);
Date now = new Date();
if (user == null) {
// 7.1 创建新用户
user = new User();
user.setOpenid(openid);
if (unionid != null) {
// 如果有unionid也保存
user.setUnionid(unionid);
}
user.setCreateTime(now);
user.setUpdateTime(now);
userMapper.insert(user);
log.info("新用户注册成功: openid={}", openid);
} else {
// 7.2 更新用户信息
User updateUser = new User();
updateUser.setId(user.getId());
if (unionid != null && !unionid.equals(user.getUnionid())) {
// 如果unionid有变化,更新unionid
updateUser.setUnionid(unionid);
}
updateUser.setUpdateTime(now);
// 更新最后登录时间等信息
updateUser.setLastLoginTime(now);
userMapper.updateById(updateUser);
log.info("用户登录信息更新成功: openid={}", openid);
}
// 8. 生成自定义登录态token
String token = jwtUtil.generateToken(openid);
// 9. 将session_key存入Redis
String redisKey = String.format("wx:session:%s", openid);
redisTemplate.opsForValue().set(redisKey, sessionKey, 30, TimeUnit.DAYS);
// 10. 如果有unionid,也存入Redis
if (unionid != null) {
String unionidKey = String.format("wx:unionid:%s", openid);
redisTemplate.opsForValue().set(unionidKey, unionid, 30, TimeUnit.DAYS);
}
// 11. 构建返回数据
LoginVO loginVO = new LoginVO();
loginVO.setToken(token);
return loginVO;
} catch (Exception e) {
log.error("微信登录处理失败", e);
throw new RuntimeException("微信登录失败,请稍后重试");
}
}
}
- JwtUtil
package com.wxapp.common.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* JWT工具类
* 用于生成、解析和验证JWT token
* 实现了基于Redis的token版本控制,确保token的唯一性和安全性
*/
@Component
@RequiredArgsConstructor
public class JwtUtil {
private final StringRedisTemplate redisTemplate;
/**
* JWT密钥,从配置文件中读取
*/
@Value("${jwt.secret}")
private String secret;
/**
* JWT过期时间(毫秒),从配置文件中读取
*/
@Value("${jwt.expiration}")
private Long expiration;
/**
* 生成JWT token
* 包含了用户ID、版本号等信息,并使用Redis存储版本信息
*
* @param userId 用户ID
* @return 生成的JWT token字符串
*/
public String generateToken(String userId) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);
// 生成随机版本号,用于token的唯一性控制
String tokenVersion = UUID.randomUUID().toString();
// 将版本号存入Redis,用于后续验证
String versionKey = String.format("token:version:%s", userId);
redisTemplate.opsForValue().set(versionKey, tokenVersion, expiration, TimeUnit.MILLISECONDS);
// 构建JWT token
return Jwts.builder()
.setSubject(userId) // 设置用户ID
.setIssuedAt(now) // 设置签发时间
.setExpiration(expiryDate) // 设置过期时间
.setId(String.valueOf(System.currentTimeMillis())) // 设置JWT ID
.claim("version", tokenVersion) // 添加版本号声明
.signWith(SignatureAlgorithm.HS512, secret) // 使用HS512算法签名
.compact();
}
/**
* 解析JWT token
*
* @param token JWT token字符串
* @return Claims对象,包含token中的所有声明
*/
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 获取token剩余有效时间
*
* @param token JWT token字符串
* @return 剩余有效时间(毫秒)
*/
public long getRemainTime(String token) {
Claims claims = parseToken(token);
return claims.getExpiration().getTime() - System.currentTimeMillis();
}
/**
* 验证token是否有效
* 检查token的签名、过期时间和版本号
*
* @param token JWT token字符串
* @return true表示token有效,false表示token无效
*/
public boolean validateToken(String token) {
try {
Claims claims = parseToken(token);
String userId = claims.getSubject();
String tokenVersion = claims.get("version", String.class);
// 从Redis获取当前有效的token版本号
String versionKey = String.format("token:version:%s", userId);
String currentVersion = redisTemplate.opsForValue().get(versionKey);
// 验证token版本是否匹配
if (currentVersion == null || !currentVersion.equals(tokenVersion)) {
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
}
文章参考
【1】史上最详细微信小程序授权登录与后端SprIngBoot交互操作说明!!!附源代码
【2】微信getUserProfile不弹出授权框及获取到的是默认信息问题
更多推荐



所有评论(0)