Spring Boot 快速接入微信登錄
前言
小程序登錄在開發(fā)中是最常見的需求,哪怕小程序登錄不是你做,你還是要了解一下流程,后續(xù)都要使用到openId和unionId,你需要知道這些是干什么的。
需求分析
點(diǎn)擊登錄會彈出彈窗,需要獲取用戶手機(jī)號進(jìn)行登錄。
圖片
微信登錄業(yè)務(wù)邏輯規(guī)則:
圖片
思路說明
參考微信官方文檔的提供的思路,官方文檔:
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
微信官方推薦登錄流程:
圖片
注意點(diǎn):
- 前端在小程序集成微信相關(guān)依賴,調(diào)用
wx.login獲取臨時(shí)登錄憑證code,傳給后端。 - 后端調(diào)用
auth.code2Session接口,換取openId和、UnionId、會話秘鑰Session_Key。 - 開發(fā)者服務(wù)器可以根據(jù)用戶標(biāo)識自定義登錄狀態(tài),用于后續(xù)業(yè)務(wù)邏輯中前后端交互識別用戶身份。
表結(jié)構(gòu)說明
創(chuàng)建一張表,用于存儲用戶的信息以及oenId。
圖片
建表語句:
CREATE TABLE "family_member" (
"id" bigint NOT NULL AUTO_INCREMENT COMMENT '主鍵',
"phone" varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手機(jī)號',
"name" varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名稱',
"avatar" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '頭像',
"open_id" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OpenID',
"gender" int DEFAULT NULL COMMENT '性別(0:男,1:女)',
"create_time" timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
"update_time" timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時(shí)間',
"create_by" bigint DEFAULT NULL COMMENT '創(chuàng)建人',
"update_by" bigint DEFAULT NULL COMMENT '更新人',
"remark" varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '備注',
PRIMARY KEY ("id") USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC COMMENT='老人家屬';接口說明
接口跟平時(shí)的接口略有不同,參考微信開發(fā)者平臺提供的流程開發(fā)。
請求參數(shù):
{
"code": "0e36jkGa1ercRF0Fu4Ia1V3fPD06jkGW", //臨時(shí)登錄憑證code
"nickName": "微信用戶",
"phoneCode": "13fe315872a4fb9ed3deee1e5909d5af60dfce7911013436fddcfe13f55ecad3"
}以上三個(gè)參數(shù)都是前端調(diào)用wx.login獲取返回的參數(shù)。
- code: 臨時(shí)登錄憑證code(有效時(shí)間5分鐘)
- nickName: 微信用戶昵稱(現(xiàn)在統(tǒng)一返回:微信用戶)
- phoneCode: 詳細(xì)用戶信息code,后臺根據(jù)此code獲取手機(jī)號。
響應(yīng)示例:
{
"code": 200,
"msg": "操作成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlpb3mn7_lvIDoirE4OTE1IiwiZXhwIjoxNDY1MjI3MTMyOCwidXNlcmlkIjoxfQ.nB6ElZbUywh-yiHDNMJS8WqUpcLWCszVdvAMfySFxIM",
"nickName": "好柿開花8915"
},
"operationTime": null
}小程序環(huán)境搭建
必要配置
測試階段使用測試號,在微信小程序后臺獲取appId和小程序秘鑰,前端和后端都需要這兩個(gè)參數(shù)。
圖片
基礎(chǔ)環(huán)境說明
修改請求路徑。
圖片
本地開發(fā)忽略https校驗(yàn)。
圖片
修改小程序環(huán)境的APPID,改為自己申請的測試號APPID。
圖片
功能實(shí)現(xiàn)
實(shí)現(xiàn)思路
圖片
控制層
Controller:
@PostMapping("/login")
@ApiOperation("小程序登錄")
public AjaxResult login(@RequestBody UserLoginRequestDto userLoginRequestDto){
LoginVo loginVo = familyMemberService.login(userLoginRequestDto);
return success(loginVo);
}UserLoginRequestDTO:
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* C端用戶登錄
*/
@Data
public class UserLoginRequestDto {
@ApiModelProperty("昵稱")
private String nickName;
@ApiModelProperty("登錄臨時(shí)憑證")
private String code;
@ApiModelProperty("手機(jī)號臨時(shí)憑證")
private String phoneCode;
}LoginVo:
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* LoginVO
* @author itheima
*/
@Data
@ApiModel(value = "登錄對象")
public class LoginVo {
@ApiModelProperty(value = "JWT token")
private String token;
@ApiModelProperty(value = "昵稱")
private String nickName;
}業(yè)務(wù)層【重要】
一般像這種三方接口調(diào)用,通常會封裝一個(gè)單獨(dú)業(yè)務(wù)代碼,使其更通用。
? 獲取用戶openId
? 獲取手機(jī)號
? 獲取token(獲取手機(jī)號需要)
微信接口調(diào)用-單獨(dú)封裝
新增WeachatService接口:
public interface WechatService {
/**
* 獲取openid
* @param code
* @return
*/
public String getOpenid(String code);
/**
* 獲取手機(jī)號
* @param detailCode
* @return
*/
public String getPhone(String detailCode);
}新增WeachatServiceImpl實(shí)現(xiàn)類:
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zzyl.nursing.service.WechatService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
public class WechatServiceImpl implements WechatService {
// 登錄
private static final String REQUEST_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ī)號
private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
@Value("${wechat.appId}")
private String appid;
@Value("${wechat.appSecret}")
private String secret;
/**
* 獲取openid
* @param code
* @return
*/
@Override
public String getOpenid(String code) {
//獲取公共參數(shù)
Map<String,Object> paramMap = getAppConfig();
paramMap.put("js_code",code);
String result = HttpUtil.get(REQUEST_URL, paramMap);
//是一個(gè)map
JSONObject jsonObject = JSONUtil.parseObj(result);
//判斷接口響應(yīng)是否出錯(cuò)
if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
String openid = jsonObject.getStr("openid");
return openid;
}
/**
* 封裝公共參數(shù)
* @return
*/
private Map<String, Object> getAppConfig() {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("appid",appid);
paramMap.put("secret",secret);
return paramMap;
}
/**
* 獲取手機(jī)號
* @param detailCode
* @return
*/
@Override
public String getPhone(String detailCode) {
String token = getToken();
String url = PHONE_REQUEST_URL+token;
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("code",detailCode);
//發(fā)起請求
String result = HttpUtil.post(url, JSONUtil.toJsonStr(paramMap));
//是一個(gè)map
JSONObject jsonObject = JSONUtil.parseObj(result);
//判斷接口響應(yīng)是否出錯(cuò)
if(jsonObject.getInt("errcode") != 0){
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
return jsonObject.getJSONObject("phone_info").getStr("phoneNumber");
}
/**
* 獲取token
* @return
*/
private String getToken() {
Map<String, Object> paramMap = getAppConfig();
//發(fā)起請求
String result = HttpUtil.get(TOKEN_URL, paramMap);
//是一個(gè)map
JSONObject jsonObject = JSONUtil.parseObj(result);
//判斷接口響應(yīng)是否出錯(cuò)
if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
String token = jsonObject.getStr("access_token");
return token;
}
}上面的代碼需要讀取獲取appId和appSecret,所以我們在application.yml配置對于配置。
圖片
微信登錄業(yè)務(wù)開發(fā)
/**
* 微信登錄
* @param userLoginRequestDto
* @return
*/
LoginVo login(UserLoginRequestDto userLoginRequestDto);實(shí)現(xiàn)方法:
@Autowired
private WechatService wechatService;
@Autowired
private TokenService tokenService;
static List<String> DEFAULT_NICKNAME_PREFIX = ListUtil.of("生活更美好",
"大桔大利",
"日富一日",
"好柿開花",
"柿柿如意",
"一椰暴富",
"大柚所為",
"楊梅吐氣",
"天生荔枝"
);
/**
* 小程序端登錄
* @param userLoginRequestDto
* @return
*/
@Override
public LoginVo login(UserLoginRequestDto userLoginRequestDto) {
//1.調(diào)用微信api,根據(jù)code獲取openId
String openId = wechatService.getOpenid(userLoginRequestDto.getCode());
//2.根據(jù)openId查詢用戶
FamilyMember familyMember = getOne(Wrappers.<FamilyMember>lambdaQuery(FamilyMember.class)
.eq(FamilyMember::getOpenId, openId));
//3.如果用戶為空,則新增
if (ObjectUtil.isEmpty(familyMember)) {
familyMember = FamilyMember.builder().openId(openId).build();
}
//4.調(diào)用微信api獲取用戶綁定的手機(jī)號
String phone = wechatService.getPhone(userLoginRequestDto.getPhoneCode());
//5.保存或修改用戶
saveOrUpdateFamilyMember(familyMember, phone);
//6.將用戶id存入token,返回
Map<String, Object> claims = new HashMap<>();
claims.put("userId", familyMember.getId());
claims.put("userName", familyMember.getName());
String token = tokenService.createToken(claims);
LoginVo loginVo = new LoginVo();
loginVo.setToken(token);
loginVo.setNickName(familyMember.getName());
return loginVo;
}
/**
* 保存或修改客戶
* @param member
* @param phone
*/
private void saveOrUpdateFamilyMember(FamilyMember member, String phone) {
//1.判斷取到的手機(jī)號與數(shù)據(jù)庫中保存的手機(jī)號不一樣
if(ObjectUtil.notEqual(phone, member.getPhone())){
//設(shè)置手機(jī)號
member.setPhone(phone);
}
//2.判斷id存在
if (ObjectUtil.isNotEmpty(member.getId())) {
updateById(familyMember);
return;
}
//3.保存新的用戶
//隨機(jī)組裝昵稱,詞組+手機(jī)號后四位
String nickName = DEFAULT_NICKNAME_PREFIX.get((int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size()))
+ StringUtils.substring(member.getPhone(), 7);
member.setName(nickName);
save(member);
}注意:
小程序所有請求不走后臺的用戶,所以在新增或修改的時(shí)候,不需要自動(dòng)填充創(chuàng)建人和修改人,修改MP的自動(dòng)填充。
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.zzyl.common.core.domain.model.LoginUser;
import com.zzyl.common.utils.SecurityUtils;
import lombok.SneakyThrows;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Autowired
private HttpServletRequest request;
@SneakyThrows
public boolean isExclude() {
String requestURI = request.getRequestURI();
if(requestURI.startsWith("/member")){
returnfalse;
}
returntrue;
}
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
if(isExclude()){
this.strictInsertFill(metaObject, "createBy", String.class, loadUserId() + "");
}
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
if(isExclude()){
this.setFieldValByName("updateBy", loadUserId() + "", metaObject);
}
}
/**
* 獲取當(dāng)前登錄人的ID
*
* @return
*/
private static Long loadUserId() {
//獲取當(dāng)前登錄人的id
try {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (ObjectUtils.isNotEmpty(loginUser)) {
return loginUser.getUserId();
}
return 1L;
} catch (Exception e) {
return 1L;
}
}
}校驗(yàn)Toeken
思路分析
用戶登錄成功之后,返回前端一個(gè)token,這個(gè)token就是用來驗(yàn)證用戶信息的,用戶點(diǎn)擊小程序中的其他操作,就會token攜帶請求頭header中,方便后臺去驗(yàn)證獲取用戶信息,流程如下:
圖片
如果要驗(yàn)證用戶的token,我們可以使用攔截器實(shí)現(xiàn)。
圖片
代碼如下:
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.zzyl.common.exception.base.BaseException;
import com.zzyl.common.utils.StringUtils;
import com.zzyl.common.utils.UserThreadLocal;
import com.zzyl.framework.web.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
@Component
public class MemberInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//判斷當(dāng)前請求是否是handler()
if(!(handler instanceof HandlerMethod)){
returntrue;
}
//獲取token
String token = request.getHeader("authorization");
if(StringUtils.isEmpty(token)){
throw new BaseException("認(rèn)證失敗");
}
//解析token
Map<String, Object> claims = tokenService.parseToken(token);
if(ObjectUtil.isEmpty(claims)){
throw new BaseException("認(rèn)證失敗");
}
Long userId = MapUtil.get(claims, "userId", Long.class);
if(ObjectUtil.isEmpty(userId)){
throw new BaseException("認(rèn)證失敗");
}
//把數(shù)據(jù)存儲到線程中
UserThreadLocal.set(userId);
returntrue;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
}使攔截器生效(WebMvcConfigurer實(shí)現(xiàn)類):
/**
* 自定義攔截規(guī)則
*/
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
registry.addInterceptor(membersInterceptor).excludePathPatterns(EXCLUDE_PATH_PATTERNS).addPathPatterns("/member/**");
}總結(jié)
? openId是用戶在這個(gè)小程序的唯一標(biāo)識,unionId是微信是你在微信開發(fā)平臺的唯一標(biāo)識,就是多個(gè)小程序中你的unionId都是一樣的。
? 前端wx.login獲取臨時(shí)登錄code,傳給后端,后端用來換取openId。
? 獲取手機(jī)號需要先獲取token,然后再去獲取手機(jī)號。






























