前后端-微信小程序登录功能

前言

微信小程序登录-时序图jpg.jpg

UniApp前端

  • 页面初始化完毕时,调用uni.login(),获取到微信小程序的登录凭证,然后保存到一个变量(坑,如果不提前获取,例如点击登录时获取,有时候会失败,所以就只能提前获取了)
  • 页面放置一个button按钮,并需要设置open-type属性为getPhoneNumber,则表示该按钮是用于获取用户手机号的,然后监听getphonenumber事件,当用户允许获取手机号后,获取到手机号信息时回调
  • 手机号信息中,会包含以下参数
    • code,获取手机号的凭证,和uni.login返回的code不一样,登录的code只能用于登录,而这里的code只能用于换取用户手机号。后端定义登录的code为code,而获取手机号的codephoneCode
    • encryptedData,加密信息
    • iv,加密向量
  • 调用我们自己后端的login登录方法,把登录的code和获取手机号的code,一起传过去,如果encryptedDataiv后端有需要的话,也一起传

前端页面

<template>
  <view class="viewport">
    <view class="logo">
      <image src=".static/images/logo_icon.png"></image>
    </view>
    <view class="login">
      <!-- #ifdef MP-WEIXIN -->
      <button style="margin-bottom: 20rpx;" class="button phone" open-type="getPhoneNumber"
        @getphonenumber="onGetPhone">
        <text class="icon icon-phone"></text>
        微信手机号快捷登录
      </button>
      <!-- #endif -->
      <view class="extra">
        <view class="caption">
          <text>其它登录方式</text>
        </view>
        <view class="options">
          <button>
            <text class="icon icon-weixin">微信</text>
          </button>
          <button>
            <text class="icon icon-phone">手机</text>
          </button>
          <button>
            <text class="icon icon-mail">邮箱</text>
          </button>
        </view>
      </view>
      <view class="tips">登录/注册即视为你同意《服务条款》和《隐私协议》</view>
    </view>
  </view>
</template>

<script setup>
import { loginByWXAPI } from '@/api/profile'

import { useUserStore } from '@/store'
import { onLoad } from '@dcloudio/uni-app'

// 创建用户信息Store
const userStore = useUserStore()
const { saveProfile } = userStore

// 微信临时凭证code(动态令牌,code来换取用户手机号。每个code有效期为5分钟,且只能使用一次)
let loginCode = ""

// fix:由于获取code的时机和getPhoneNumber,同时以 async await 同步形式执行,会有一定概率会登录失败,所以需要将获取code的时机提前,来解决这个问题
onLoad(async () => {
  // 小程序端,才执行小程序登录
  // #ifndef H5

  // 发起微信登录
  const result = await uni.login()
  console.log(result);
  loginCode = result.code
  console.log(`获取登录的code成功:${loginCode}`);

  // #endif
})

// 微信手机号快捷登录
const onGetPhone = async (e) => {
  console.log(e);

  // 获取返回的用户加密数据和解密需要使用的iv
  const { code: phoneCode, encryptedData, iv, errMsg } = e.detail;

  if (!encryptedData || !iv) {
    uni.showToast({
      title: `登录失败:${errMsg}`,
      icon: 'none'
    })
    return
  }

  // 调用自己后端的登录接口,将encryptedData、iv、code,传给后端
  const result = await loginByWXAPI({
    encryptedData: encryptedData,
    iv: iv,
    // 登录的临时凭证
    code: loginCode,
    // 获取手机号的临时凭证
    phoneCode: phoneCode
  })
  console.log(result);

  // 登录成功,保存用户信息到pinia中
  saveProfile(result.result)
}
</script>

<style lang="scss">
</style>

请求API

  • 封装业务接口,并提供js请求方法
import http from "@/utils/http";

/**
 * 小程序登录
 * 
 * @param {string} encryptedData 加密的手机号信息 getphonenumber事件回调中获取
 * @param {string} iv 加密相关 getphonenumber事件回调中获取
 * @param {string} code 通过 wx.login() 获取
 */
export const loginByWXAPI = (data) => {
    return http({
        url: "/customer/user/login",
        method: "POST",
        data: {
            // 用户加密数据
            encryptedData: data.encryptedData,
            // 解密使用的向量
            iv: data.iv,
            // 登录临时凭证
            code: data.code,
            // 获取手机号的临时凭证
            phoneCode: data.phoneCode
        }
    });
};

请求工具类

  • 封装UniApp的请求拦截器和响应拦截器
  • 在请求拦截器中,统一将token放到请求头中
  • 在响应拦截器中,进行数据剥离,以及处理401的HTTP状态码,处理登录态失效,跳转到登录页,清除本地保存的token信息
  • 在请求前和请求后,展示Loading,以及错误信息的Toast
import { useUserStore } from '@/store'

// 基地址
const baseURL = "https://xxx.api.com";

// 发请求前触发-等价于请求拦截器
const interceptor = {
  invoke(args) {
    // 获取用户信息Store
    const userStore = useUserStore()

    // 统一显示Loading
    uni.showLoading({ title: "拼命加载中..." });

    // 通用参数
    const commonParamsHeader = {}

    // 不是https开头,则将URL拼接上基地址(也就是,如果写全了地址,则不拼接基地址了)
    if (!args.url.startsWith("https")) {
      args.url = baseURL + args.url;
    }

    // 设置token
    const { token } = userStore
    if (token) {
      commonParamsHeader.Authorization = token
    }

    // 设置请求头
    args.header = {
      // 保留原本的 header
      ...args.header,
      // 添加小程序端调用标识
      "source-client": "miniapp",
      // 添加通用参数
      ...commonParamsHeader
    };
  },
  complete(res) {
    // 请求完成,隐藏Loading 
    uni.hideLoading();
  },
};

// 请求拦截器
uni.addInterceptor("request", interceptor);
// 文件上传拦截器
uni.addInterceptor("uploadFile", interceptor);

// 发请求后-等价于响应拦截器
const http = async (options) => {
  // 请求返回结果,返回一个数组,第一个参数:错误信息,第二个参数:接口返回的结果
  const res = await uni.request(options);

  // HTTP响应状态码
  const { statusCode } = res

  // token过期,跳转到登录页面
  if (statusCode === 401) {
    uni.navigateTo({ url: "/pages/login/login" });
    return
  }

  // 请求成功
  if (statusCode >= 200 && statusCode < 300) {
    return res.data;
  } else {
    // 请求失败,提示错误信息
    uni.showToast({
      title: res.data.message,
      icon: 'none'
    })
    return Promise.reject(new Error(res))
  }
};

// 导出
export default http;

Java后端

客户表实体类

/**
 * 用户表
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member extends BaseEntity {
    /**
     * 手机号
     */
    private String phone;

    /**
     * 名称
     */
    private String name;

    /**
     * 头像
     */
    private String avatar;

    /**
     * 微信OpenID
     */
    private String openId;

    /**
     * 性别
     */
    private Integer gender;
}

客户登录Dto

/**
 * C端用户登录Dto
 */
@Data
public class UserLoginRequestDto {
    @ApiModelProperty("微信昵称")
    private String nickName;

    @ApiModelProperty("登录临时凭证")
    private String code;

    @ApiModelProperty("手机号临时凭证")
    private String phoneCode;
}

客户登录Vo

/**
 * C端用户登录Vo
 */
@Data
@Builder
@ApiModel(value = "登录对象")
public class LoginVo {
    @ApiModelProperty(value = "JWT token")
    private String token;

    @ApiModelProperty(value = "昵称")
    private String nickName;
}

CustomerUserController,登录接口

@Slf4j
@Api(tags = "客户管理")
@RestController
@RequestMapping("/customer/user")
public class CustomerUserController {
    @Autowired
    private MemberService memberService;

    /**
     * C端用户登录--微信登录
     *
     * @param userLoginRequestDto 用户登录信息
     * @return 登录结果
     */
    @PostMapping("/login")
    @ApiOperation("登录")
    public ResponseResult<LoginVo> login(@RequestBody UserLoginRequestDto userLoginRequestDto) {
        LoginVo loginVo = memberService.login(userLoginRequestDto);
        return ResponseResult.success(loginVo);
    }
}

MemberService,客户业务层接口

/**
 * C端用户业务层接口
 */
public interface MemberService {
    /**
     * 微信小程序端登录
     */
    LoginVo login(UserLoginRequestDto userLoginRequestDto);
}

MemberServiceImpl,客户业务层实现类

  • 通过前端传过来的登录临时凭证code,换取用户的openId,也就是小程序登录
  • 以及通过获取手机号的临时凭证phoneCode,换取用户的手机号
  • 通过openId查询用户是否存在
    • 不存在,则为第一次登录,保存用户的手机号和openId到数据库表中
    • 存在,则判断用户手机号是否有变更,有则更新到数据库表中
    • 最后,通过JWT生成用户的token,并和用户信息一起返回给前端
  • 与微信的API交互,是通过HTTP协议,都封装到了WechatService
/**
 * C端用户业务层实现类
 */
@Service
public class MemberServiceImpl implements MemberService {
    @Autowired
    private WechatService wechatService;

    @Autowired
    private MemberMapper memberMapper;

    /**
     * JWT配置信息
     */
    @Autowired
    private JwtTokenManagerProperties jwtTokenManagerProperties;

    /**
     * 用户昵称,随机前缀
     */
    private static final List<String> DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
            "生活更美好",
            "大桔大利",
            "日富一日",
            "好柿开花",
            "柿柿如意",
            "一椰暴富",
            "大柚所为",
            "杨梅吐气",
            "天生荔枝"
    );

    /**
     * 小程序端登录
     */
    @Override
    public LoginVo login(UserLoginRequestDto userLoginRequestDto) {
        // 调用微信API,根据前端传过来的code(临时凭证),获取openId
        String openId = wechatService.getOpenid(userLoginRequestDto.getCode());
        // 调用微信API,获取用户绑定的手机号
        String phone = wechatService.getPhoneNumber(userLoginRequestDto.getPhoneCode());

        // 根据openId,查询用户信息
        Member member = memberMapper.getByOpenId(openId);

        // 如果用户为空信息,则为新用户,则创建新用户信息,并设置openId
        if (ObjectUtil.isEmpty(member)) {
            member = Member.builder()
                    .openId(openId)
                    .build();
        }

        // 保存或修改用户
        saveOrUpdate(member, phone);

        // 创建token
        String token = createMemberToken(member);

        // 返回用户信息
        return LoginVo.builder()
                .token(token)
                .nickName(member.getName())
                .build();
    }

    /**
     * 保存或修改客户
     *
     * @param member      数据库中的用户信息
     * @param phoneNumber 手机号
     */
    private void saveOrUpdate(Member member, String phoneNumber) {
        // 如果从微信取到的手机号,和数据库中保存的手机号不一样,那么更新手机号
        if (ObjectUtil.notEqual(phoneNumber, member.getPhone())) {
            member.setPhone(phoneNumber);
        }

        // id存在,则更新用户信息
        if (member.getId() != null) {
            memberMapper.updateMember(member);
        } else {
            // 随机组装昵称,词组+手机号后四位
            int randomIndex = (int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size());
            String nickName = DEFAULT_NICKNAME_PREFIX.get(randomIndex)
                    + StringUtils.substring(member.getPhone(), 7);
            member.setName(nickName);

            // id不存在,则为新用户,创建用户
            memberMapper.addMember(member);
        }
    }

    /**
     * 常见用户token
     */
    private String createMemberToken(Member member) {
        // token保存的信息
        Map<String, Object> claims = new HashMap<>();

        // 用户Id
        claims.put(Constants.JWT_USERID, member.getId());
        // 用户名称
        claims.put(Constants.JWT_USERNAME, member.getName());

        String secretKey = jwtTokenManagerProperties.getBase64EncodedSecretKey();
        // 过期时间
        int dateOffset = jwtTokenManagerProperties.getTtl();

        // 创建token
        return JwtUtil.createJWT(
                secretKey,
                dateOffset,
                claims
        );
    }
}

MemberMapper,客户Mapper接口

  • 定义客户表的增、删、查、改方法
/**
 * C端用户Mapper
 */
@Mapper
public interface MemberMapper {
    /**
     * 根据微信OpenId,查询用户信息
     */
    Member getByOpenId(String openId);

    /**
     * 添加用户信息
     */
    void addMember(Member member);

    /**
     * 更新用户信息
     */
    void updateMember(Member member);
}

MemberMapper.xml,客户Mapper的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="com.zzyl.mapper.MemberMapper">
    <resultMap id="BaseResultMap" type="com.zzyl.entity.Member">
        <id column="id" property="id"/>
        <result column="phone" property="phone"/>
        <result column="name" property="name"/>
        <result column="avatar" property="avatar"/>
        <result column="open_id" property="openId"/>
        <result column="gender" property="gender"/>
        <result column="create_by" property="createBy"/>
        <result column="update_by" property="updateBy"/>
        <result column="remark" property="remark"/>
        <result column="create_time" property="createTime"/>
        <result column="update_time" property="updateTime"/>
    </resultMap>

    <select id="getByOpenId" resultType="com.zzyl.entity.Member">
        SELECT *
        FROM member
        WHERE open_id = #{openId}
    </select>

    <insert id="addMember" parameterType="com.zzyl.entity.Member" keyProperty="id" useGeneratedKeys="true">
        INSERT INTO member (phone, name, avatar, open_id, gender, create_by, create_time)
        VALUES (#{phone}, #{name}, #{avatar}, #{openId}, #{gender}, #{createBy}, #{createTime})
    </insert>

    <update id="updateMember" parameterType="com.zzyl.entity.Member">
        UPDATE member
        SET phone       = #{phone},
            name        = #{name},
            avatar      = #{avatar},
            open_id     = #{openId},
            gender      = #{gender},
            update_by   = #{updateBy},
            update_time = #{updateTime}
        WHERE id = #{id}
    </update>
</mapper>

微信相关业务

自定义yml属性

# 自定义属性配置
zzyl:
  # 微信小程序配置
  wechat:
    # 微信小程序的appid和appSecret
    appId: xxx
    appSecret: xxx

微信配置类

  • 读取并映射,yml中的自定义属性到Java类的属性上
/**
 * 微信配置的Properties属性类
 */
@Setter
@Getter
@NoArgsConstructor
@ToString
@Configuration
@ConfigurationProperties(prefix = "zzyl.wechat")
public class WeChatConfigProperties {
    /**
     * 微信小程序的AppId
     */
    private String appId;

    /**
     * 微信小程序的AppSecret
     */
    private String appSecret;
}

WechatService,微信业务层接口

/**
 * 微信业务层接口
 */
public interface WechatService {
    /**
     * 获取openid
     *
     * @param code 登录凭证
     */
    String getOpenid(String code);

    /**
     * 获取手机号
     *
     * @param phoneCode 手机号凭证
     */
    String getPhoneNumber(String phoneCode);
}

WechatServiceImpl,微信业务层实现类

  • 主要有3个接口需要我们调用,分别为:
    • jscode2session,通过登录临时凭证code,换取微信小程序的openId,也就是小程序登录
    • token,获取微信小程序的AccessToken,获取用户手机号请求中,需要添加该参数
    • getuserphonenumber,通过获取手机号临时凭证phoneCode,获取用户的手机号
/**
 * 微信业务层实现类
 */
@Service
public class WechatServiceImpl implements WechatService {
    /**
     * 使用code,获取OpenId
     */
    private static final String CODE_2_SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session?grant_type=authorization_code";

    /**
     * 获取token
     */
    private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential";

    /**
     * 获取手机号
     */
    private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";

    /**
     * 微信配置信息
     */
    @Autowired
    private WeChatConfigProperties weChatConfigProperties;


    /**
     * 获取openid
     *
     * @param code 微信小程序的登录临时凭证
     */
    @Override
    public String getOpenid(String code) {
        // 获取公共参数
        Map<String, Object> requestUrlParam = getAppConfig();
        // 登录时获取的 code,可通过wx.login获取
        requestUrlParam.put("js_code", code);
        // 授权类型
        requestUrlParam.put("grant_type", "authorization_code");

        // 发起请求,用code换取openId
        String result = HttpUtil.get(CODE_2_SESSION_URL, requestUrlParam);

        JSONObject jsonObject = JSONUtil.parseObj(result);
        // 若code不正确,则获取不到openid,那么响应失败
        Integer errCode = jsonObject.getInt("errcode");
        if (errCode != null && errCode != 0) {
            // 错误信息
            String errMsg = jsonObject.getStr("errmsg");
            throw new RuntimeException(errMsg);
        }
        return jsonObject.getStr("openid");
    }

    /**
     * 获取手机号
     *
     * @param phoneCode 手机号凭证
     */
    @Override
    public String getPhoneNumber(String phoneCode) {
        // 获取微信的AccessToken
        String accessToken = getAccessToken();

        // 将token,拼接到请求路径中
        String url = PHONE_REQUEST_URL + accessToken;

        Map<String, Object> param = new HashMap<>();
        // 手机号临时凭证
        param.put("code", phoneCode);

        // 参数要转成json,不能直接传Map,否则会返回失败,提示请求参数格式不正确
        String json = JSONUtil.toJsonStr(param);
        // 发起请求
        String result = HttpUtil.post(url, json);

        // 解析响应
        JSONObject jsonObject = JSONUtil.parseObj(result);
        Integer errCode = jsonObject.getInt("errcode");

        // 若code不正确,则获取不到手机号,那么响应失败
        if (errCode != 0) {
            // 错误信息
            String errMsg = jsonObject.getStr("errmsg");
            throw new RuntimeException(errMsg);
        }

        /*
            响应示例:
            {
                "errcode":0,
                "errmsg":"ok",
                "phone_info": {
                    "phoneNumber":"xxxxxx",
                    "purePhoneNumber": "xxxxxx",
                    "countryCode": 86,
                    "watermark": {
                        "timestamp": 1637744274,
                        "appid": "xxxx"
                    }
                }
            }
         */

        // 用户的手机号信息
        JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");
        // 获取手机号
        return phoneInfo.getStr("purePhoneNumber");
    }

    /**
     * 获取微信的AccessToken
     */
    public String getAccessToken() {
        // 获取公共参数
        Map<String, Object> requestUrlParam = getAppConfig();
        // 授权类型
        requestUrlParam.put("grant_type", "client_credential");

        // 发起请求
        String result = HttpUtil.get(TOKEN_URL, requestUrlParam);

        // 解析响应
        JSONObject jsonObject = JSONUtil.parseObj(result);
        // 错误码
        Integer errCode = jsonObject.getInt("errcode");

        // 如果错误,则返回错误信息
        if (errCode != null && errCode != 0) {
            // 错误信息
            String errMsg = jsonObject.getStr("errmsg");
            throw new RuntimeException(errMsg);
        }
        return jsonObject.getStr("access_token");
    }

    /**
     * 获取公共参数
     */
    private Map<String, Object> getAppConfig() {
        Map<String, Object> requestUrlParam = new HashMap<>();
        requestUrlParam.put("appid", weChatConfigProperties.getAppId());
        requestUrlParam.put("secret", weChatConfigProperties.getAppSecret());
        return requestUrlParam;
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容