小程序——开放接口(登录和用户信息)详解

开放接口(登录和用户信息)

一、登录

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关联的,有很多中实现方法:

  1. 最简单的,可以将openid和session_key直接存到redis中,前端访问的时候带上二者,后端进行比对即可
  2. 使用JWT方式生成Token返回给前端,token与openid和session_key关联
  3. 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: ""
	}
}

我们需要保存的是

  1. encrytedData:包括敏感数据在内的完整用户信息的加密数据(即可以通过反解密,获取出用户数据),详见用户数据的签名验证和加解密
  2. 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>
相关推荐
2501_915921432 小时前
从构建到 IPA 保护,Flutter iOS 包如何做混淆与安全处理
android·安全·flutter·ios·小程序·uni-app·iphone
努力成为包租婆2 小时前
【微信小程序-内嵌H5,微信开发工具上-页面没有更新】
微信小程序·小程序
2501_916008892 小时前
iPhone 手机硬件组件使用耗能历史记录查看,能耗查看
android·ios·智能手机·小程序·uni-app·iphone·webview
人生导师yxc11 小时前
微信小程序接入支付宝沙箱支付(http请求)
微信小程序·小程序
云起SAAS11 小时前
日历黄历八字排盘紫微斗数奇门遁甲姓名分析号码吉凶命理抖音快手微信小程序看广告流量主开源
微信小程序·小程序·ai编程·看广告变现轻·日历黄历八字排盘紫微斗数
倔强的石头10615 小时前
工业平台选型指南:权限、审计与多租户治理——用 Apache IoTDB 把“数据可用”升级为“数据可控”
人工智能·apache·iotdb
小小王app小程序开发15 小时前
一番赏潮玩抽赏小程序开发全解析(2026技术版)
小程序
CRMEB系统商城17 小时前
CRMEB标准版系统(PHP)v6.0公测版发布,商城主题市场上线~
java·开发语言·小程序·php
Dragon Wu18 小时前
Taro 小程序开发注意事项(不定期记录更新)
前端·javascript·小程序·typescript·taro