小鸭子的学习笔记duck

Duck Blog

唐如飞

( ^∀^)/欢迎\( ^∀^)

79 文章数
14 评论数

微信登录

tangrufei
2022-08-29 / 0 评论 / 193 阅读 / 0 点赞

微信第三方登录

课程内容

微信第三方登录课程内容一.三方登录概念1.三方登录介绍1.1.什么是三方登录1.2.三方登录的好处2.Oauth2协议2.1.什么是 Oauth2协议2.2.Oauth2授权流程理解2.3. Oauth2相关概念3.Oauth2授权模式【了解】3.1.授权码模式3.2.简化模式3.3.密码模式3.4.客户端模式二.微信开发平台(企业)1.开通账号1.1.注册账号1.2.开通企业资质1.3.创建应用2.接入微信登录分析2.1.接入流程分析2.2.微信登录API三.微信登录接入测试1.获取授权码1.1.准备环境1.2 准备工作1.2.参数配置1.3 微信配置对象1.4.获取授权码请求1.5.登录拦截器放行1.6 浏览器访问效果:1.7扫描后回调效果:2.获取Token和个人信息2.1.Token返回结果封装2.2.Userinfo返回结果封装2.3.获取Token和个人信息2.4.增加拦截器放行2.5.访问测试3. 平台微信登录业务实现(重点掌握)3.1微信用户表字段分析3.2 业务流程3.3 获取微信用户信息后,按照上面的业务流程图进行代码的逐步实现3.4 代码实现3.5 代码测试前准备

一.三方登录概念

1.三方登录介绍

1.1.什么是三方登录

用户可以使用第三方主流平台如:微信,QQ,支付宝等 来快速登录或者注册你的平台。及时不使用自己平台的账号也能完成登录。而这里的三方主流平台一般是已经拥有大量用户的平台,国外的比如Facebook,Twitter等,国内的比如微博、微信、QQ等。

1.2.三方登录的好处

第三方登录的目的是使用用户在其他平台上频繁使用的账号,来快速登内录己方产品,也可以实现不注容册就能登录,好处就是登录比较方便快捷,安全,可以不用注册,同时达到了为平台引流的效果。

2.Oauth2协议

2.1.什么是 Oauth2协议

OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的,oAuth是Open Authorization的简写,目前的版本是2.0版。

2.2.Oauth2授权流程理解

  • 传统账号授权模式:

1620083354354

  • Oauth2授权码模式:

1620083391073

2.3. Oauth2相关概念

  • Third-party application

    第三方应用程序,本文中又称"客户端"(client),即栗子中的"云打印"。

  • HTTP service

    HTTP服务提供商,本文中简称"服务提供商",即上一节例子中的QQ。

  • Resource Owner

    资源所有者,本文中又称"用户"(user)。

  • User Agent

    用户代理,如浏览器,移动端等。

  • Authorization server

    认证服务器,即服务提供商专门用来处理认证的服务器QQ。

  • Resource server

    资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器QQ

3.Oauth2授权模式【了解】

我们着重理解授权码模式的流程即可,其他的授权模式在项目三中去详细讲

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

  • 授权码模式(authorization code)

  • 简化模式(implicit)

  • 密码模式(resource owner password credentials)

  • 客户端模式(client credentials)

3.1.授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的认授权流程如下:【授权码+令牌】

image-20211008051203926

授权流程:

(A)用户访问客户端,后者将前者导向认证服务器。 (B)用户选择是否给予客户端授权。 (C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。 (D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。 (E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

获取授权码URL如:

/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https://www.xxx.com/callback

获取令牌URL如:

grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=https://www.xxx.com/callback

参数说明: response_type:表示授权类型,必选项,此处的值固定为“code” client_id:表示客户端的ID,必选项 redirect_uri:表示重定向URI,可选项 scope:表示申请的权限范围,可选项 state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

3.2.简化模式

隐式授权模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

image-20211008051352494

3.3.密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

image-20211008051413359

3.4.客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

image-20211008051428551

二.微信开发平台(企业)

1.开通账号

1.1.注册账号

微信开放平台:https://open.weixin.qq.com/

1620084500868

找到注册入口进行注册:https://open.weixin.qq.com/cgi-bin/readtemplate?t=regist/regist_tmpl&lang=zh_CN

1620084589505

填写企业信息

1620085226185

填写注册人信息

1620085265333

1.2.开通企业资质

点击用户名进入个人中心

1620085621919

申请开发者资质认证

1620085554491

根据提示流程,填写机构相关资料。

1.3.创建应用

网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。 在进行微信OAuth2.0授权登录接入之前,在微信开放平台注册开发者帐号,并拥有一个已审核通过的网站应用,并获得相应的AppID和AppSecret,申请微信登录且通过审核后,可开始接入流程。

进入管理中心创建应用

1620085728832

填写应用信息

1620085781888

1620085832770

创建好应用之后,找到创建的应用,获取到appid和appsecret,配置好回调域名,这个非常重要,是在开发中微信调用我们平台的一个域名

1620086031150

2.接入微信登录分析

2.1.接入流程分析

文档:资料中心 -> 网站应用:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/Wechat_Login.html

1620086248834

微信三方登录流程

1620086343992

解释一下:

  • 微信用户 : 就是使用我们平台的用户

  • 三方应用 : 就是我们的平台:pethome

  • 微信开发平台 : 支持微信登录的微信平台

该模式整体流程为:

  1. 我们平台向发起微信授权登录请求

    1. 微信用户允许授权第三方应用(返回一个二维码让用户去扫描)

    2. 微信会重定向到第我们平台(controller),并且带上授权临时票据code参数

  2. 【HttpClient】通过code参数加上AppID和AppSecret等,通过API换取access_token;

  3. 【HttpClient】通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。

【重要】我们接入微信三方登录的目的就是进行微信授权之后,获取微信的用户个人信息绑定到我们平台,用作于我们平台的登录

2.2.微信登录API

见文档:

  1. 获取code

https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect 
  1. 获取Token

http请求方式: GET
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
  1. 刷新Token

http请求方式: GET
https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN
  1. 获取资源

http请求方式: GET
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

三.微信登录接入测试

宏观的登录流程

1620139892018

1.获取授权码

1.1.准备环境

下面是可用的微信登录账号

APPID = "wxd853562a0548a7d0"
SECRET = "4a5d5615f93f24bdba2ba8534642dbb6"
域名:bugtracker.itsource.cn(这里填写的域名是外网可访问的地址) --- 域名只能关联服务器的80端口

对于域名而言,需要在hosts中配置本地域名,因为该域名是一个真实的域名,我们需要让微信回调该域名的时候回调我们本地的应用,在【C:\Windows\System32\drivers\etc\hosts】文件中加入如下配置:

注意:在本地环境,配置域名映射时,指向的IP地址,只能使用默认的80端口,否则会配置失败

127.0.0.1 bugtracker.itsource.cn

1.2 准备工作

1. 方微信登录,会与我们自己的后台,进行交互。
2. 微信获取授权码后,会以调用 配置的回调地址进行返回
所以:后台的访问端口,必须是:80

要将后台启动端口,修改为 80

注意,修改了后台启动端口后,同时需要修改2个前端的axios访问路径

1.2.参数配置

把相关参数配置到yml中,并绑定到对象

pethome:
wechart:
#获取授权码URL
authorization-code-url: "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect"
#获取Token的URL
token-url: "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
#获取用户信息 URL
userinfo-url: "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s"
#刷新Token的 URL
refresh-token-url: "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"
# 应用ID
app-id: "wxd853562a0548a7d0"
# 应用秘钥
app-secrect: "4a5d5615f93f24bdba2ba8534642dbb6"
# 授权码回调接口,对应我们平台的controller
redirect-uri: "http://bugtracker.itsource.cn/wechat/callback"

1.3 微信配置对象

package cn.itsource.pethome.dto;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

//微信登录相关参数实体类
@Data
@Component
@ConfigurationProperties("pethome.wechart")
public class WechatProperties {
private String authorizationCodeUrl;
private String userinfoUrl;
private String refreshTokenUrl;
private String appId;
private String appSecrect;
private String redirectUri;
}

1.4.获取授权码请求

根据微信文档,设置好相关参数,向微信发起一个获取授权码的请求,微信返回一个二维码地址,把该地址重定向到浏览器即可展示二维码:

【注意】 1.不能使用RestController,因为我们要重定向到一个页面 ; 2.这里直接拼接好URL就直接重定向即可,请求交给页面去发

package cn.itsource.pethome.controller;

import cn.itsource.pethome.dto.WechatProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.UUID;

/**
* 处理微信相关业务
*/
@Controller
@RequestMapping("/wechart")
public class WechatController {

@Autowired
private WechatProperties wechatProperties;

/**
* 发起一个微信登录的申请,其实就是获取授权码的申请
* 1、获取到要发送的请求URL
* 2、替换URL中的占位符
* 3、重定向到这个URL去获取授权码code
*/
@GetMapping("/apply")
public String apply(){
//https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect
//生成一个唯一标识作为state
String state = UUID.randomUUID().toString().replace("-","");
//生成完整的授权码URL
String authorizationCodeUrl = String.format(wechatProperties.getAuthorizationCodeUrl(), wechatProperties.getAppId(), wechatProperties.getRedirectUri(), state);
return "redirect:" + authorizationCodeUrl;
}
}

1.5.登录拦截器放行

package cn.itsource.pethome.config;

import cn.itsource.pethome.interceptor.LoginCheckInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

@Autowired
private LoginCheckInterceptor loginCheckInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//将自定义的拦截器,添加到Springmvc的配置中,这样就可以生效
registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**")
//excludePathPatterns:用来配置不需要拦截的资源路径(类似于白名单)
.excludePathPatterns(
"/verifycode/mobile/send", "/file/fastdfs/upload", "/shop/settlement", "/login/admin",
"/user/exist", "/login/user", "/wechat/apply", "/wechat/callback"
);
}
}

1.6 浏览器访问效果:

1620089594112

1.7扫描后回调效果:

1620089008567

2.获取Token和个人信息

2.1.Token返回结果封装

根据返回结果进行对象封装方便操作

//微信获取Token返回结果
@Data
public class WechartTokenDto {
//{"access_token":"44_QfGkFOzewXRoLFGjydhphpo0O6n3sLnRKQlUyUFf0x-LIxMLcSIPiLkeYEKWyt_rx93-XYUuRu4QGwUFstFBO2gfUTOS1Q4D7g2IDM30Bc8",
// "expires_in":7200,
// "refresh_token":"44_9xsOalBhPa98SRnxbuWWNadc8wW3rQFgbRgsrNIp-wSuC3r4cYzSQ5b9oJAICcennhis5oY6p5cdKnHreEW-B_WPTRDIqzcHC8ZccMHOxUY",
// "openid":"oa9wA06iSpmWAF5YO-WjATExOQvw",
// "scope":"snsapi_login",
// "unionid":"ovnCWw1NSHHr6EkoPiBF3S68ah-k"}
private String access_token;
private Integer expires_in;
private String refresh_token;
private String openid;
private String scope;
private String unionid;
}

2.2.Userinfo返回结果封装

package cn.itsource.pethome.dto;

import lombok.Data;
import java.util.List;

//微信个人信息结果参数封装
@Data
public class WechatUserInfoDto {
private String openid;
private String nickname;
private Integer sex;
private String province;
private String city;
private String country;
private String headimgurl;
private String unionid;
private List<String> privilege;
}

2.3.获取Token和个人信息

/**
* 处理授权码方法
* 1、得到授权码,判断一下state
* 2、根据授权码发请求获取token
* 3、根据Token再发请求获取用户信息
*/
@GetMapping("/callback")
public String callback(String code, String state) throws Exception {
log.info("授权码回调接收参数:code={},state={}", code, state);
if(!StringUtils.hasLength(code)){
throw new Exception("无效的授权码");
}

//拼接获取token的URL,替换URL中的占位符
String newUrl = String.format(wechatProperties.getTokenUrl(), wechatProperties.getAppId(), wechatProperties.getAppSecrect(), code);
log.info("获取token的URL={}", newUrl);

//通过工具类发送请求
String token = HttpUtil.sendPost(newUrl, null);
/**
* 获取token结果={
* "access_token":"54_WYI-XPyh7rgvSMzHZFhKZJMZeeO85psiKzVqnl7Se2fFf31oprhkyYJU6tVSS1lwVBDA7SN96sePNCGFYvJLVYvGQjdh1smbSKhhavbEHGA",
* "expires_in":7200,
* "refresh_token":"54_y3K3QFCbxFs-jNZuVWpA6cknmU3MaoitNDRA2n20Dgrf0btHwiWpJzFQtflkrGOhpEs8nuOirwQ3_Aur8KNhVVN-CclcBAJ2sFN-0esrn9A",
* "openid":"oa9wA00wg-6gaBdnDBnuO3iQZZRw",
* "scope":"snsapi_login",
* "unionid":"ovnCWw38CO3_CZpX2W4tpr3-mT8Y"}
*/
log.info("获取token结果={}", token);

//将返回结果转成对象
WechatTokenDto accessTokenResult = JSONObject.parseObject(token, WechatTokenDto.class);
log.info("获取token结果转对象={}", accessTokenResult.toString());

//根据Token再发请求获取用户信息
String userinfoUrl = wechatProperties.getUserinfoUrl();
//替换URL中的占位符
String userInfoUrl = String.format(userinfoUrl, accessTokenResult.getAccess_token(), accessTokenResult.getOpenid());
//发请求
String userInfoString = HttpUtil.sendPost(userInfoUrl, null);
//将结果数据JSON串转对象
WechatUserInfoDto wechatUserInfoDto = JSONObject.parseObject(userInfoString, WechatUserInfoDto.class);
log.info("获取微信用户信息={}", wechatUserInfoDto.toString());

/**
* 下面是具体的业务实现代码
* 1、先判断t_wxuser表中是否存在openid,如果不存在,直接跳 绑定页面
* 2、再判断user_id是否存在,不存在,直接跳 绑定页面
* 3、判断t_user表是否有手机号,没有,跳绑定页面
* 4、上述几个判断,均符合要求,则直接跳首页(表示该微信用户以前登录过了)
*/
//1、先判断t_wxuser表中是否存在openid,如果不存在,直接跳 绑定页面
Wxuser queryUser = new Wxuser();
queryUser.setOpenid(wechatUserInfoDto.getOpenid());
Wxuser wxuser = wxuserMapper.selectOne(queryUser);
userInfoString = StrUtils.cleanBlank(userInfoString);//不去除空格的话,在地址栏中会显示%20的乱码
if(wxuser == null){
//跳转到绑定页面
return "redirect:http://localhost:6001/binder.html?openid=" + userInfoString;
}

//2、再判断user_id是否存在,不存在,直接跳 绑定页面
if(wxuser.getUserId() == null){
//跳转到绑定页面
return "redirect:http://localhost:6001/binder.html?openid=" + userInfoString;
}

//3、判断t_user表是否有手机号,没有,跳绑定页面
User user = userMapper.selectUserByUserId(wxuser.getUserId());
if(user == null || !StringUtils.hasLength(user.getPhone())){
//跳转到绑定页面
return "redirect:http://localhost:6001/binder.html?openid=" + userInfoString;
}

//根据用户ID查询出用户信息返回token给前端
User user1 = userMapper.selectUserByUserId(wxuser.getUserId());
Logininfo logininfo = getMap(user1);

//跳转到首页
String redirectUrl = "redirect:http://localhost:6001/index.html?username=%s&token=%s";
redirectUrl = String.format(redirectUrl, user1.getUsername(), logininfo.getToken());
redirectUrl = StrUtils.cleanBlank(redirectUrl);
return redirectUrl;
}

2.4.增加拦截器放行

对 "/wechart/callback" 路径放行

2.5.访问测试

控制台打印效果

2022-02-26 11:28:43.515  INFO 4192 --- [p-nio-80-exec-2] c.i.pethome.controller.WechatController  : 授权码回调接收参数:code=071AdiGa1YNBIC0tGkFa1YZGW02AdiGq,state=8e54180a613a4416a8a3d8cbd2b55e0b
2022-02-26 11:28:43.515 INFO 4192 --- [p-nio-80-exec-2] c.i.pethome.controller.WechatController : 获取token的URL=https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxd853562a0548a7d0&secret=4a5d5615f93f24bdba2ba8534642dbb6&code=071AdiGa1YNBIC0tGkFa1YZGW02AdiGq&grant_type=authorization_code
2022-02-26 11:28:44.072 INFO 4192 --- [p-nio-80-exec-2] cn.itsource.pethome.util.HttpUtil : HTTP工具发送网络请求的结果={"access_token":"54_1szScIel3rlyRUfoEs5EBXA6s3siULKYcM8noPwdUdpphr9ANoXChYaXQdEr5ygXkKIzdMktt97sRFv9iWDCJkCzMOraX8ZVkKNocr4z8wY","expires_in":7200,"refresh_token":"54_SOfcBhWn0iniPl_Y1RtKK5I29srgsSQDShbaIaOj7RvWYpr8-TBl2Gixxugc7XSmJmnF3Tor2orfKiCkfGYtB_wiqdyNbvj59ap6uqK8vkg","openid":"oa9wA00wg-6gaBdnDBnuO3iQZZRw","scope":"snsapi_login","unionid":"ovnCWw38CO3_CZpX2W4tpr3-mT8Y"}
2022-02-26 11:28:44.072 INFO 4192 --- [p-nio-80-exec-2] c.i.pethome.controller.WechatController : 获取token结果={"access_token":"54_1szScIel3rlyRUfoEs5EBXA6s3siULKYcM8noPwdUdpphr9ANoXChYaXQdEr5ygXkKIzdMktt97sRFv9iWDCJkCzMOraX8ZVkKNocr4z8wY","expires_in":7200,"refresh_token":"54_SOfcBhWn0iniPl_Y1RtKK5I29srgsSQDShbaIaOj7RvWYpr8-TBl2Gixxugc7XSmJmnF3Tor2orfKiCkfGYtB_wiqdyNbvj59ap6uqK8vkg","openid":"oa9wA00wg-6gaBdnDBnuO3iQZZRw","scope":"snsapi_login","unionid":"ovnCWw38CO3_CZpX2W4tpr3-mT8Y"}
2022-02-26 11:28:44.106 INFO 4192 --- [p-nio-80-exec-2] c.i.pethome.controller.WechatController : 获取token结果转对象=WechatTokenDto(access_token=54_1szScIel3rlyRUfoEs5EBXA6s3siULKYcM8noPwdUdpphr9ANoXChYaXQdEr5ygXkKIzdMktt97sRFv9iWDCJkCzMOraX8ZVkKNocr4z8wY, expires_in=7200, refresh_token=54_SOfcBhWn0iniPl_Y1RtKK5I29srgsSQDShbaIaOj7RvWYpr8-TBl2Gixxugc7XSmJmnF3Tor2orfKiCkfGYtB_wiqdyNbvj59ap6uqK8vkg, openid=oa9wA00wg-6gaBdnDBnuO3iQZZRw, scope=snsapi_login, unionid=ovnCWw38CO3_CZpX2W4tpr3-mT8Y)
2022-02-26 11:28:44.381 INFO 4192 --- [p-nio-80-exec-2] cn.itsource.pethome.util.HttpUtil : HTTP工具发送网络请求的结果={"openid":"oa9wA00wg-6gaBdnDBnuO3iQZZRw","nickname":"吴大胖","sex":0,"language":"","city":"","province":"","country":"","headimgurl":"https:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/Q0j4TwGTfTLuIDWJ49dZUSTCoIibNf13pDQpzSHjH0sa095PLoTicyu20SuSjN8ODSmYNqa05TTaupRTbZz6lkibQ\/132","privilege":[],"unionid":"ovnCWw38CO3_CZpX2W4tpr3-mT8Y"}
2022-02-26 11:28:44.384 INFO 4192 --- [p-nio-80-exec-2] c.i.pethome.controller.WechatController : 获取微信用户信息=WechatUserInfoDto(openid=oa9wA00wg-6gaBdnDBnuO3iQZZRw, nickname=吴大胖, sex=0, province=, city=, country=, headimgurl=https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLuIDWJ49dZUSTCoIibNf13pDQpzSHjH0sa095PLoTicyu20SuSjN8ODSmYNqa05TTaupRTbZz6lkibQ/132, unionid=ovnCWw38CO3_CZpX2W4tpr3-mT8Y, privilege=[])

3. 平台微信登录业务实现(重点掌握)

3.1微信用户表字段分析

我们需要单独一张表来维护平台账号和微信账号的关系 , 当用户扫码后根据OpenId去查询关系表就知道是否已经绑定:

image-20211111163123888

3.2 业务流程

image-20211111161439072

3.3 获取微信用户信息后,按照上面的业务流程图进行代码的逐步实现

1. 先判断t_wxuser表中是否存在openid,如果不存在,直接跳 绑定页面

2. 再判断user_id是否存在,不存在,直接跳绑定页面

3. 再判断t_user表是否能查到用户,不能,直接跳绑定页面

4. 判断t_user表是否有手机号,不能,跳绑定

5.如果上述几个判断,均符合要求,则直接跳首页(表示该微信用户以前登录过了)

3.4 代码实现

package cn.itsource.pethome.controller;

import cn.itsource.pethome.domain.Logininfo;
import cn.itsource.pethome.domain.User;
import cn.itsource.pethome.domain.Wxuser;
import cn.itsource.pethome.dto.*;
import cn.itsource.pethome.dto.WechatProperties;
import cn.itsource.pethome.mapper.UseMapper;
import cn.itsource.pethome.mapper.WxuserMapper;
import cn.itsource.pethome.result.JSONResult;
import cn.itsource.pethome.service.RedisService;
import cn.itsource.pethome.util.HttpUtil;
import cn.itsource.pethome.util.MD5Utils;
import cn.itsource.pethome.util.StrUtils;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.UUID;

/**
* 处理微信相关业务
*/
@Controller
@RequestMapping("/wechat")
@Slf4j
public class WechatController {

@Autowired
private WechatProperties wechatProperties;

@Autowired
private WxuserMapper wxuserMapper;

@Autowired
private UseMapper userMapper;

@Autowired
private RedisService redisService;

/**
* 发起一个微信登录的申请,其实就是获取授权码的申请
* 1、获取到要发送的请求URL
* 2、替换URL中的占位符
* 3、重定向到这个URL去获取授权码code
*/
@GetMapping("/apply")
public String apply(){
//https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect
//生成一个唯一标识作为state
String state = UUID.randomUUID().toString().replace("-","");
//生成完整的授权码URL
String authorizationCodeUrl = String.format(wechatProperties.getAuthorizationCodeUrl(), wechatProperties.getAppId(), wechatProperties.getRedirectUri(), state);
return "redirect:" + authorizationCodeUrl;
}

/**
* 处理授权码方法
* 1、得到授权码,判断一下state
* 2、根据授权码发请求获取token
* 3、根据Token再发请求获取用户信息
*/
@GetMapping("/callback")
public String callback(String code, String state) throws Exception {
log.info("授权码回调接收参数:code={},state={}", code, state);
if(!StringUtils.hasLength(code)){
throw new Exception("无效的授权码");
}

//拼接获取token的URL,替换URL中的占位符
String newUrl = String.format(wechatProperties.getTokenUrl(), wechatProperties.getAppId(), wechatProperties.getAppSecrect(), code);
log.info("获取token的URL={}", newUrl);

//通过工具类发送请求
String token = HttpUtil.sendPost(newUrl, null);
/**
* 获取token结果={
* "access_token":"54_WYI-XPyh7rgvSMzHZFhKZJMZeeO85psiKzVqnl7Se2fFf31oprhkyYJU6tVSS1lwVBDA7SN96sePNCGFYvJLVYvGQjdh1smbSKhhavbEHGA",
* "expires_in":7200,
* "refresh_token":"54_y3K3QFCbxFs-jNZuVWpA6cknmU3MaoitNDRA2n20Dgrf0btHwiWpJzFQtflkrGOhpEs8nuOirwQ3_Aur8KNhVVN-CclcBAJ2sFN-0esrn9A",
* "openid":"oa9wA00wg-6gaBdnDBnuO3iQZZRw",
* "scope":"snsapi_login",
* "unionid":"ovnCWw38CO3_CZpX2W4tpr3-mT8Y"}
*/
log.info("获取token结果={}", token);

//将返回结果转成对象
WechatTokenDto accessTokenResult = JSONObject.parseObject(token, WechatTokenDto.class);
log.info("获取token结果转对象={}", accessTokenResult.toString());

//根据Token再发请求获取用户信息
String userinfoUrl = wechatProperties.getUserinfoUrl();
//替换URL中的占位符
String userInfoUrl = String.format(userinfoUrl, accessTokenResult.getAccess_token(), accessTokenResult.getOpenid());
//发请求
String userInfoString = HttpUtil.sendPost(userInfoUrl, null);
//将结果数据JSON串转对象
WechatUserInfoDto wechatUserInfoDto = JSONObject.parseObject(userInfoString, WechatUserInfoDto.class);
log.info("获取微信用户信息={}", wechatUserInfoDto.toString());

/**
* 下面是具体的业务实现代码
* 1、先判断t_wxuser表中是否存在openid,如果不存在,直接跳 绑定页面
* 2、再判断user_id是否存在,不存在,直接跳 绑定页面
* 3、判断t_user表是否有手机号,没有,跳绑定页面
* 4、上述几个判断,均符合要求,则直接跳首页(表示该微信用户以前登录过了)
*/
//1、先判断t_wxuser表中是否存在openid,如果不存在,直接跳 绑定页面
Wxuser wxuser = wxuserMapper.findByOpenId(wechatUserInfoDto.getOpenid());
userInfoString = StrUtils.cleanBlank(userInfoString);//不去除空格的话,在地址栏中会显示%20的乱码
if(wxuser == null){
//跳转到绑定页面
return "redirect:http://localhost:6001/binder.html?openid=" + userInfoString;
}

//2、再判断user_id是否存在,不存在,直接跳 绑定页面
if(wxuser.getUserId() == null){
//跳转到绑定页面
return "redirect:http://localhost:6001/binder.html?openid=" + userInfoString;
}

//3、判断t_user表是否有手机号,没有,跳绑定页面
User user = userMapper.selectUserByUserId(wxuser.getUserId());
if(user == null || !StringUtils.hasLength(user.getPhone())){
//跳转到绑定页面
return "redirect:http://localhost:6001/binder.html?openid=" + userInfoString;
}

//根据用户ID查询出用户信息返回token给前端
User user1 = userMapper.selectUserByUserId(wxuser.getUserId());
Logininfo logininfo = getMap(user1);

//跳转到首页
String redirectUrl = "redirect:http://localhost:6001/index.html?username=%s&token=%s";
redirectUrl = String.format(redirectUrl, user1.getUsername(), logininfo.getToken());
redirectUrl = StrUtils.cleanBlank(redirectUrl);
return redirectUrl;
}

/**
* 处理微信与手机号的绑定请求
*/
@PostMapping("/binder")
@ResponseBody
public JSONResult binder(@RequestBody BinderDto dto) throws UnsupportedEncodingException {
log.info("BinderDto==============={}", dto.toString());
String openidTemp = dto.getOpenid();
WechatUserInfoDto wechatUserInfoDto = JSONObject.parseObject(openidTemp, WechatUserInfoDto.class);

//1、校验短信验证码是否正确
String code = redisService.getKeyObjectValue(dto.getPhone(), String.class);
if (!org.apache.commons.lang3.StringUtils.equals(code, dto.getCode())) {
return JSONResult.error("短信验证码错误!");
}

//2、根据openid查t_wxuser的信息
Wxuser wxuser = wxuserMapper.findByOpenId(wechatUserInfoDto.getOpenid());
//2.1、根据查到的t_wxuser信息中的user_id进行判断
if (wxuser != null && wxuser.getUserId() != null) {
User user = userMapper.selectUserByUserId(wxuser.getUserId());
//登录成功
return JSONResult.success(getMap(user));
}
else {
//此时说明是第一次用微信登录
User user = new User();
user.setPhone(dto.getPhone());
//在保存密码时,可以对密码进行加密处理 md5(不可逆)
//salt盐:加盐的目的,就是为了让md5的破解变的更难
user.setSalt("");
user.setPassword(MD5Utils.encrypByMd5("123456"));
user.setCreatetime(new Date());
user.setState(0);
//调用注册接口,用户信息入库
userMapper.registerUser(user);

Wxuser wUser = new Wxuser();
wUser.setUserId(user.getId());
wUser.setOpenid(wechatUserInfoDto.getOpenid());
String nickName = wechatUserInfoDto.getNickname();
nickName = new String(nickName.getBytes("ISO-8859-1"), "UTF-8");
log.info("nickName={}", wechatUserInfoDto.getNickname());
wUser.setNickname(nickName);
wUser.setAddress(wechatUserInfoDto.getCountry() + "-" + wechatUserInfoDto.getCity());
wUser.setSex(wechatUserInfoDto.getSex());
wUser.setHeadimgurl(wechatUserInfoDto.getHeadimgurl());
wUser.setUnionid(wechatUserInfoDto.getUnionid());
wxuserMapper.saveWxUser(wUser);
return JSONResult.success(getMap(user));
}
}

//获取返回结果集
public Logininfo getMap(User user){
//创建token
String token = org.apache.commons.lang3.StringUtils.replace(UUID.randomUUID().toString(), "-", "");
//token保存到Redis
Logininfo logininfo = new Logininfo();
logininfo.setId(user.getId());
logininfo.setUsername(user.getUsername());
logininfo.setPhome(user.getPhone());
logininfo.setToken(token);
redisService.setStringKeyAndValue(token, logininfo, 30);
return logininfo;
}

//微信登录成功后,绑定操作时发送验证码
@GetMapping("/sendBindCode/{phone}")
public JSONResult sendBindCode(@PathVariable("phone") String phone){
String code = StrUtils.getRandomString(6);
log.info("发送短信验证码={}", code);
redisService.setStringKeyAndValue(phone, code);
return JSONResult.success();
}
}

UserMapper.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">
<!--
namespace: 名称空间 : 指定具体的映射文件
-->
<mapper namespace="cn.itsource.pethome.mapper.UseMapper">

<select id="selectUserByPhone" resultType="cn.itsource.pethome.domain.User">
SELECT *
FROM t_user
WHERE phone = #{phone}
</select>

<select id="selectUserByUserId" resultType="cn.itsource.pethome.domain.User">
SELECT *
FROM t_user
WHERE id = #{userId}
</select>

<insert id="registerUser" parameterType="cn.itsource.pethome.domain.User" useGeneratedKeys="true" keyProperty="id">
insert into t_user(phone, salt, password, state, createtime)
values(#{phone}, #{salt}, #{password}, #{state}, #{createtime})
</insert>

<update id="updateOfPhone" parameterType="cn.itsource.pethome.domain.User">
update t_user set phone = #{phone} where id = #{id}
</update>

</mapper>

WxuserMapper.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">
<!--
namespace: 名称空间 : 指定具体的映射文件
-->
<mapper namespace="cn.itsource.pethome.mapper.WxuserMapper">

<select id="findByOpenId" resultType="cn.itsource.pethome.domain.Wxuser">
SELECT *
FROM t_wxuser
WHERE openid = #{openid}
</select>

<update id="updateOfUserId" parameterType="cn.itsource.pethome.domain.Wxuser">
update t_wxuser set user_id = #{userId} where id = #{id}
</update>

<insert id="saveWxUser" parameterType="cn.itsource.pethome.domain.Wxuser" useGeneratedKeys="true" keyProperty="id">
insert into t_wxuser(openid, nickname, sex, address, headimgurl, unionid, user_id)
values(#{openid}, #{nickname}, #{sex}, #{address}, #{headimgurl}, #{unionid}, #{userId})
</insert>

</mapper>

WechatTokenDto

package cn.itsource.pethome.dto;

import lombok.Data;

//微信获取Token返回结果
@Data
public class WechatTokenDto {
//{"access_token":"44_QfGkFOzewXRoLFGjydhphpo0O6n3sLnRKQlUyUFf0x-LIxMLcSIPiLkeYEKWyt_rx93-XYUuRu4QGwUFstFBO2gfUTOS1Q4D7g2IDM30Bc8",
// "expires_in":7200,
// "refresh_token":"44_9xsOalBhPa98SRnxbuWWNadc8wW3rQFgbRgsrNIp-wSuC3r4cYzSQ5b9oJAICcennhis5oY6p5cdKnHreEW-B_WPTRDIqzcHC8ZccMHOxUY",
// "openid":"oa9wA06iSpmWAF5YO-WjATExOQvw",
// "scope":"snsapi_login",
// "unionid":"ovnCWw1NSHHr6EkoPiBF3S68ah-k"}
private String access_token;
private Integer expires_in;
private String refresh_token;
private String openid;
private String scope;
private String unionid;
}

StrUtils工具类新增方法

//清空字符串的空格
public static String cleanBlank(String str){
return StrUtil.cleanBlank(str);
}

3.5 代码测试前准备

  • 三方登录的测试前,需要先删除t_wxuser和t_user表中原有的测试数据,以防止因为数据问题导致登录失败



文章不错,扫码支持一下吧~
上一篇 下一篇
评论
来首音乐
光阴似箭
今日已经过去小时
这周已经过去
本月已经过去
今年已经过去个月