这是微信的开发文档:微信登录功能 / 网站应用微信登录开发指南 (qq.com)
微信扫码登录流程
1,用户访问网页,网页向微信开放平台发送请求,获取一个access_token和一个ticket。
2,网页根据ticket向微信开放平台请求二维码的地址,并将二维码显示在网页上。
3,用户使用手机微信扫描二维码,微信开放平台会向网页发送一个事件推送,包含一个授权码。
4,网页根据授权码向微信开放平台请求用户信息,并完成登录逻辑。
具体实现
那我们逻辑明白要怎么写代码呢?
前提:在微信开发者平台完成资历验证(要200块)
首先,编写一个生成二维码的地址并重定向返回给前端的,用来让用户扫码登录。(默认Springboot,SpringMVC都有所了解)。
里面ConstantPropertiesUtil是初始化属性类,用于获取properties文件的数据:
package com.atguigu.educenter.util;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
//@PropertySource("classpath:application.properties")//解释的文件
public class ConstantPropertiesUtil implements InitializingBean {
@Value("${wx.open.app_id}")//微信开发者ID
private String appId;
@Value("${wx.open.app_secret}")//秘钥
private String appSecret;
@Value("${wx.open.redirect_url}")//重定向地址
private String redirectUrl;
public static String WX_OPEN_APP_ID;
public static String WX_OPEN_APP_SECRET;
public static String WX_OPEN_REDIRECT_URL;
@Override
public void afterPropertiesSet() throws Exception {
WX_OPEN_APP_ID = appId;
WX_OPEN_APP_SECRET = appSecret;
WX_OPEN_REDIRECT_URL = redirectUrl;
}
}
注:为什么这里使用InitializingBean获取properties数据,而不是使用@ConfigurationProperties()的原因是:
使用InitializingBean可以在bean的属性被设置后,做一些自定义的操作,比如把非静态变量的值赋值给静态变量。(主要是这个)
使用InitializingBean可以避免使用反射调用init-method指定的方法,提高效率。
使用InitializingBean可以实现解耦,把一些配置信息写在配置文件中,然后通过@Value注解来获取。
下面的微信开放平台授权baseUrl有六个参数:
appid 应用唯一标识
redirect_uri 请使用urlEncode对链接进行处理
response_type 填code
scope 应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login
state(不必须) 用于保持请求和回调的状态,授权请求后原样带回给第三方。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议第三方带上该参数,可设置为简单的随机数加session进行校验
lang(不必须) 界面语言,支持cn(中文简体)与en(英文),默认为cn
用户允许授权后,将会重定向到redirect_uri的网址上,并且带上code和state参数。
@GetMapping("login")
public String getWxCode(HttpSession session) {
// 微信开放平台授权baseUrl
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
// 回调地址
String redirectUrl = ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL; //获取业务服务器重定向地址
try {
redirectUrl = URLEncoder.encode(redirectUrl, "UTF-8"); //url编码
} catch (UnsupportedEncodingException e) {
throw new GuliException(20001, e.getMessage());
}
String state = "xxxx";//这里填写你在ngrok的前置域名
System.out.println("state = " + state);
//生成qrcodeUrl
String qrcodeUrl = String.format(
baseUrl,
ConstantPropertiesUtil.WX_OPEN_APP_ID,
redirectUrl,
state);
return "redirect:" + qrcodeUrl;//重定向显示二维码
}
然后接收微信的回调信息,获取用户的信息,并生成token返回给前端的
HttpClientUtils.get()方法:HttpClientUtils是自定义的一个模拟浏览器请求的工具类,get方法用来发送一个HTTP GET请求的方法使用了以下两个包:
org.apache.httpcomponents:httpclient: 这是一个用于发送和接收HTTP请求和响应的依赖,它是Apache HttpComponents项目的一部分,提供了HttpClient, HttpGet, HttpResponse等类,可以帮助你实现HTTP客户端的功能。
org.apache.commons:commons-io: 这是一个用于处理输入输出操作的依赖,它是Apache Commons项目的一部分,提供了IOUtils, FileUtils, FilenameUtils等工具类,可以帮助你简化输入输出相关的代码。
<!-- Apache HttpClient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Apache Commons IO -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
</dependency>
它的参数和返回值如下:
url: 一个字符串,表示要请求的网址。
charset: 一个字符串,表示响应的字符编码,例如 "UTF-8"。
connTimeout: 一个整数,表示连接超时的时间,单位是毫秒,如果为null,则使用默认值
readTimeout: 一个整数,表示读取超时的时间,单位是毫秒,如果为null,则使用默认值。
result返回值: 一个字符串,表示响应的内容。
/**
* 发送一个 GET 请求
*
* @param url
* @param charset
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return
* @throws ConnectTimeoutException 建立链接超时
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String get(String url, String charset, Integer connTimeout,Integer readTimeout)
throws ConnectTimeoutException,SocketTimeoutException, Exception {
HttpClient client = null;
HttpGet get = new HttpGet(url);
String result = "";
try {
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
get.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(get);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(get);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
get.releaseConnection();
if (url.startsWith("https") && client != null && client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
下面的baseAccessTokenUrl对应的参数:
appid 唯一标识,在微信开放平台提交应用审核通过后获得
secret 密钥AppSecret,在微信开放平台提交应用审核通过后获得
code 填写上面获取的code参数
grant_type 填authorization_code
请求完后Url返回:
access_token 接口调用凭证
expires_in access_token接口调用凭证超时时间,单位(秒)
refresh_token 用户刷新access_token
openid 授权用户唯一标识
scope 用户授权的作用域,使用逗号(,)分隔
unionid 当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段。
{
"access_token":"ACCESS_TOKEN",
"expires_in":7200,
"refresh_token":"REFRESH_TOKEN",
"openid":"OPENID",
"scope":"SCOPE",
"unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
下面的baseUserInfoUrl对应的参数:
access_token 调用凭证
openid 普通用户的标识,对当前开发者账号唯一
lang(不必须) 国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语,默认为en
返回值:
openid 普通用户的标识,对当前开发者账号唯一
nickname 普通用户昵称
sex 普通用户性别,1为男性,2为女性
province 普通用户个人资料填写的省份
city 普通用户个人资料填写的城市
country 国家,如中国为CN
headimgurl 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
privilege 用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
unionid 用户统一标识。针对一个微信开放平台账号下的应用,同一用户的unionid是唯一的。
{
"openid":"OPENID",
"nickname":"NICKNAME",
"sex":1,
"province":"PROVINCE",
"city":"CITY",
"country":"COUNTRY",
"headimgurl": "https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0",
"privilege":[
"PRIVILEGE1",
"PRIVILEGE2"
],
"unionid": " o6_bmasdasdsad6_2sgVt7hMZOPfL"
}
callback具体代码:
callback():这个方法是用来接收微信的回调信息,获取用户的信息,并生成token返回给前端的。下面方法的详细解释:
它有三个参数,分别是code、state和session。它首先拼接了一个获取access_token的URL,其中包含了微信的appid、密钥和code等参数。然后它用HttpClientUtils.get()方法向这个URL发送请求,获取到一个包含access_token和openid的JSON字符串。
接着它用Gson.fromJson()方法将这个字符串转换成一个HashMap对象,并从中取出access_token和openid的值。然后它根据openid查询数据库中是否已经存在该用户,如果不存在,则再拼接一个获取用户信息的URL,其中包含了access_token和openid等参数。再次用HttpClientUtils.get()方法向这个URL发送请求,获取到一个包含用户昵称和头像等信息的JSON字符串。
再用Gson.fromJson()方法将这个字符串转换成一个HashMap对象,并从中取出用户昵称和头像的值。然后创建一个UcenterMember对象,并将openid、昵称和头像等属性赋值给它,并保存到数据库中。如果已经存在该用户,则直接从数据库中获取该用户的信息。最后用JwtUtils.getJwtToken()方法根据用户的id和昵称生成一个token,并用redirect()方法将其返回给前端。前端收到token后,就可以实现用户的登录状态。
@GetMapping("callback")
public String callback(String code, String state, HttpSession session) {
//向认证服务器发送请求换取access_token
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
String accessTokenUrl = String.format(baseAccessTokenUrl,
ConstantPropertiesUtil.WX_OPEN_APP_ID,
ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
code);
try {
String accessTokenInfo = HttpClientUtils.get(accessTokenUrl);
System.out.println("accessTokenInfo:"+accessTokenInfo);
//使用gson将accessTokenInfo的JSON字符串转换map
Gson gson = new Gson();
HashMap mapAccessToken = gson.fromJson(accessTokenInfo, HashMap.class);
String access_token = (String) mapAccessToken.get("access_token");
String openid = (String) mapAccessToken.get("openid");
//根据openid查询是否已经添加用户
UcenterMember member = memberService.getOpenIdMember(openid);
if (member==null){
//访问微信的资源服务器,获取用户信息
String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid);
String userInfo = HttpClientUtils.get(userInfoUrl);
System.out.println("userInfo"+userInfo);
HashMap mapUserInfo = gson.fromJson(userInfo, HashMap.class);
String nickname = (String) mapUserInfo.get("nickname");
String headimgurl = (String) mapUserInfo.get("headimgurl");
member = new UcenterMember();
member.setOpenid(openid);
member.setNickname(nickname);
member.setAvatar(headimgurl);
memberService.save(member);
}
//
String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname());
//得到授权临时票据code
System.out.println("code = " + code);
System.out.println("state = " + state);
return "redirect:http://localhost:8080?token="+jwtToken;
} catch (Exception e) {
e.printStackTrace();
throw new GuliException(20001,"登录异常");
}
}