基于SpringBoot实现简单微信扫码登录

对于微信扫码登录,首先我们需要有一个公众号平台账号(可以是测试账户,也可以是已经注册绑定营业的账户).

前言

直达链接(当前样例仅在测试号环境下处理):

还需要准备一个内网穿透的工具(任选其一即可)

在此仅列出俩个, 需要获取更多信息 链接

内网穿透

一句话来说就是,让外网能访问你的内网;把自己的内网(主机)当成服务器,让外网能访问

所以我们需要通过工具配置(当然你也可以不用工具,我在此使用工具的方式实现)

Ngrok

首先在Ngrok注册账号, 点击上面链接下载Ngrok

打开ngrok在弹出控制台输入

1
ngrok http 80

例如我需要映射本地的80端口, 输入完成控制台会有一个回调显示, 复制映射地址即可

花生壳

花生壳配置如图

配置

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.5</version>
</dependency>
<!-- 生成二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.3.3</version>
</dependency>
<!-- wx -->
<!-- 微信授权登录-->
<!-- https://mvnrepository.com/artifact/com.github.binarywang/weixin-java-mp -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>3.4.0</version>
</dependency>

yaml

1
2
3
wechat:
mpAppId: wxfc5465******** # your appid
mpAppSecret: 49e5997***** # your appSecret

测试号管理配置

我们需要配置的是 接口配置信息

URL和Token

URL: 验证的方法

接下来直接列出代码

controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Slf4j
@CrossOrigin
@RestController
public class WeixinCheckController {
/**
* 微信公众号签名认证接口
*/
@RequestMapping(value = "/wxCheck",
method = {RequestMethod.POST, RequestMethod.GET},
produces = "text/html; charset=utf-8") // 必须为这个返回结果格式
public String WxToken(@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr
) {
log.info("微信Token认证===>signature={},timestamp={},echostr={}", signature, timestamp, echostr);
// 通过检验signature对请求进行校验,若校验成功则原样返回echostr,表示接入成功,否则接入失败
if (signature != null && WeixinCheckoutUtil.checkSignature(signature, timestamp, nonce)) {
return echostr;
}
return null;
}
}

WeixinCheckoutUtil:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
* @ClassName WeixinCheckoutUtil
* @Author Calyee
* @Version 1.0
*/
public class WeixinCheckoutUtil {

// 与接口配置信息中的Token要一致
private static String token = "calyee";

/**
* 验证签名
*
* @param signature
* @param timestamp
* @param nonce
* @return
*/
public static boolean checkSignature(String signature, String timestamp, String nonce) {
String[] arr = new String[]{token, timestamp, nonce};
// 将token、timestamp、nonce三个参数进行字典序排序
// Arrays.sort(arr);
sort(arr);
StringBuilder content = new StringBuilder();
for (int i = 0; i < arr.length; i++) {
content.append(arr[i]);
}
MessageDigest md = null;
String tmpStr = null;

try {
md = MessageDigest.getInstance("SHA-1");
// 将三个参数字符串拼接成一个字符串进行sha1加密
byte[] digest = md.digest(content.toString().getBytes());
tmpStr = byteToStr(digest);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
content = null;
// 将sha1加密后的字符串可与signature对比,标识该请求来源于微信

return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
}

/**
* 将字节数组转换为十六进制字符串
*
* @param byteArray
* @return
*/
private static String byteToStr(byte[] byteArray) {
String strDigest = "";
for (int i = 0; i < byteArray.length; i++) {
strDigest += byteToHexStr(byteArray[i]);
}
return strDigest;
}

/**
* 将字节转换为十六进制字符串
*
* @param mByte
* @return
*/
private static String byteToHexStr(byte mByte) {
char[] Digit = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] tempArr = new char[2];
tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
tempArr[1] = Digit[mByte & 0X0F];
String s = new String(tempArr);
return s;
}

public static void sort(String a[]) {
for (int i = 0; i < a.length - 1; i++) {
for (int j = i + 1; j < a.length; j++) {
if (a[j].compareTo(a[i]) < 0) {
String temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
}
}

接下来使用内外穿透的地址访问此接口吧, 注意是在公众号配置信息处

开发

微信开发者文档将授权流程分为了4步: (微信网页开发)

  1. 引导用户进入授权页面同意授权,获取code
  2. 通过code换取网页授权access_token(与基础支持中的access_token不同)
  3. 如果需要,开发者可以刷新网页授权access_token,避免过期
  4. 通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)

第一步: 用户同意授权获取code

在确保微信公众账号拥有授权作用域(scope参数)的权限的前提下(已认证服务号,默认拥有scope参数中的snsapi_base和snsapi_userinfo 权限),引导关注者打开如下页面:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @ClassName WeixinLoginController
* @Description 微信登录
* @Author Calyee
* @DATE 2023/12/17 017 18:32
* @Version 1.0
*/
@RestController
@Slf4j
public class WeixinLoginController {
private static final String baseUrl = "https://2886**********/";

@Value("${wechat.mpAppId}")
private static final String appid = "wxfc546**********";

/**
* 生成二维码
*
* @param response
* @throws Exception
*/
@GetMapping("/wxLogin")
public void login(HttpServletResponse response) throws Exception {
// redirect_uri是回调的地址注意要转成UrLEncode格式
String redirectUrl = URLEncoder.encode(baseUrl + "wxCallBack", "UTF-8");
// 构造二维码链接地址
String url = "https://open.weixin.qq.com/connect/oauth2/authorize?" +
"appid=" + appid +
"&redirect_uri=" + redirectUrl +
"&response_type=code" +
"&scope=snsapi_userinfo" + // 授权作用域
"&state=STATE#wechat_redirect"; // #wechat_redirect必须传
// <link> https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
// https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
// 生成二维码的,扫描后跳转上面的地址
response.setContentType("image/png");
// 生成二维码
QrCodeUtil.generate(url, 300, 300, "jpg", response.getOutputStream());
}
}

授权后,页面将跳转至 redirect_uri/?code=CODE&state=STATE

第二步:通过code换取网页授权access_token

首先请注意,这里通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同。公众号可通过下述接口来获取网页授权access_token。如果网页授权的作用域为snsapi_base,则本步骤中获取到网页授权access_token的同时,也获取到了openid,snsapi_base式的网页授权流程即到此为止。

尤其注意:由于公众号的secret和获取到的access_token安全级别都非常高,必须只保存在服务器,不允许传给客户端。后续刷新access_token、通过access_token获取用户信息等步骤,也必须从服务器发起。

在当前controller添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Resource
private WxMpService wxMpService;

/**
* 用户点同意回调
* 通过code换取网页的授权access_token
*
* @param code 页面拿到的code
* @param request
* @param response
* @throws Exception
*/
@RequestMapping("/wxCallBack")
public String getWxCallBack(String code,
String state,
HttpSession session,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
log.info("code={}", code);
WxMpOAuth2AccessToken wxMpOAuth2AccessToken;
try {
wxMpOAuth2AccessToken = wxMpService.oauth2getAccessToken(code);
} catch (WxErrorException e) {
log.info("【微信网页授权发生错误】");
throw new Exception(e.getError().getErrorMsg());
}
// wxMpOAuth2AccessToken: 可以获取用户授权的信息
String openId = wxMpOAuth2AccessToken.getOpenId();
log.info("【微信网页授权】openId={},accessToken={}", openId, wxMpOAuth2AccessToken.getAccessToken());
// 缓存一下
session.setAttribute("openid", openId);
return "redirect:" + state + "?openid=" + openId;
}

第三步:刷新access_token(如果需要)

见文档, 此步骤文档描述道通过refresh_token请求以下链接获取access_token

https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN

第四步:拉取用户信息(需scope为 snsapi_userinfo)

见文档, 如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了。