大家好,欢迎来到IT知识分享网。
微信登录的接口
我们来看一下微信官方给提供的这个接口,这个是用于微信登录的接口,链接如下:
https://developers.weixin..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
方法来去构建更多的条件参数
查询天气预报
接口文档说明:https://market.aliyun.com/products/57096001/cmapi013828.html?spm=5176.730005.result.10.27823524RFEJhP&innerSource=search_%E5%A4%A9%E6%B0%94%E9%A2%84%E6%8A%A5#sku=yuncode782800000
使用糊涂工具调用天气预报接口:
@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":"13fea4fb9ed3deee1e5909d5af60dfcefddcfe13f55ecad3" }
以上三个参数,都是前端开发人员调用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..com/sns/jscode2session?grant_type=authorization_code"; // 获取token private static final String TOKEN_URL = "https://api.weixin..com/cgi-bin/token?grant_type=client_credential"; // 获取手机号 private static final String PHONE_REQUEST_URL = "https://api.weixin..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:
读取配置文件的配置类
@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() ; } }
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/126370.html