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

前言

微信小程序登錄-時(shí)序圖jpg.jpg

UniApp前端

  • 頁(yè)面初始化完畢時(shí),調(diào)用uni.login(),獲取到微信小程序的登錄憑證,然后保存到一個(gè)變量(坑,如果不提前獲取,例如點(diǎn)擊登錄時(shí)獲取,有時(shí)候會(huì)失敗,所以就只能提前獲取了)
  • 頁(yè)面放置一個(gè)button按鈕,并需要設(shè)置open-type屬性為getPhoneNumber,則表示該按鈕是用于獲取用戶手機(jī)號(hào)的,然后監(jiān)聽(tīng)getphonenumber事件,當(dāng)用戶允許獲取手機(jī)號(hào)后,獲取到手機(jī)號(hào)信息時(shí)回調(diào)
  • 手機(jī)號(hào)信息中,會(huì)包含以下參數(shù)
    • code,獲取手機(jī)號(hào)的憑證,和uni.login返回的code不一樣,登錄的code只能用于登錄,而這里的code只能用于換取用戶手機(jī)號(hào)。后端定義登錄的code為code,而獲取手機(jī)號(hào)的codephoneCode
    • encryptedData,加密信息
    • iv,加密向量
  • 調(diào)用我們自己后端的login登錄方法,把登錄的code和獲取手機(jī)號(hào)的code,一起傳過(guò)去,如果encryptedDataiv后端有需要的話,也一起傳

前端頁(yè)面

<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>
        微信手機(jī)號(hào)快捷登錄
      </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">手機(jī)</text>
          </button>
          <button>
            <text class="icon icon-mail">郵箱</text>
          </button>
        </view>
      </view>
      <view class="tips">登錄/注冊(cè)即視為你同意《服務(wù)條款》和《隱私協(xié)議》</view>
    </view>
  </view>
</template>

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

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

// 創(chuàng)建用戶信息Store
const userStore = useUserStore()
const { saveProfile } = userStore

// 微信臨時(shí)憑證code(動(dòng)態(tài)令牌,code來(lái)?yè)Q取用戶手機(jī)號(hào)。每個(gè)code有效期為5分鐘,且只能使用一次)
let loginCode = ""

// fix:由于獲取code的時(shí)機(jī)和getPhoneNumber,同時(shí)以 async await 同步形式執(zhí)行,會(huì)有一定概率會(huì)登錄失敗,所以需要將獲取code的時(shí)機(jī)提前,來(lái)解決這個(gè)問(wèn)題
onLoad(async () => {
  // 小程序端,才執(zhí)行小程序登錄
  // #ifndef H5

  // 發(fā)起微信登錄
  const result = await uni.login()
  console.log(result);
  loginCode = result.code
  console.log(`獲取登錄的code成功:${loginCode}`);

  // #endif
})

// 微信手機(jī)號(hào)快捷登錄
const onGetPhone = async (e) => {
  console.log(e);

  // 獲取返回的用戶加密數(shù)據(jù)和解密需要使用的iv
  const { code: phoneCode, encryptedData, iv, errMsg } = e.detail;

  if (!encryptedData || !iv) {
    uni.showToast({
      title: `登錄失?。?{errMsg}`,
      icon: 'none'
    })
    return
  }

  // 調(diào)用自己后端的登錄接口,將encryptedData、iv、code,傳給后端
  const result = await loginByWXAPI({
    encryptedData: encryptedData,
    iv: iv,
    // 登錄的臨時(shí)憑證
    code: loginCode,
    // 獲取手機(jī)號(hào)的臨時(shí)憑證
    phoneCode: phoneCode
  })
  console.log(result);

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

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

請(qǐng)求API

  • 封裝業(yè)務(wù)接口,并提供js請(qǐng)求方法
import http from "@/utils/http";

/**
 * 小程序登錄
 * 
 * @param {string} encryptedData 加密的手機(jī)號(hào)信息 getphonenumber事件回調(diào)中獲取
 * @param {string} iv 加密相關(guān) getphonenumber事件回調(diào)中獲取
 * @param {string} code 通過(guò) wx.login() 獲取
 */
export const loginByWXAPI = (data) => {
    return http({
        url: "/customer/user/login",
        method: "POST",
        data: {
            // 用戶加密數(shù)據(jù)
            encryptedData: data.encryptedData,
            // 解密使用的向量
            iv: data.iv,
            // 登錄臨時(shí)憑證
            code: data.code,
            // 獲取手機(jī)號(hào)的臨時(shí)憑證
            phoneCode: data.phoneCode
        }
    });
};

請(qǐng)求工具類(lèi)

  • 封裝UniApp的請(qǐng)求攔截器和響應(yīng)攔截器
  • 在請(qǐng)求攔截器中,統(tǒng)一將token放到請(qǐng)求頭中
  • 在響應(yīng)攔截器中,進(jìn)行數(shù)據(jù)剝離,以及處理401的HTTP狀態(tài)碼,處理登錄態(tài)失效,跳轉(zhuǎn)到登錄頁(yè),清除本地保存的token信息
  • 在請(qǐng)求前和請(qǐng)求后,展示Loading,以及錯(cuò)誤信息的Toast
import { useUserStore } from '@/store'

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

// 發(fā)請(qǐng)求前觸發(fā)-等價(jià)于請(qǐng)求攔截器
const interceptor = {
  invoke(args) {
    // 獲取用戶信息Store
    const userStore = useUserStore()

    // 統(tǒng)一顯示Loading
    uni.showLoading({ title: "拼命加載中..." });

    // 通用參數(shù)
    const commonParamsHeader = {}

    // 不是https開(kāi)頭,則將URL拼接上基地址(也就是,如果寫(xiě)全了地址,則不拼接基地址了)
    if (!args.url.startsWith("https")) {
      args.url = baseURL + args.url;
    }

    // 設(shè)置token
    const { token } = userStore
    if (token) {
      commonParamsHeader.Authorization = token
    }

    // 設(shè)置請(qǐng)求頭
    args.header = {
      // 保留原本的 header
      ...args.header,
      // 添加小程序端調(diào)用標(biāo)識(shí)
      "source-client": "miniapp",
      // 添加通用參數(shù)
      ...commonParamsHeader
    };
  },
  complete(res) {
    // 請(qǐng)求完成,隱藏Loading 
    uni.hideLoading();
  },
};

// 請(qǐng)求攔截器
uni.addInterceptor("request", interceptor);
// 文件上傳攔截器
uni.addInterceptor("uploadFile", interceptor);

// 發(fā)請(qǐng)求后-等價(jià)于響應(yīng)攔截器
const http = async (options) => {
  // 請(qǐng)求返回結(jié)果,返回一個(gè)數(shù)組,第一個(gè)參數(shù):錯(cuò)誤信息,第二個(gè)參數(shù):接口返回的結(jié)果
  const res = await uni.request(options);

  // HTTP響應(yīng)狀態(tài)碼
  const { statusCode } = res

  // token過(guò)期,跳轉(zhuǎn)到登錄頁(yè)面
  if (statusCode === 401) {
    uni.navigateTo({ url: "/pages/login/login" });
    return
  }

  // 請(qǐng)求成功
  if (statusCode >= 200 && statusCode < 300) {
    return res.data;
  } else {
    // 請(qǐng)求失敗,提示錯(cuò)誤信息
    uni.showToast({
      title: res.data.message,
      icon: 'none'
    })
    return Promise.reject(new Error(res))
  }
};

// 導(dǎo)出
export default http;

Java后端

客戶表實(shí)體類(lèi)

/**
 * 用戶表
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Member extends BaseEntity {
    /**
     * 手機(jī)號(hào)
     */
    private String phone;

    /**
     * 名稱(chēng)
     */
    private String name;

    /**
     * 頭像
     */
    private String avatar;

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

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

客戶登錄Dto

/**
 * C端用戶登錄Dto
 */
@Data
public class UserLoginRequestDto {
    @ApiModelProperty("微信昵稱(chēng)")
    private String nickName;

    @ApiModelProperty("登錄臨時(shí)憑證")
    private String code;

    @ApiModelProperty("手機(jī)號(hào)臨時(shí)憑證")
    private String phoneCode;
}

客戶登錄Vo

/**
 * C端用戶登錄Vo
 */
@Data
@Builder
@ApiModel(value = "登錄對(duì)象")
public class LoginVo {
    @ApiModelProperty(value = "JWT token")
    private String token;

    @ApiModelProperty(value = "昵稱(chēng)")
    private String nickName;
}

CustomerUserController,登錄接口

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

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

MemberService,客戶業(yè)務(wù)層接口

/**
 * C端用戶業(yè)務(wù)層接口
 */
public interface MemberService {
    /**
     * 微信小程序端登錄
     */
    LoginVo login(UserLoginRequestDto userLoginRequestDto);
}

MemberServiceImpl,客戶業(yè)務(wù)層實(shí)現(xiàn)類(lèi)

  • 通過(guò)前端傳過(guò)來(lái)的登錄臨時(shí)憑證code,換取用戶的openId,也就是小程序登錄
  • 以及通過(guò)獲取手機(jī)號(hào)的臨時(shí)憑證phoneCode,換取用戶的手機(jī)號(hào)
  • 通過(guò)openId查詢(xún)用戶是否存在
    • 不存在,則為第一次登錄,保存用戶的手機(jī)號(hào)和openId到數(shù)據(jù)庫(kù)表中
    • 存在,則判斷用戶手機(jī)號(hào)是否有變更,有則更新到數(shù)據(jù)庫(kù)表中
    • 最后,通過(guò)JWT生成用戶的token,并和用戶信息一起返回給前端
  • 與微信的API交互,是通過(guò)HTTP協(xié)議,都封裝到了WechatService
/**
 * C端用戶業(yè)務(wù)層實(shí)現(xiàn)類(lèi)
 */
@Service
public class MemberServiceImpl implements MemberService {
    @Autowired
    private WechatService wechatService;

    @Autowired
    private MemberMapper memberMapper;

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

    /**
     * 用戶昵稱(chēng),隨機(jī)前綴
     */
    private static final List<String> DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
            "生活更美好",
            "大桔大利",
            "日富一日",
            "好柿開(kāi)花",
            "柿柿如意",
            "一椰暴富",
            "大柚所為",
            "楊梅吐氣",
            "天生荔枝"
    );

    /**
     * 小程序端登錄
     */
    @Override
    public LoginVo login(UserLoginRequestDto userLoginRequestDto) {
        // 調(diào)用微信API,根據(jù)前端傳過(guò)來(lái)的code(臨時(shí)憑證),獲取openId
        String openId = wechatService.getOpenid(userLoginRequestDto.getCode());
        // 調(diào)用微信API,獲取用戶綁定的手機(jī)號(hào)
        String phone = wechatService.getPhoneNumber(userLoginRequestDto.getPhoneCode());

        // 根據(jù)openId,查詢(xún)用戶信息
        Member member = memberMapper.getByOpenId(openId);

        // 如果用戶為空信息,則為新用戶,則創(chuàng)建新用戶信息,并設(shè)置openId
        if (ObjectUtil.isEmpty(member)) {
            member = Member.builder()
                    .openId(openId)
                    .build();
        }

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

        // 創(chuàng)建token
        String token = createMemberToken(member);

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

    /**
     * 保存或修改客戶
     *
     * @param member      數(shù)據(jù)庫(kù)中的用戶信息
     * @param phoneNumber 手機(jī)號(hào)
     */
    private void saveOrUpdate(Member member, String phoneNumber) {
        // 如果從微信取到的手機(jī)號(hào),和數(shù)據(jù)庫(kù)中保存的手機(jī)號(hào)不一樣,那么更新手機(jī)號(hào)
        if (ObjectUtil.notEqual(phoneNumber, member.getPhone())) {
            member.setPhone(phoneNumber);
        }

        // id存在,則更新用戶信息
        if (member.getId() != null) {
            memberMapper.updateMember(member);
        } else {
            // 隨機(jī)組裝昵稱(chēng),詞組+手機(jī)號(hào)后四位
            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不存在,則為新用戶,創(chuàng)建用戶
            memberMapper.addMember(member);
        }
    }

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

        // 用戶Id
        claims.put(Constants.JWT_USERID, member.getId());
        // 用戶名稱(chēng)
        claims.put(Constants.JWT_USERNAME, member.getName());

        String secretKey = jwtTokenManagerProperties.getBase64EncodedSecretKey();
        // 過(guò)期時(shí)間
        int dateOffset = jwtTokenManagerProperties.getTtl();

        // 創(chuàng)建token
        return JwtUtil.createJWT(
                secretKey,
                dateOffset,
                claims
        );
    }
}

MemberMapper,客戶Mapper接口

  • 定義客戶表的增、刪、查、改方法
/**
 * C端用戶Mapper
 */
@Mapper
public interface MemberMapper {
    /**
     * 根據(jù)微信OpenId,查詢(xún)用戶信息
     */
    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>

微信相關(guān)業(yè)務(wù)

自定義yml屬性

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

微信配置類(lèi)

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

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

WechatService,微信業(yè)務(wù)層接口

/**
 * 微信業(yè)務(wù)層接口
 */
public interface WechatService {
    /**
     * 獲取openid
     *
     * @param code 登錄憑證
     */
    String getOpenid(String code);

    /**
     * 獲取手機(jī)號(hào)
     *
     * @param phoneCode 手機(jī)號(hào)憑證
     */
    String getPhoneNumber(String phoneCode);
}

WechatServiceImpl,微信業(yè)務(wù)層實(shí)現(xiàn)類(lèi)

  • 主要有3個(gè)接口需要我們調(diào)用,分別為:
    • jscode2session,通過(guò)登錄臨時(shí)憑證code,換取微信小程序的openId,也就是小程序登錄
    • token,獲取微信小程序的AccessToken,獲取用戶手機(jī)號(hào)請(qǐng)求中,需要添加該參數(shù)
    • getuserphonenumber,通過(guò)獲取手機(jī)號(hào)臨時(shí)憑證phoneCode,獲取用戶的手機(jī)號(hào)
/**
 * 微信業(yè)務(wù)層實(shí)現(xiàn)類(lèi)
 */
@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";

    /**
     * 獲取手機(jī)號(hào)
     */
    private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";

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


    /**
     * 獲取openid
     *
     * @param code 微信小程序的登錄臨時(shí)憑證
     */
    @Override
    public String getOpenid(String code) {
        // 獲取公共參數(shù)
        Map<String, Object> requestUrlParam = getAppConfig();
        // 登錄時(shí)獲取的 code,可通過(guò)wx.login獲取
        requestUrlParam.put("js_code", code);
        // 授權(quán)類(lèi)型
        requestUrlParam.put("grant_type", "authorization_code");

        // 發(fā)起請(qǐng)求,用code換取openId
        String result = HttpUtil.get(CODE_2_SESSION_URL, requestUrlParam);

        JSONObject jsonObject = JSONUtil.parseObj(result);
        // 若code不正確,則獲取不到openid,那么響應(yīng)失敗
        Integer errCode = jsonObject.getInt("errcode");
        if (errCode != null && errCode != 0) {
            // 錯(cuò)誤信息
            String errMsg = jsonObject.getStr("errmsg");
            throw new RuntimeException(errMsg);
        }
        return jsonObject.getStr("openid");
    }

    /**
     * 獲取手機(jī)號(hào)
     *
     * @param phoneCode 手機(jī)號(hào)憑證
     */
    @Override
    public String getPhoneNumber(String phoneCode) {
        // 獲取微信的AccessToken
        String accessToken = getAccessToken();

        // 將token,拼接到請(qǐng)求路徑中
        String url = PHONE_REQUEST_URL + accessToken;

        Map<String, Object> param = new HashMap<>();
        // 手機(jī)號(hào)臨時(shí)憑證
        param.put("code", phoneCode);

        // 參數(shù)要轉(zhuǎn)成json,不能直接傳Map,否則會(huì)返回失敗,提示請(qǐng)求參數(shù)格式不正確
        String json = JSONUtil.toJsonStr(param);
        // 發(fā)起請(qǐng)求
        String result = HttpUtil.post(url, json);

        // 解析響應(yīng)
        JSONObject jsonObject = JSONUtil.parseObj(result);
        Integer errCode = jsonObject.getInt("errcode");

        // 若code不正確,則獲取不到手機(jī)號(hào),那么響應(yīng)失敗
        if (errCode != 0) {
            // 錯(cuò)誤信息
            String errMsg = jsonObject.getStr("errmsg");
            throw new RuntimeException(errMsg);
        }

        /*
            響應(yīng)示例:
            {
                "errcode":0,
                "errmsg":"ok",
                "phone_info": {
                    "phoneNumber":"xxxxxx",
                    "purePhoneNumber": "xxxxxx",
                    "countryCode": 86,
                    "watermark": {
                        "timestamp": 1637744274,
                        "appid": "xxxx"
                    }
                }
            }
         */

        // 用戶的手機(jī)號(hào)信息
        JSONObject phoneInfo = jsonObject.getJSONObject("phone_info");
        // 獲取手機(jī)號(hào)
        return phoneInfo.getStr("purePhoneNumber");
    }

    /**
     * 獲取微信的AccessToken
     */
    public String getAccessToken() {
        // 獲取公共參數(shù)
        Map<String, Object> requestUrlParam = getAppConfig();
        // 授權(quán)類(lèi)型
        requestUrlParam.put("grant_type", "client_credential");

        // 發(fā)起請(qǐng)求
        String result = HttpUtil.get(TOKEN_URL, requestUrlParam);

        // 解析響應(yīng)
        JSONObject jsonObject = JSONUtil.parseObj(result);
        // 錯(cuò)誤碼
        Integer errCode = jsonObject.getInt("errcode");

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

    /**
     * 獲取公共參數(shù)
     */
    private Map<String, Object> getAppConfig() {
        Map<String, Object> requestUrlParam = new HashMap<>();
        requestUrlParam.put("appid", weChatConfigProperties.getAppId());
        requestUrlParam.put("secret", weChatConfigProperties.getAppSecret());
        return requestUrlParam;
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容