前言
微信,作为全球最受欢迎的社交平台之一,其登录认证和用户信息获取机制对于所有希望集成微信功能的开发者来说都是必不可少的一环。在这篇博客中,我将详细解读微信登录流程、微信用户信息获取和在数据库中存储emoji的方法。
一、微信登录流程
小程序可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系。
图解
微信登录流程主要分为以下几个步骤:
- 用户在手机上点击微信登录
- 微信客户端向服务器请求一个临时登录凭证(code)
- 服务器返回code给客户端
- 客户端将code发送给自己的服务器
- 自己的服务器将code发送到微信开放平台
- 微信开放平台返回用户唯一标识(openid)和会话密钥(session_key)给服务器
- 服务器生成自身的会话密钥,并返回客户端
- 客户端保存会话密钥用于后续通信。
- 开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
注意:临时登录凭证code只能使用一次
微信用户信息可以通过微信小程序、公众号或者开放平台来获取。获取用户信息通常需要用户授权。在用户授权后,可以获取到的信息包括用户的昵称、头像、性别、所在地等。
二、微信用户信息获取(授权登录)
微信用户信息可以通过微信小程序、公众号或者开放平台来获取。获取用户信息通常需要用户授权。在用户授权后,可以获取到的信息包括用户的昵称、头像、性别、所在地等。
下面我以两种方式的代码来给大家论证一下微信用户授权登录的流程,第一种不需要用户确认即可完成用户授权登录在开发中并不是那么的安全,第二种则是需要用户确认方可进行授权登录。
前期准备
wxml代码展示
<!--pages/index/index.wxml--> <view> <button wx:if="{{canIUseGetUserProfile}}" type="primary" class="wx-login-btn" bindtap="getUserProfile">微信直接登录1</button> <button wx:else open-type="getUserInfo" type="primary" class="wx-login-btn" bindgetuserinfo="wxLogin">微信直接登录2</button> <image mode="scaleToFill" src="{{userInfo.avatarUrl}}" /> <text>昵称:{{userInfo.nickName}}</text> </view>
1.wx.login
JS代码展示
wxLogin: function(e) { debugger console.log('wxLogin') console.log(e.detail.userInfo); this.setData({ userInfo: e.detail.userInfo }) if (e.detail.userInfo == undefined) { app.globalData.hasLogin = false; util.showErrorToast('微信登录失败'); return; }
效果演示
2.wx.getUserProfile
JS代码展示
getUserProfile(e) { console.log('getUserProfile') // 推荐使用 wx.getUserProfile 获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认 // 开发者妥善保管用户快速填写的头像昵称,避免重复弹窗 wx.getUserProfile({ desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写 success: (res) => { console.log(res); this.setData({ userInfo: res.userInfo, hasUserInfo: true }) } }) }
效果演示
3.wx.login和wx.getUserProfile的区别
wx.login:
wx.login
是用来获取用户登录的临时凭证(code),开发者需要把这个 code 发送到开发者服务器,然后在服务器端调用微信提供的 API 来换取用户的openid
和session_key
。openid
是用户在当前小程序中的唯一标识,而session_key
是微信服务器用来验证用户会话的一个密钥。需要注意的是用wx.login
获取的并不是用户的个人信息,而是用于识别用户身份的一些密钥和标识。wx.getUserProfile:
wx.getUserProfile
用来获取用户的个人信息,包括昵称、头像、性别等。这个 API 需要用户明确授权才能获取到这些信息。需要注意的是,从 2021 年 4 月 13 日起,微信废弃了wx.getUserInfo
接口,新的获取用户信息的方式是通过wx.getUserProfile
接口。wx.getUserProfile
提供了一个更加安全和符合隐私规范的方式来获取用户信息。
三、获取用户信息实现后端交互
1.前端代码实现
1.1.封装工具代码
utils/util.js
function formatTime(date) { var year = date.getFullYear() var month = date.getMonth() + 1 var day = date.getDate() var hour = date.getHours() var minute = date.getMinutes() var second = date.getSeconds() return [year, month, day].map(formatNumber).join('-') + ' ' + [hour, minute, second].map(formatNumber).join(':') } function formatNumber(n) { n = n.toString() return n[1] ? n : '0' + n } /** * 封封微信的的request */ function request(url, data = {}, method = "GET") { return new Promise(function (resolve, reject) { wx.request({ url: url, data: data, method: method, timeout:3000, header: { 'Content-Type': 'application/json', 'X-OA-Token': wx.getStorageSync('token') }, success: function (res) { if (res.statusCode == 200) { if (res.data.errno == 501) { // 清除登录相关内容 try { wx.removeStorageSync('userInfo'); wx.removeStorageSync('token'); } catch (e) { // Do something when catch error } // 切换到登录页面 wx.navigateTo({ url: '/pages/auth/login/login' }); } else { resolve(res.data); } } else { reject(res.errMsg); } }, fail: function (err) { reject(err) } }) }); } function redirect(url) { //判断页面是否需要登录 if (false) { wx.redirectTo({ url: '/pages/auth/login/login' }); return false; } else { wx.redirectTo({ url: url }); } } function showErrorToast(msg) { wx.showToast({ title: msg, image: '/static/images/icon_error.png' }) } function jhxLoadShow(message) { if (wx.showLoading) { // 基础库 1.1.0 微信6.5.6版本开始支持,低版本需做兼容处理 wx.showLoading({ title: message, mask: true }); } else { // 低版本采用Toast兼容处理并将时间设为20秒以免自动消失 wx.showToast({ title: message, icon: 'loading', mask: true, duration: 20000 }); } } function jhxLoadHide() { if (wx.hideLoading) { // 基础库 1.1.0 微信6.5.6版本开始支持,低版本需做兼容处理 wx.hideLoading(); } else { wx.hideToast(); } } module.exports = { formatTime, request, redirect, showErrorToast, jhxLoadShow, jhxLoadHide }
如果使用util.request函数,每次请求都会携带'X-OA-Token': wx.getStorageSync('token');而服器已经保存了所有的token,所以服器通过token区分每个客户端
utils/user.js
/** * 用户相关服务 */ const util = require('../utils/util.js'); const api = require('../config/api.js'); /** * Promise封装wx.checkSession */ function checkSession() { return new Promise(function(resolve, reject) { wx.checkSession({ success: function() { resolve(true); }, fail: function() { reject(false); } }) }); } /** * Promise封装wx.login */ function login() { return new Promise(function(resolve, reject) { wx.login({ success: function(res) { if (res.code) { resolve(res); } else { reject(res); } }, fail: function(err) { reject(err); } }); }); } /** * 调用微信登录 */ function loginByWeixin(userInfo) { return new Promise(function(resolve, reject) { return login().then((res) => { //登录远程服务器 util.request(api.AuthLoginByWeixin, { code: res.code, userInfo: userInfo }, 'POST').then(res => { if (res.errno === 0) { //存储用户信息 wx.setStorageSync('userInfo', res.data.userInfo); wx.setStorageSync('token', res.data.token); resolve(res); } else { reject(res); } }).catch((err) => { reject(err); }); }).catch((err) => { reject(err); }) }); } /** * 判断用户是否登录 */ function checkLogin() { return new Promise(function(resolve, reject) { if (wx.getStorageSync('userInfo') && wx.getStorageSync('token')) { checkSession().then(() => { resolve(true); }).catch(() => { reject(false); }); } else { reject(false); } }); } module.exports = { loginByWeixin, checkLogin, };
将userInfo,token数据保存到本地
config/api.js
// 以下是业务服务器API地址 // 本机开发API地址 var WxApiRoot = 'http://localhost:8080/oapro/wx/'; // 测试环境部署api地址 // var WxApiRoot = 'http://192.168.191.1:8080/oapro/wx/'; // 线上平台api地址 //var WxApiRoot = 'https://www.oa-mini.com/demo/wx/'; module.exports = { IndexUrl: WxApiRoot + 'home/index', //首页数据接口 SwiperImgs: WxApiRoot+'swiperImgs', MettingInfos: WxApiRoot+'meeting/list', AuthLoginByWeixin: WxApiRoot + 'auth/login_by_weixin', //微信登录 UserIndex: WxApiRoot + 'user/index', //个人页面用户相关信息 AuthLogout: WxApiRoot + 'auth/logout', //账号登出 AuthBindPhone: WxApiRoot + 'auth/bindPhone' //绑定微信手机号 };
1.2.前端页面布局于实现
wxml
<!--pages/auth/login/login.wxml--> <view class="container"> <view class="login-box"> <button wx:if="{{canIUseGetUserProfile}}" type="primary" class="wx-login-btn" bindtap="getUserProfile">微信直接登录</button> <button wx:else open-type="getUserInfo" type="primary" class="wx-login-btn" bindgetuserinfo="wxLogin">微信直接登录</button> <button type="primary" class="account-login-btn" bindtap="accountLogin">账号登录</button> </view> </view>
wxss
page{ background-color: #f5f5f5; } .container { box-sizing: border-box; background-color: #f4f4f4; font-family: PingFangSC-Light, helvetica, 'Heiti SC'; } .login-box { width: 100%; height: auto; overflow: hidden; padding: 0 40rpx; margin-top: 200rpx; background: #f4f4f4; } .wx-login-btn { margin: 60rpx 0 40rpx 0; height: 96rpx; line-height: 96rpx; font-size: 30rpx; border-radius: 6rpx; width: 90%; color: #fff; right: 0; display: flex; justify-content: center; align-items: center; position: flex; bottom: 0; left: 0; padding: 0; margin-left: 5%; text-align: center; /* padding-left: -5rpx; */ border-top-left-radius: 50rpx; border-bottom-left-radius: 50rpx; border-top-right-radius: 50rpx; border-bottom-right-radius: 50rpx; letter-spacing: 3rpx; } .account-login-btn { width: 90%; margin: 0 auto; color: #fff; font-size: 30rpx; height: 96rpx; line-height: 96rpx; right: 0; display: flex; justify-content: center; align-items: center; position: flex; bottom: 0; left: 0; border-radius: 0; padding: 0; margin-left: 5%; text-align: center; /* padding-left: -5rpx; */ border-top-left-radius: 50rpx; border-bottom-left-radius: 50rpx; border-top-right-radius: 50rpx; border-bottom-right-radius: 50rpx; letter-spacing: 3rpx; background-image: linear-gradient(to right, #9a9ba1 0%, #9a9ba1 100%); }
js
// pages/auth/login/login.js var util = require('../../../utils/util.js'); var user = require('../../../utils/user.js'); const app = getApp(); Page({ /** * 页面的初始数据 */ data: { canIUseGetUserProfile: false, // 用于向前兼容 lock:false }, onLoad: function(options) { // 页面初始化 options为页面跳转所带来的参数 // 页面渲染完成 if (wx.getUserProfile) { this.setData({ canIUseGetUserProfile: true }) } //console.log('login.onLoad.canIUseGetUserProfile='+this.data.canIUseGetUserProfile) }, /** * 生命周期函数--监听页面初次渲染完成 */ onReady() { }, /** * 生命周期函数--监听页面显示 */ onShow() { }, getUserProfile(e) { // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认 // 开发者妥善保管用户快速填写的头像昵称,避免重复弹窗 wx.getUserProfile({ desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写 success: (res) => { //console.log(res); debugger user.checkLogin().catch(() => { user.loginByWeixin(res.userInfo).then(res => { app.globalData.hasLogin = true; debugger wx.navigateBack({ delta: 1 }) }).catch((err) => { app.globalData.hasLogin = false; if(err.errMsg=="request:fail timeout"){ util.showErrorToast('微信登录超时'); }else{ util.showErrorToast('微信登录失败'); } this.setData({ lock:false }) }); }); }, fail: (res) => { app.globalData.hasLogin = false; console.log(res); util.showErrorToast('微信登录失败'); } }); }, wxLogin: function(e) { if (e.detail.userInfo == undefined) { app.globalData.hasLogin = false; util.showErrorToast('微信登录失败'); return; } user.checkLogin().catch(() => { user.loginByWeixin(e.detail.userInfo).then(res => { app.globalData.hasLogin = true; wx.navigateBack({ delta: 1 }) }).catch((err) => { app.globalData.hasLogin = false; if(err.errMsg=="request:fail timeout"){ util.showErrorToast('微信登录超时'); }else{ util.showErrorToast('微信登录失败'); } }); }); }, accountLogin() { console.log('开发中....') } })
2.后端代码的实现
2.1.准备数据表
DROP TABLE IF EXISTS `wx_user`; CREATE TABLE `wx_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名称', `password` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户密码', `gender` tinyint(3) NOT NULL DEFAULT 0 COMMENT '性别:0 未知, 1男, 1 女', `birthday` date NULL DEFAULT NULL COMMENT '生日', `last_login_time` datetime(0) NULL DEFAULT NULL COMMENT '最近一次登录时间', `last_login_ip` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '最近一次登录IP地址', `user_level` tinyint(3) NULL DEFAULT 0 COMMENT '用户层级 0 普通用户,1 VIP用户,2 区域代理用户', `nickname` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户昵称或网络名称', `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户手机号码', `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户头像图片', `weixin_openid` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '微信登录openid', `status` tinyint(3) NOT NULL DEFAULT 0 COMMENT '0 可用, 1 禁用, 2 注销', `add_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', `deleted` tinyint(1) NULL DEFAULT 0 COMMENT '逻辑删除', `share_user_id` int(11) NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `user_name`(`username`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Compact;
2.2.反向生成工具生成
- WxUser.java
- WxUserMapper.java
- WxUserMapper.xml
2.3.准备封装前端传过来的数据
- UserInfo
- WxLoginInfo
2.4.小程序服器配置
导入微信小程序SDK
<dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-miniapp</artifactId> <version>3.3.0</version> </dependency>
application.yml
oa: wx: app-id: app-secret: msgDataFormat: JSON
注意:
app-id,app-secret需自行配置
app-secret密钥第一次需生成
WxProperties
封装oa.wx的数据
@Data @Configuration @ConfigurationProperties(prefix = "oa.wx") public class WxProperties { /** * 设置微信小程序的appId */ private String appId; /** * 设置微信小程序的Secret */ private String appSecret; /** * 消息数据格式 */ private String msgDataFormat; }
WxConfig
注册WxMaService
@Configuration public class WxConfig { @Autowired private WxProperties properties; @Bean public WxMaConfig wxMaConfig() { WxMaInMemoryConfig config = new WxMaInMemoryConfig(); config.setAppid(properties.getAppId()); config.setSecret(properties.getAppSecret()); config.setMsgDataFormat(properties.getMsgDataFormat()); return config; } @Bean public WxMaService wxMaService(WxMaConfig maConfig) { WxMaService service = new WxMaServiceImpl(); service.setWxMaConfig(maConfig); return service; } }
2.5.WxAuthController
@RequestMapping("/wx/auth") public class WxAuthController { @Autowired private WxMaService wxService; @PostMapping("login_by_weixin") public Object loginByWeixin(@RequestBody WxLoginInfo wxLoginInfo, HttpServletRequest request) { //客户端需携带code与userInfo信息 String code = wxLoginInfo.getCode(); UserInfo userInfo = wxLoginInfo.getUserInfo(); if (code == null || userInfo == null) { return ResponseUtil.badArgument(); } //调用微信sdk获取openId及sessionKey String sessionKey = null; String openId = null; try { WxMaJscode2SessionResult result = this.wxService.getUserService().getSessionInfo(code); sessionKey = result.getSessionKey();//session id openId = result.getOpenid();//用户唯一标识 OpenID } catch (Exception e) { e.printStackTrace(); } if (sessionKey == null || openId == null) { log.error("微信登录,调用官方接口失败:{}", code); return ResponseUtil.fail(); }else{ log.info("openId={},sessionKey={}",openId,sessionKey); } //根据openId查询wx_user表 //如果不存在,初始化wx_user,并保存到数据库中 //如果存在,更新最后登录时间 //.... // token UserToken userToken = null; try { userToken = UserTokenManager.generateToken(user.getId()); } catch (Exception e) { log.error("微信登录失败,生成token失败:{}", user.getId()); e.printStackTrace(); return ResponseUtil.fail(); } userToken.setSessionKey(sessionKey); log.info("SessionKey={}",UserTokenManager.getSessionKey(user.getId())); Map<Object, Object> result = new HashMap<Object, Object>(); result.put("token", userToken.getToken()); result.put("tokenExpire", userToken.getExpireTime().toString()); result.put("userInfo", userInfo); //.... log.info("【请求结束】微信登录,响应结果:{}", JSONObject.toJSONString(result)); return ResponseUtil.ok(result); }
响应给客户端数据有:
token userInfo
效果演示
四、Emoji(表情包)的存储
mysql的utf8编码的一个字符最多3个字节,但是一个emoji表情为4个字节,所以utf8不支持存储emoji表情。但是utf8的超集utf8mb4一个字符最多能有4字节,所以能支持emoji表情的存储。
- UTF8MB4
UTF-8编码可以用1到4个字节来表示一个字符,但MySQL中默认的UTF-8编码只使用1到3个字节,这也就意味着它无法表示所有的Unicode字符,尤其是Emoji。
正确的做法是使用UTF8MB4编码,它支持最多4个字节的Unicode字符,足以覆盖所有的Emoji。
Linux系统中MySQL的配置文件为my.cnf。
Winows中的配置文件为my.ini。
我们创建表时:
CREATE TABLE `emoji` ( `id` int(11) NOT NULL AUTO_INCREMENT, `emoji_char` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
或者我们创建数据库时 :