简要

微信小程序登录:并不是我们自己去实现微信登录。
而是我们通过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不再返回真实用户信息了并且也不再弹出授权框(详见微信官方公告)
在这里插入图片描述

总结得知:

  1. wx.getUserProfile 和 wx.getUserInfo 都不提供真实的用户信息了。
    包括从它们两者提供 encryptedData 中解密出来的用户数据,也是不含的真实的用户信息。
  2. 用户微信小程序登录成功后,让用户自己改一下微信小程序里面的个人的昵称和头像吧

二、小程序登录接入方式一: 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不弹出授权框及获取到的是默认信息问题

更多推荐