微信登录的接口
我们来看一下微信官方给提供的这个接口,这个是用于微信登录的接口,链接如下:
https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
这个文档中比较关键的部分就是:
-
功能描述,查阅是否符合我们业务对接的需求
-
调用方式,三方接口一般都是https居多,更加安全,包含了请求方式和请求地址
-
请求参数,接口的入参,特别注意的是,必须要传的参数
-
返回参数,查看返回的数据结构和内容,是否符合自己的返回预期
-
错误码,当接口调用失败的时候,可以查阅以方便修正接口调用失败原因
-
HTTP客户端工具有很多
-
HttpClient 体量庞大,jdk11以上支持不好
-
OkHttp 学习成本较高
-
Spring提供的RestTemplate 后期课程feign会重点讲解
-
糊涂工具包,使用说明地址:https://doc.hutool.cn/pages/http/
Hutool-http优点
- 根据URL自动判断是请求HTTP还是HTTPS,不需要单独写多余的代码。
- 表单数据中有File对象时自动转为
multipart/form-data
表单,不必单独做操作。 - 默认情况下Cookie自动记录,比如可以实现模拟登录,即第一次访问登录URL后后续请求就是登录状态。
- 自动识别304跳转并二次请求
- 自动识别页面编码,即根据header信息或者页面中的相关标签信息自动识别编码,最大可能避免乱码。
- 自动识别并解压Gzip格式返回内容
糊涂工具常见方法
1.添加单元测试依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
2.
-
get请求
@Test
public void testGet() {
// 最简单的HTTP请求,可以自动通过header等信息判断编码,不区分HTTP和HTTPS
String result = HttpUtil.get("https://www.baidu.com");
System.out.println(result);
}
@Test
public void testGetByParam() {
//可以单独传入http参数,这样参数会自动做URL编码,拼接在URL中
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("pageNum", 1);
paramMap.put("pageSize", 10);
String result = HttpUtil.get("http://localhost:9995/nursing_project", paramMap);
System.out.println(result);
}
@Test
public void testCreateRequest() {
String url = "http://localhost:9995/nursing_project";
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("pageNum", 1);
paramMap.put("pageSize", 10);
HttpResponse response = HttpUtil.createRequest(Method.GET, url)
.form(paramMap)
.execute();
if (response.getStatus() == 200) {
System.out.println(response.body());
}
}
-
post请求
@Test
public void testPost(){
String url = "http://localhost:9995/nursing_project";
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", "护理项目测试");
paramMap.put("orderNo", 1);
paramMap.put("unit", "次");
paramMap.put("price", 10.00);
paramMap.put("image", "https://yjy-slwl-oss.oss-cn-hangzhou.aliyuncs.com/ae7cf766-fb7b-49ff-a73c-c86c25f280e1.png");
paramMap.put("nursingRequirement", "无特殊要求");
paramMap.put("status", 1);
String result = HttpUtil.post(url, JSONUtil.toJsonStr(paramMap));
System.out.println(result);
}
@Test
public void testPost2(){
String url = "http://localhost:9995/nursing_project";
HashMap<String, Object> paramMap = new HashMap<>();
paramMap.put("name", "护理项目测试2");
paramMap.put("orderNo", 1);
paramMap.put("unit", "次");
paramMap.put("price", 10.00);
paramMap.put("image", "https://yjy-slwl-oss.oss-cn-hangzhou.aliyuncs.com/ae7cf766-fb7b-49ff-a73c-c86c25f280e1.png");
paramMap.put("nursingRequirement", "无特殊要求");
paramMap.put("status", 1);
HttpResponse response = HttpUtil.createRequest(Method.POST, url)
.body(JSONUtil.toJsonStr(paramMap))
.execute();
if(response.getStatus() == 200){
System.out.println(response.body());
}
}
以上两个方法调用相对简单,如果请求参数中没有额外的条件设置,就可以使用上述的两个方法,但是,有的时候三方接口调用需要使用到header或者是文件上传,则不能使用上述两个方法
我们可以使用HttpUtil.
createRequest
方法来去构建更多的条件参数
查询天气预报
使用糊涂工具调用天气预报接口:
@Test
public void testWeater() {
String url = "https://aliv18.data.moji.com/whapi/json/alicityweather/forecast24hours";
String appcode = "d0bbfea3151e8bc1b";
Map<String, Object> param = new HashMap<String, Object>();
param.put("cityId", "2");
HttpResponse response = HttpUtil.createRequest(Method.POST, url)
.header("Authorization", "APPCODE " + appcode)
.form(param)
.execute();
if (response.getStatus() == 200) {
System.out.println(response.body());
}
}
响应的结果:
{
"code": 0,
"data": {
"city": {
"cityId": 2,
"counname": "中国",
"ianatimezone": "Asia/Shanghai",
"name": "北京市",
"pname": "北京市",
"secondaryname": "北京市",
"timezone": "8"
},
"hourly": [
{
"condition": "晴",
"conditionId": "5",
"date": "2024-01-04",
"hour": "20",
"humidity": "20",
"iconDay": "0",
"iconNight": "30",
"pop": "0",
"pressure": "1014",
"qpf": "0",
"realFeel": "1",
"snow": "0",
"temp": "7",
"updatetime": "2024-01-04 20:12:16",
"uvi": "1",
"windDegrees": "0",
"windDir": "N",
"windSpeed": "22.284",
"windlevel": "4"
},
{
"condition": "晴",
"conditionId": "5",
"date": "2024-01-04",
"hour": "21",
"humidity": "39",
"iconDay": "0",
"iconNight": "30",
"pop": "0",
"pressure": "1022",
"qpf": "0.0",
"realFeel": "-1",
"snow": "0",
"temp": "5",
"updatetime": "2024-01-04 20:12:16",
"uvi": "1",
"windDegrees": "315",
"windDir": "WNW",
"windSpeed": "24.08",
"windlevel": "4"
}
]
},
"msg": "success",
"rc": {
"c": 0,
"p": "success"
}
}
微信登录的流程
注意点:
-
前端在小程序中集成微信相关依赖,当用户请求登录的同时,调用wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
-
后端服务器调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key
-
开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
-
表结构设计
从小程序端登录的用户主要有两类,第一类是参观预约的用户,第二类是,老人的家人(子女),方便查看老人信息、给老人下单、查看账单、查看合同等服务。
如果是老人的家人,需要跟入住的老人进行绑定,方便后期的更多服务,所以,在小程序端登录的用户,我们需要保存下来,保存到一张表中 member(客户表)
根据我们梳理的信息,我们现在可以大概知道,客户表中比如会有如下字段:
-
主键、手机号、名称、头像、微信OpenID(个人微信唯一ID)、性别
然后再加上必要的字段:
-
创建时间、更新时间、创建人、更新人、备注
最终的创建表的sql如下:
CREATE TABLE "member" (
"id" bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
"phone" varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号',
"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 '创建时间',
"update_time" timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
"create_by" bigint DEFAULT NULL COMMENT '创建人',
"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=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
-
登录接口说明
这个接口跟我们平时开发的接口略有不同,这个需要参考微信开发者平台提供的流程来开发。
目前,小程序的代码已经开发完了,我们需要在后台提供接口就可以了
-
接口路径(已固定):
/customer/user/login
-
请求方式(已固定):
POST
-
请求参数:(已固定)
{
"code": "0e36jkGa1ercRF0Fu4Ia1V3fPD06jkGW", //临时登录凭证code "nickName": "微信用户",
"phoneCode":"13fe315872a4fb9ed3deee1e5909d5af60dfce7911013436fddcfe13f55ecad3" }
以上三个参数,都是前端开发人员调用wx.login()方法返回的数据
-
code:临时登录凭证code
-
nickName:微信用户昵称(现在微信统一返回为:微信用户)
-
phoneCode:详细用户信息code,后台根据此code可以获取用户手机号
-
响应示例
-
-
-
{ "code": 200, "msg": "操作成功", "data": { "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiLlpb3mn7_lvIDoirE4OTE1IiwiZXhwIjoxNDY1MjI3MTMyOCwidXNlcmlkIjoxfQ.nB6ElZbUywh-yiHDNMJS8WqUpcLWCszVdvAMfySFxIM", "nickName": "好柿开花8915" }, "operationTime": null }
微信登录后端接口实现
如图所视:我们对于wechat的接口服务有2个接口方法:
1.获取openId
2.获取手机号
我们可用参考微信小程序开发的文档:
可以确认在开发中的模板:
1.新增类CustomerUserController
/**
* <p>
* 用户管理
*/
@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);
}
}
2.UserLoginRequestDto
/**
* C端用户登录
*/
@Data
public class UserLoginRequestDto {
@ApiModelProperty("昵称")
private String nickName;
@ApiModelProperty("登录临时凭证")
private String code;
@ApiModelProperty("手机号临时凭证")
private String phoneCode;
}
3.返回类型:LoginVo
@Data
@ApiModel(value = "登录对象")
public class LoginVo {
@ApiModelProperty(value = "JWT token")
private String token;
@ApiModelProperty(value = "昵称")
private String nickName;
}
4.mapper
@Mapper
public interface MemberMapper {
@Select("SELECT * FROM member WHERE open_id = #{openId}")
Member getByOpenId(String openId);
void save(Member member);
void update(Member member);
}
5.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>
<insert id="save" 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="update" 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>
我们可以先分析微信开发者平台的接口,接口地址:
-
获取用户openId
-
获取手机号
-
获取token:获取接口调用凭据 | 微信开放文档
-
-
1.微信接口调用-单独封装
新增WechatService
public interface WechatService {
/**
* 获取openid
* @param code 登录凭证
* @return
* @throws IOException
*/
public String getOpenid(String code) ;
/**
* 获取手机号
* @param code 手机号凭证
* @return
* @throws IOException
*/
public String getPhone(String code);
}
WechatService实现类
@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";
// 获取手机号
private static final String PHONE_REQUEST_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=";
@Value("${zzyl.wechat.appId}")
private String appId;
@Value("${zzyl.wechat.appSecret}")
private String secret;
/**
* 获取openid
*
* @param code 登录凭证
* @return
* @throws IOException
*/
@Override
public String getOpenid(String code) throws IOException {
//封装参数
Map<String,Object> requestUrlParam = getAppConfig();
requestUrlParam.put("js_code",code);
String result = HttpUtil.get(REQUEST_URL, requestUrlParam);
JSONObject jsonObject = JSONUtil.parseObj(result);
// 若code不正确,则获取不到openid,响应失败
if (ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))) {
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
return jsonObject.getStr("openid");
}
/**
* 封装公共参数
* @return
*/
private Map<String, Object> getAppConfig() {
Map<String, Object> requestUrlParam = new HashMap<>();
requestUrlParam.put("appid",appId);
requestUrlParam.put("secret",secret);
return requestUrlParam;
}
/**
* 获取手机号
*
* @param code 手机号凭证
* @return
* @throws IOException
*/
@Override
public String getPhone(String code) throws IOException {
//获取access_token
String token = getToken();
//拼接请求路径
String url = PHONE_REQUEST_URL + token;
Map<String,Object> param = new HashMap<>();
param.put("code",code);
String result = HttpUtil.post(url, JSONUtil.toJsonStr(param));
JSONObject jsonObject = JSONUtil.parseObj(result);
if (jsonObject.getInt("errcode") != 0) {
//若code不正确,则获取不到phone,响应失败
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
return jsonObject.getJSONObject("phone_info").getStr("purePhoneNumber");
}
public String getToken(){
Map<String, Object> requestUrlParam = getAppConfig();
String result = HttpUtil.get(TOKEN_URL, requestUrlParam);
//解析
JSONObject jsonObject = JSONUtil.parseObj(result);
//如果code不正确,则失败
if(ObjectUtil.isNotEmpty(jsonObject.getInt("errcode"))){
throw new RuntimeException(jsonObject.getStr("errmsg"));
}
return jsonObject.getStr("access_token");
}
}
由于上述代码需要读取配置来获取微信小程序的appid和appSecret ,所以需要在application.yml添加对应配置,这里大家注意,这个要跟微信开发者平台设置的相同,就是我们刚才自己申请的测试小程序的appid和appSecret
项目名称:
wechat:
appId: xxxxxxxxxxxxxxx
appSecret: xxxxxxxxxxxxxxxxxxxxxx
注意,使用自己申请的appid和secret,不然小程序无法登录
2.微信登录业务开发
定义:MemberService
public interface MemberService {
/**
* 小程序端登录
* @param userLoginRequestDto
* @return
*/
LoginVo login(UserLoginRequestDto userLoginRequestDto);
}
定义:MemberService实现类MemberServiceImpl
@Service
public class MemberServiceImpl implements MemberService {
@Autowired
private WechatService wechatService;
@Autowired
private MemberMapper memberMapper;
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
static ArrayList DEFAULT_NICKNAME_PREFIX = Lists.newArrayList(
"生活更美好",
"大桔大利",
"日富一日",
"好柿开花",
"柿柿如意",
"一椰暴富",
"大柚所为",
"杨梅吐气",
"天生荔枝"
);
/**
* 小程序端登录
*
* @param userLoginRequestDto
* @return
*/
@Override
public LoginVo login(UserLoginRequestDto userLoginRequestDto){
//1.调用微信api,根据code获取openId
String openId = wechatService.getOpenid(userLoginRequestDto.getCode());
//2.根据openId查询用户
Member member = memberMapper.getByOpenId(openId);
//3.如果用户为空,则新增
if (ObjectUtil.isEmpty(member)) {
member = Member.builder().openId(openId).build();
}
//4.调用微信api获取用户绑定的手机号
String phone = wechatService.getPhone(userLoginRequestDto.getPhoneCode());
//5.保存或修改用户
saveOrUpdate(member, phone);
//6.将用户id存入token,返回
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.JWT_USERID, member.getId());
claims.put(Constants.JWT_USERNAME, member.getName());
String token = JwtUtil.createJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), jwtTokenManagerProperties.getTtl(), claims);
LoginVo loginVo = new LoginVo();
loginVo.setToken(token);
loginVo.setNickName(member.getName());
return loginVo;
}
/**
* 保存或修改客户
*
* @param member
* @param phone
*/
private void saveOrUpdate(Member member, String phone) {
//1.判断取到的手机号与数据库中保存的手机号是否一样
if(ObjectUtil.notEqual(phone, member.getPhone())){
//设置手机号
member.setPhone(phone);
}
//2.判断id存在
if (ObjectUtil.isNotEmpty(member.getId())) {
memberMapper.update(member);
return;
}
//3.保存新的用户
//随机组装昵称,词组+手机号后四位
String nickName = DEFAULT_NICKNAME_PREFIX.get((int) (Math.random() * DEFAULT_NICKNAME_PREFIX.size()))
+ StringUtils.substring(member.getPhone(), 7);
member.setName(nickName);
memberMapper.save(member);
}
}
其中jwt相关的配置,我们已经在application.yml文件中定义,主要有两个属性,一个是签名,一个是过期时间
项目:
framework:
jwt:
base64-encoded-secret-key: $2a$10$PVtHnkj86mJgf6li/yron.LRx/cQAlaiZkBJ9BeogCNTryXJRT1YC
ttl: 3600000
读取配置文件的配置类
@Setter
@Getter
@NoArgsConstructor
@ToString
@Configuration
@ConfigurationProperties(prefix = "zzyl.framework.jwt")
public class JwtTokenManagerProperties implements Serializable {
/**
* 签名密码
*/
private String base64EncodedSecretKey;
/**
* 有效时间
*/
private Integer ttl;
}
Threadlocal的使用
定义拦截器:
1.前置拦截器: (1) 获取token (2) 解析token (3) 将UserId传入到Threadlocal中共享
2.后置拦截器: (1)清除Threadlocal的数据
@Component
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
@Autowired
private JwtTokenManagerProperties jwtTokenManagerProperties;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//如果不是映射到方法就放行,比如跨域验证请求、静态资源等不需要身份校验的请求
if(!(handler instanceof HandlerMethod)){
return true;
}
//获取header的参数
String token = request.getHeader(Constants.USER_TOKEN);
log.info("开始解析 customer user token:{}",token);
if(ObjectUtil.isEmpty(token)){
//token失效
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
Map<String,Object> claims = JwtUtil.parseJWT(jwtTokenManagerProperties.getBase64EncodedSecretKey(), token);
if (ObjectUtil.isEmpty(claims)) {
//token失效
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
//获取用户ID
Long userId = MapUtil.get(claims, Constants.JWT_USERID, Long.class);
if (ObjectUtil.isEmpty(userId)) {
throw new BaseException(BasicEnum.SECURITY_ACCESSDENIED_FAIL);
}
//存入当前请求的线程中
UserThreadLocal.set(userId);
//以上检查都没问题,一定返回true,这个请求才能继续
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//响应结束,需清理ThreadLocal中的数据,防止内存泄漏
UserThreadLocal.remove();
}
}
public static final String USER_TOKEN ="authorization";
使拦截器生效: 要再webMvcConfig中配置
@Configuration
@ComponentScan("springfox.documentation.swagger.web")
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
//拦截的时候过滤掉swagger相关路径和登录相关接口
private static final String[] EXCLUDE_PATH_PATTERNS = new String[]{"/swagger-ui.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs",
// 登录接口
"/customer/user/login"};
/**
* @Description 拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 小程序端接口鉴权拦截器
registry.addInterceptor(userInterceptor)
.excludePathPatterns(EXCLUDE_PATH_PATTERNS)
.addPathPatterns("/customer/**");
}
}
我们对UserThreadLocal进行了封装:
/**
* subjectContent.java
* 用户主体对象
*/
@Slf4j
public class UserThreadLocal {
/***
* 创建线程局部userVO变量
*/
public static ThreadLocal<String> subjectThreadLocal = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return null;
}
};
// 提供线程局部变量set方法
public static void setSubject(String subject) {
subjectThreadLocal.set(subject);
}
// 提供线程局部变量get方法
public static String getSubject() {
return subjectThreadLocal.get();
}
//清空当前线程,防止内存溢出
public static void removeSubject() {
subjectThreadLocal.remove();
}
private static final ThreadLocal<Long> LOCAL = new ThreadLocal<>();
private UserThreadLocal() {
}
/**
* 将authUserInfo放到ThreadLocal中
*
* @param authUserInfo {@link Long}
*/
public static void set(Long authUserInfo) {
LOCAL.set(authUserInfo);
}
/**
* 从ThreadLocal中获取authUserInfo
*/
public static Long get() {
return LOCAL.get();
}
/**
* 从当前线程中删除authUserInfo
*/
public static void remove() {
LOCAL.remove();
}
/**
* 从当前线程中获取前端用户id
* @return 用户id
*/
public static Long getUserId() {
return LOCAL.get();
}
/**
* 从当前线程中获取前端后端id
* @return 用户id
*/
public static Long getMgtUserId() {
String subject = subjectThreadLocal.get();
if (ObjectUtil.isEmpty(subject)) {
return null;
}
BaseVo baseVo = JSONObject.parseObject(subject, BaseVo.class);
return baseVo.getId() ;
}
}