开放接口(登录和用户信息)
一、登录
1.1、wx.pluginLogin
该接口仅在小程序插件中可调用,调用接口获得插件用户标志凭证(code)。插件可以此凭证换取用于识别用户的标识 openpid。用户不同、宿主小程序不同或插件不同的情况下,该标识均不相同,即当且仅当同一个用户在同一个宿主小程序中使用同一个插件时,openpid 才会相同。
参数:Object args
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|---|---|---|---|---|
success |
function | 否 | 接口调用成功的回调函数 | |
fail |
function | 否 | 接口调用失败的回调函数 | |
complete |
function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
args.success回调函数:
参数:Object res
| 属性 | 类型 | 说明 |
|---|---|---|
| code | string | 用于换取 openpid 的凭证(有效期五分钟)。插件开发者可以用此 code 在开发者服务器后台调用 getPluginOpenPId 换取 openpid。 |
1.2、wx.login
调用接口获取登录凭证(code)。通过凭证进而换取用户登录态信息,包括用户在当前小程序的唯一标识(openid)、微信开放平台账号下的唯一标识(unionid,若当前小程序已绑定到微信开放平台账号)及本次登录的会话密钥(session_key)等。用户数据的加解密通讯需要依赖会话密钥完成。
参数:Object args
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|---|---|---|---|---|
timeout |
number | 否 | 超时时间,单位ms | |
success |
function | 否 | 接口调用成功的回调函数 | |
fail |
function | 否 | 接口调用失败的回调函数 | |
complete |
function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
object.success回调函数:
参数:Object res
| 属性 | 类型 | 说明 |
|---|---|---|
| code | string | code string 用户登录凭证(有效期五分钟)。开发者需要在开发者服务器后台调用 code2Session,使用 code 换取 openid、unionid、session_key 等信息 |
object.fail回调函数:
参数:Object err
| 属性 | 类型 | 说明 |
|---|---|---|
| errMsg | String | 错误信息 |
| errno | Number | errno 错误码 |
js
wx.login({
success (res) {
if (res.code) {
//发起网络请求
wx.request({
url: 'https://example.com/onLogin',
data: {
code: res.code
}
})
} else {
console.log('登录失败!' + res.errMsg)
}
}
})
二、用户信息
2.1、wx.getUserProfile
获取用户信息。页面产生点击事件(例如 button 上 bindtap 的回调中)后才可调用,每次请求都会弹出授权窗口,用户同意后返回 userInfo。该接口用于替换 wx.getUserInfo,详见 用户接口调用说明。
参数:Object object
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|---|---|---|---|---|
lang |
string | en | 否 | 显示用户信息的语言,合法值如下: en:英文 zh_CN:简体中文 zh_TW:繁体中文 |
desc |
string | 是 | 声明获取用户个人信息后的用途,不超过30个字符 | |
success |
function | 否 | 接口调用成功的回调函数 | |
fail |
function | 否 | 接口调用失败的回调函数 | |
complete |
function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
object.success回调函数:
参数:Object res
| 属性 | 类型 | 说明 |
|---|---|---|
| userInfo | UserInfo | 用户信息对象 |
| rawData | string | 不包括敏感信息的原始数据字符串,用于计算签名 |
| signature | string | 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息,详见 用户数据的签名验证和加解密 |
| encryptedData | string | 包括敏感数据在内的完整用户信息的加密数据,详见 用户数据的签名验证和加解密 |
| iv | string | 加密算法的初始向量,详见 用户数据的签名验证和加解密 |
| cloudID | string | 敏感数据对应的云 ID,开通云开发的小程序才会返回,可通过云调用直接获取开放数据,详细见云调用直接获取开放数据 |
html
<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>
js
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
})
},
})
2.2、wx.getUserInfo
获取用户信息。
参数:Object object
| 属性 | 类型 | 默认值 | 必填 | 说明 |
|---|---|---|---|---|
withCredentials |
boolean | 否 | 是否带上登录态信息。当 withCredentials 为 true 时,要求此前有调用过 wx.login 且登录态尚未过期,此时返回的数据会包含 encryptedData, iv 等敏感信息;当 withCredentials 为 false 时,不要求有登录态,返回的数据不包含 encryptedData, iv 等敏感信息。 | |
lang |
string | en | 否 | 显示用户信息的语言,合法值如下: en:英文 zh_CN:简体中文 zh_TW:繁体中文 |
success |
function | 否 | 接口调用成功的回调函数 | |
fail |
function | 否 | 接口调用失败的回调函数 | |
complete |
function | 否 | 接口调用结束的回调函数(调用成功、失败都会执行) |
object.success回调函数:
参数:Object res
| 属性 | 类型 | 说明 |
|---|---|---|
| userInfo | UserInfo | 用户信息对象 |
| rawData | string | 不包括敏感信息的原始数据字符串,用于计算签名 |
| signature | string | 使用 sha1( rawData + sessionkey ) 得到字符串,用于校验用户信息,详见 用户数据的签名验证和加解密 |
| encryptedData | string | 包括敏感数据在内的完整用户信息的加密数据,详见 用户数据的签名验证和加解密 |
| iv | string | 加密算法的初始向量,详见 用户数据的签名验证和加解密 |
| cloudID | string | 敏感数据对应的云 ID,开通云开发的小程序才会返回,可通过云调用直接获取开放数据,详细见云调用直接获取开放数据 |
js
// 必须是在用户已经授权的情况下调用
wx.getUserInfo({
success: function(res) {
var userInfo = res.userInfo
var nickName = userInfo.nickName
var avatarUrl = userInfo.avatarUrl
var gender = userInfo.gender //性别 0:未知、1:男、2:女
var province = userInfo.province
var city = userInfo.city
var country = userInfo.country
}
})
2.3、UserInfo
用户信息。
- string nickName
用户昵称 - string avatarUrl
用户头像图片的 URL。URL 最后一个数值代表正方形头像大小(有 0、46、64、96、132 数值可选,0 代表 640x640 的正方形头像,46 表示 46x46 的正方形头像,剩余数值以此类推。默认132),用户没有头像时该项为空。若用户更换头像,原有头像 URL 将失效 - number gender
用户性别。不再返回 - string country
用户所在国家。不再返回,参考 相关公告 - string province
用户所在省份。不再返回,参考 相关公告 - string city
用户所在城市。不再返回,参考 相关公告 - string language
显示 country,province,city 所用的语言。强制返回 "zh_CN"
2.4、用户头像昵称获取
上面所有的API在新版本中均已废弃,详见:小程序登录、用户信息相关接口调整说明
目前使用头像昵称填写能力的方式:
- 头像获取:使用<button>组件,设置open-type="chooseAvatar",通过bindchooseavatar事件回调获取临时头像路径。
- 昵称获取:使用<input>组件,设置type="nickname",通过bindinput或bindblur事件获取用户输入的昵称。
html
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar"/>
<input type="nickname" class="nickname-input" placeholder="请输入昵称" bind:change="onInputChange" />
js
//获取头像
onChooseAvatar(e) {
const { avatarUrl } = e.detail
const { nickName } = this.data.userInfo
this.setData({
"userInfo.avatarUrl": avatarUrl
})
},
//获取用户昵称
onInputChange(e) {
const nickname = e.detail.value
const { avatarUrl } = this.data.userInfo
this.setData({
"userInfo.nickname": nickname
})
},
三、登录逻辑
3.1、登录流程

1. 调用wx.login()获取code
这个code的作用是实现微信临时登录的url中的一个非常重要的参数,需要将这个code发送到后端。
json
code: "0e3M5FGa1pxSkL0Ph6Ia1lgjSy4M5FGw"
2. 后端向微信认证接口发送请求
url为https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code
所需4个参数:
- jscode:即发送过来的code
- appid:小程序的应用id,微信公众平台中【开发管理】->【开发设置】获取
- secret:小程序的应用密钥,微信公众平台中【开发管理】->【开发设置】获取
- grant_type:固定authorization_code即可
java
String url = code2SessionUrl + "?appid=" + appid +
"&secret=" + secret +
"&js_code=" + code +
"&grant_type=authorization_code";
String response = HttpUtil.get(url);
log.info("微信接口返回: {}", response);
微信接口返回:
json
{"session_key":"pZFfM9iUc3Of/KL4hZ9PVA==","openid":"oeM0E5V-7ns-cwYacmkRb8DXtZEE"}
3. 保存或更新用户信息
openid代表该用户在当前小程序中的唯一id,我们需要在数据库中查询此id,然后新增或更新用户。
4. 生成自定义登录态
自定义的登录态是与openid和session_key关联的,有很多中实现方法:
- 最简单的,可以将openid和session_key直接存到redis中,前端访问的时候带上二者,后端进行比对即可
- 使用JWT方式生成Token返回给前端,token与openid和session_key关联
- JWT与openid关联生成Token返回给前端,后端openid和session_key关联,即openid作为key将session_key存入redis中,最终token->openid->session_key。
5. 前端保存返回的登录态
前端保存登录态,在后续接口调用中携带。
js
wx.setStorageSync('token', token);
3.2、用户信息解密
getUserProfile和getUserInfo返回的信息是加密的:
json
{
cloudID: undefined
encryptedData: "bVuc1UbrGSJt/pvIUJymRczs8CmtohO9ZJgNTuPAG5rQ6ymsPraFVSmsfoL/w1XuVzcg2wR9XMkvyMmp/7aaZO8Z/LqlP6STz1ynPsejYPE3EV7cRxfhCB5cdy8UI4+cS/BQ1LKec3wdLL/g6Jrn8m+LCxXpRbm7HI6Y9tk9JBPMP90CJ7xMGi2ivJCHoWseetk3ex4yyR6LA27fDADl0dXVSJs5gPNzUX1sKNaCdOPEiADT/rCf/UmdDHIuf7q58iNllsLlhq/iqYj9CMPzuYSjWVb/lPQJ1+uSgB7KGx8fQmuuyicLuNbLujieFmwhwzLk3n4eXd39JWrXo3j9SS98aFrHO/3ZkaiJ7sMDIZE+KltvFOEG3tvGDSWT5wikwOZmt9eirxo2UHsUJ7m/yblfKwQ4aH6Gucu5xdcwDrqWJPx21hhUqbYIRJJbzJb73Fq5WrMBwSMgrSxCzm2VFQ=="
iv: "8JmT2UoAL2oTKPiTNeSsnA=="
rawData: "{"nickName":"微信用户","gender":0,"language":"","city":"","province":"","country":"","avatarUrl":"https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132"}"
signature: "48b63d6d5c2948bf65c0e9a0f60524978e9e064b"
userInfo: {
avatarUrl: "https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132"
city: ""
country: ""
gender: 0
language: ""
nickName: "微信用户"
province: ""
}
}
我们需要保存的是
- encrytedData:包括敏感数据在内的完整用户信息的加密数据(即可以通过反解密,获取出用户数据),详见用户数据的签名验证和加解密
- iv:加密算法的初始向量,详见 详见用户数据的签名验证和加解密
Java解密示例:
java
public String decryptData(String encryptedData, String sessionKey, String iv) {
try {
byte[] dataByte = Base64.decode(encryptedData);
byte[] keyByte = Base64.decode(sessionKey);
byte[] ivByte = Base64.decode(iv);
// 使用AES解密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(keyByte, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
return new String(resultByte, "UTF-8");
}
} catch (Exception e) {
log.error("解密用户信息失败", e);
}
return null;
}
解密后数据,新版后基本都不返回了:
json
{
"openId":"oeM0E5V-7ns-cwYacmkRb8DXtZEE",
"nickName":"微信用户",
"gender":0,
"language":"",
"city":"",
"province":"",
"country":"",
"avatarUrl":"https://thirdwx.qlogo.cn/mmopen/vi_32/POgEwh4mIHO4nibH0KlMECNjjGxQUq24ZEaGT4poC6icRiccVGKSyXwibcPq4BWmiaIGuG1icwxaQX6grC9VemZoJ8rg/132",
"watermark":{
"timestamp":1773409494,
"appid":"wx2318bc561b6061ee"
}
}
四、完整示例


4.1、小程序端
html
<view>
<button type="primary" bind:tap="login">点击登录</button>
<button type="primary" bind:tap="checkSession">校验登录状态</button>
</view>
<viw>
<block wx:if="{{canIUseNicknameComp}}">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
</button>
<view class="nickname-wrapper">
<text class="nickname-label">昵称</text>
<input type="nickname" class="nickname-input" placeholder="请输入昵称" bind:change="onInputChange" />
</view>
</block>
<button type="primary" bind:tap="updateUserInfo">更新用户信息</button>
</viw>
js
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
Page({
data: {
userInfo: {
avatarUrl: defaultAvatarUrl,
nickname: '',
},
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
},
onLoad: function(){
if (wx.getUserProfile) {
this.setData({
canIUseGetUserProfile: true
})
}
},
onShow: function(){
},
// 微信登录
login: function() {
wx.login({
success: (res) => {
if (res.code) {
console.log(res)
wx.getUserInfo({
success: (userInfoRes) => {
console.log(userInfoRes)
this.loginRequest(res.code,
userInfoRes.encryptedData,
userInfoRes.iv
);
},
fail: (err)=>{
console.log(err)
}
});
}
}
});
},
// 调用后端登录接口
loginRequest: function(code, encryptedData, iv) {
wx.request({
url: 'http://127.0.0.1:8080/api/user/login',
method: 'POST',
data: {
code: code,
encryptedData: encryptedData,
iv: iv
},
success: (res) => {
if (res.data.code === 200) {
const token = res.data.data.token;
const userInfo = res.data.data.user;
// 存储token到本地
wx.setStorageSync('token', token);
}
}
});
},
//获取头像
onChooseAvatar(e) {
const { avatarUrl } = e.detail
const { nickName } = this.data.userInfo
this.setData({
"userInfo.avatarUrl": avatarUrl
})
},
//获取用户昵称
onInputChange(e) {
const nickname = e.detail.value
const { avatarUrl } = this.data.userInfo
this.setData({
"userInfo.nickname": nickname
})
},
// 更新用户信息
updateUserInfo: function() {
const token = wx.getStorageSync('token');
//请求后端更新用户信息
wx.request({
url: 'http://127.0.0.1:8080/api/user/info',
method: 'POST',
header: {
'Authorization': 'Bearer ' + token
},
data: {
avatarUrl: this.data.userInfo.avatarUrl,
nickname: this.data.userInfo.nickname
},
success: (res) => {
console.log('用户信息', res.data);
}
});
},
//校验登录状态
checkSession: function(){
wx.checkSession({
success: ()=>{
wx.showToast({
title: '在线',
icon: 'success',
duration: 2000
})
},
fail: ()=>{
this.login()
wx.showToast({
title: '重新登录',
icon: 'success',
duration: 2000
})
}
})
}
})
css
button {
margin-top: 20rpx;
}
.userinfo {
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
width: 80%;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.usermotto {
margin-top: 200px;
}
.avatar-wrapper {
padding: 0;
width: 56px !important;
border-radius: 8px;
margin-top: 40px;
margin-bottom: 40px;
}
.avatar {
display: block;
width: 56px;
height: 56px;
}
.nickname-wrapper {
display: flex;
width: 100%;
padding: 16px;
box-sizing: border-box;
border-top: .5px solid rgba(0, 0, 0, 0.1);
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
color: black;
}
.nickname-label {
width: 105px;
}
.nickname-input {
flex: 1;
}
4.2、后端项目结构和依赖
项目结构:

Maven依赖
xml
<!-- SpringBoot启动父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- MySQL 连接驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- Jackson (JSON序列化) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Jackson数据类型支持 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
4.3、配置类和配置文件
Redis配置类:
java
@EnableCaching
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
// 解决Jackson无法反序列化LocalDateTime的问题
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
// 空值不序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 允许未知属性
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
serializer.setObjectMapper(objectMapper);
// 设置key和value的序列化规则
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
return stringRedisTemplate;
}
}
WebMV配置类:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private AuthInterceptor authInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/user/login", "/api/user/register");
}
}
配置文件application.yml
json
spring:
#数据源
datasource:
url: jdbc:mysql://localhost:3306/wx_demo?useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
redis:
database: 0
port: 6379
host: localhost
password:
server:
port: 8080
#mybatis配置
#configLocation: classpath:/mybatis-config.xml
wx:
appid: "xxxxxxxx"
secret: "xxxxxxxx"
grant-type: authorization_code
code2session-url: https://api.weixin.qq.com/sns/jscode2session
jwt:
secret: "aGVsbG8gd29ybGQgdGhpcyBpcyBhIHNlY3JldCBrZXkgZm9yIGp3dCB0b2tlbiBzaWduaW5nIHByb2Nlc3MgaW4gdGhlIGFwcGxpY2F0aW9uIQ=="
expire: 7200
header: Authorization
token-prefix: Bearer
4.4、表和实体
user表:
sql
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`unionid` varchar(100) DEFAULT NULL COMMENT '微信unionid',
`nickname` varchar(100) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像',
`gender` tinyint(4) DEFAULT '0' COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`phone` varchar(50) DEFAULT NULL COMMENT '手机号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`),
KEY `idx_unionid` (`unionid`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
User实体:
java
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String openid;
private String nickname;
private String avatarUrl;
private Integer gender;
private String country;
private String province;
private String phone;
private String city;
private String unionid;
private Date createTime;
private Date updateTime;
}
登录实体:
java
@Data
public class WxLoginDTO {
private String code;
private String encryptedData; // 加密的用户数据
private String iv; // 加密算法的初始向量
}
认证成功返回的Session实体:
java
@Data
public class WxSession {
private String openid;
private String session_key;
private String unionid;
private Integer errcode;
private String errmsg;
}
通用响应实体:
java
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> success(String message, T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage(message);
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
return result;
}
public static <T> Result<T> error(Integer code, String message) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setTimestamp(System.currentTimeMillis());
return result;
}
}
4.5、控制器
java
@RestController
@RequestMapping("/api/user")
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public Result login(@RequestBody WxLoginDTO loginDTO) {
try {
Map<String, Object> result = userService.wxLogin(loginDTO);
return Result.success("登录成功", result);
} catch (Exception e) {
log.error("登录失败", e);
return Result.error(500, "登录失败:" + e.getMessage());
}
}
@PostMapping("/info")
public Result updateUserInfo(@RequestBody User user, HttpServletRequest request) {
String token = jwtUtil.getTokenFromHeader(request);
userService.updateUserInfo(token, user);
return Result.success("更新成功");
}
}
4.6、拦截器
用于拦截token的拦截器:
java
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 放行登录接口
String requestURI = request.getRequestURI();
if (requestURI.contains("/login") || requestURI.contains("/error")) {
return true;
}
// 获取token
String token = jwtUtil.getTokenFromHeader(request);
if (token == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(JSONUtil.toJsonStr(
Result.error(HttpStatus.UNAUTHORIZED.value(), "未提供token")
));
return false;
}
// 验证token
if (!jwtUtil.validateToken(token)) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(JSONUtil.toJsonStr(
Result.error(HttpStatus.UNAUTHORIZED.value(), "token无效或已过期")
));
return false;
}
// 设置用户信息到请求中
User user = userService.getUserByToken(token);
if (user == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(JSONUtil.toJsonStr(
Result.error(HttpStatus.UNAUTHORIZED.value(), "用户不存在")
));
return false;
}
request.setAttribute("currentUser", user);
request.setAttribute("userId", user.getId());
return true;
}
}
4.7、Service
java
@Service
@Slf4j
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private WxService wxService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private JwtUtil jwtUtil;
/**
* 微信登录
*/
public Map<String, Object> wxLogin(WxLoginDTO loginDTO) {
// 1. 获取微信session
WxSession wxSession = wxService.code2Session(loginDTO.getCode());
// 2. 解密用户信息
String userInfoJson = wxService.decryptData(
loginDTO.getEncryptedData(),
wxSession.getSession_key(),
loginDTO.getIv()
);
// 3. 解析用户信息
JSONObject userInfo = JSONUtil.parseObj(userInfoJson);
log.info("解析后的用户信息:{}", userInfo);
// 4. 保存或更新用户信息
User user = saveOrUpdateUser(wxSession, userInfo);
// 5. 生成token
String token = jwtUtil.generateToken(user.getId().toString(), user.getOpenid());
// 6. 将session_key存入redis(有效期2小时)
String redisKey = "wx:session:" + user.getOpenid();
redisTemplate.opsForValue().set(redisKey, wxSession.getSession_key(), 2, TimeUnit.HOURS);
// 7. 返回结果
Map<String, Object> result = new HashMap<>();
result.put("token", token);
result.put("user", user);
return result;
}
/**
* 保存或更新用户
*/
private User saveOrUpdateUser(WxSession session, JSONObject userInfo) {
String openid = session.getOpenid();
User user = userMapper.selectByOpenid(openid);
if (user == null) {
// 新用户
user = new User();
user.setOpenid(openid);
user.setNickname(userInfo.getStr("nickName"));
user.setAvatarUrl(userInfo.getStr("avatarUrl"));
user.setGender(userInfo.getInt("gender"));
user.setCountry(userInfo.getStr("country"));
user.setProvince(userInfo.getStr("province"));
user.setCity(userInfo.getStr("city"));
user.setCreateTime(new Date());
user.setUpdateTime(new Date());
userMapper.insert(user);
} else {
// 更新用户信息
user.setNickname(userInfo.getStr("nickName"));
user.setAvatarUrl(userInfo.getStr("avatarUrl"));
user.setGender(userInfo.getInt("gender"));
user.setCountry(userInfo.getStr("country"));
user.setProvince(userInfo.getStr("province"));
user.setCity(userInfo.getStr("city"));
user.setUpdateTime(new Date());
userMapper.updateById(user);
}
return user;
}
/**
* 根据token获取用户
*/
public User getUserByToken(String token) {
if (jwtUtil.validateToken(token)) {
String userId = jwtUtil.getUserIdFromToken(token);
return userMapper.selectById(Long.parseLong(userId));
}
return null;
}
/**
* 更新用户信息
*/
public boolean updateUserInfo(String token, User user) {
String openid = jwtUtil.getOpenidFromToken(token);
return userMapper.updateUserInfo(openid, user);
}
}
java
@Service
@Slf4j
public class WxService {
@Value("${wx.appid}")
private String appid;
@Value("${wx.secret}")
private String secret;
@Value("${wx.code2session-url}")
private String code2SessionUrl;
/**
* 通过code获取openid和session_key
*/
public WxSession code2Session(String code) {
String url = code2SessionUrl + "?appid=" + appid +
"&secret=" + secret +
"&js_code=" + code +
"&grant_type=authorization_code";
try {
String response = HttpUtil.get(url);
log.info("微信接口返回: {}", response);
WxSession session = JSONUtil.toBean(response, WxSession.class);
if (session.getErrcode() != null && session.getErrcode() != 0) {
log.error("微信登录失败: {}", session.getErrmsg());
throw new RuntimeException("微信登录失败: " + session.getErrmsg());
}
return session;
} catch (Exception e) {
log.error("调用微信接口异常", e);
throw new RuntimeException("微信服务异常");
}
}
/**
* 解密用户信息
*/
public String decryptData(String encryptedData, String sessionKey, String iv) {
try {
byte[] dataByte = Base64.decode(encryptedData);
byte[] keyByte = Base64.decode(sessionKey);
byte[] ivByte = Base64.decode(iv);
// 使用AES解密
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(keyByte, "AES");
IvParameterSpec ivSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
byte[] resultByte = cipher.doFinal(dataByte);
if (null != resultByte && resultByte.length > 0) {
return new String(resultByte, "UTF-8");
}
} catch (Exception e) {
log.error("解密用户信息失败", e);
}
return null;
}
}
4.8、JWT工具
java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expire}")
private Long expiration;
@Value("${jwt.header}")
private String header;
@Value("${jwt.token-prefix}")
private String tokenPrefix;
/**
* 生成token
*/
public String generateToken(String userId, String openid) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
return Jwts.builder()
.setSubject(userId)
.claim("openid", openid)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 从token中获取用户ID
*/
public String getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getSubject();
}
/**
* 从token中获取openid
*/
public String getOpenidFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.get("openid", String.class);
}
/**
* 验证token
*/
public boolean validateToken(String token) {
try {
getClaimsFromToken(token);
return true;
} catch (Exception e) {
return false;
}
}
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从请求头获取token
*/
public String getTokenFromHeader(HttpServletRequest request) {
String headerValue = request.getHeader(header);
if (headerValue != null && headerValue.startsWith(tokenPrefix + " ")) {
return headerValue.substring(7);
}
return null;
}
}
4.9、Mapper
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
User selectByOpenid(String openid);
boolean updateUserInfo(@Param("openid") String openid, @Param("user") User user);
}
xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace= "pers.zhang.mapper.UserMapper" >
<resultMap id ="UserInfoMap" type="pers.zhang.entity.User">
<id property="id" column="id"/>
<result property="openid" column="openid"/>
<result property="unionid" column="unionid"/>
<result property="nickname" column="nickname"/>
<result property="avatarUrl" column="avatar_Url"/>
<result property="gender" column="gender"/>
<result property="country" column="country"/>
<result property="province" column="province"/>
<result property="city" column="city"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
</resultMap>
<update id="updateUserInfo">
update user
set nickname = #{user.nickname}, avatar_url = #{user.avatarUrl}
where openid = #{openid}
</update>
<select id="selectByOpenid" resultType="pers.zhang.entity.User">
select *
from user
where openid = #{openid}
</select>
</mapper>