微信扫码登录
1 准备工作
1.1微信开放平台url:微信官方平台
1.2 注册开发者资质账号
- 注册
- 邮箱激活
- 完善开发者资料
- 开发者资质认证(准备营业执照,1-2个工作日审批、300元)
- 创建网站应用(提交审核,7个工作日审批)
注意事项:
应用通过审核后,会得到AppID和 AppSecret
因网上教程绑定了微信扫描之后跳转的域名地址,需要在测试阶段扫描跳转到自己的服务器中,需要进行如下配置:
- 修改本机host地址指向域名
127.0.0.1 note.java.Xxx.cn
- 修改tomcat的端口,设置为80
1.3 微信登录流程分析
官方文档:登录流程图
第一步:
请求CODE(生成授权URL)
第二步:
通过code获取access_token(开发回调URL)
2 微信登录Demo
2.1 创建springboot工程,导入pom.xml坐标
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--web启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--微信支付sdk-->
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.10</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
2.2 application.yml配置文件
#微信认证资质
wx:
open:
# 微信开放平台 appid
app_id: Xxxx...
# 微信开放平台 appsecret
app_secret: Xxx....
# 微信开放平台 重定向url(note.java.Xxx.cn需要在微信开放平台配置)
redirect_url: http://note.java.Xxx.cn/api/ucenter/wx/callback
2.3 创建常量类ConstantPropertiesUtil
/*
常量类,获取资质信息
InitializingBean接口为bean提供了初始化方法的方式,
它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。
*/
@Component
public class ConstantPropertiesUtil implements InitializingBean{
@Value("${wx.open.app_id}")
private String appId;//应用id
@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;
}
}
2.4 创建controller
@Controller
@RequestMapping("/api/ucenter/wx")//注意这个请求映射需要和url回调中一致,实际开发不一定
public class WxController {
//获取二维码
@GetMapping("/login")
public String generQRconnect(){
//1.获取请求code
//---------------------------------------------
//1.1拼接请求地址
/**
*说明: %s 表示占位符
*/
String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" +
"?appid=%s" +
"&redirect_uri=%s" +
"&response_type=code" +
"&scope=snsapi_login" +
"&state=%s" +
"#wechat_redirect";
//1.2 设置状态,前置域名
String state="chengfei";//为了让大家能够使用微信回调跳转服务器,这里填写你在ngrok的前置域名
//1.3回调地址要进行url编码
String redirectUrl=ConstantPropertiesUtil.WX_OPEN_REDIRECT_URL;
try {
redirectUrl = URLEncoder.encode(redirectUrl, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//1.4对请求地址进行赋值操作
/**
*param1:表示处理的字符串
*后面参数替换被处理字符串中的占位符
*/
baseUrl = String.format(baseUrl,
ConstantPropertiesUtil.WX_OPEN_APP_ID,
redirectUrl,
state
);
//1.5重定向到展示二维码地址
return "redirect:"+baseUrl;
}
}
2.5 启动测试
http://note.java.Xxx.cn/api/ucenter/wx/login
访问授权url后会得到一个微信登录二维码:
url地址栏信息:
https://open.weixin.qq.com/connect/qrconnect?appid=Xxx...&
redirect_uri=http%3A%2F%2Fnote.java.Xxx.cn%2Fapi%2Fucenter%2Fwx%2Fcallback&
response_type=code&
scope=snsapi_login&
state=chengfei
#wechat_redirect
用户扫描二维码得到:
2.6 获取开发回调URL
1 获取Code和state值
用户授权同意之后,浏览器中将会重定向到redirect_uri的网址上,并且带上code和state参数.
http://note.java.Xxx.cn/api/ucenter/wx/callback?code=091CUTGa16izIz0S1dJa1MXPmK2CUTGd&state=chengfei
2 通过code获取access_token
1加入依赖(已加可省略)
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--gson-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
2 引入工具类
package com.cf.common;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Consts;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.config.RequestConfig.Builder;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* 依赖的jar包有:commons-lang-2.6.jar、httpclient-4.3.2.jar、httpcore-4.3.1.jar、commons-io-2.4.jar
* @author zhaoyb
*
*/
public class HttpClientUtils {
public static final int connTimeout=10000;
public static final int readTimeout=10000;
public static final String charset="UTF-8";
private static HttpClient client = null;
static {
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(128);
cm.setDefaultMaxPerRoute(128);
client = HttpClients.custom().setConnectionManager(cm).build();
}
public static String postParameters(String url, String parameterStr) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}
public static String postParameters(String url, String parameterStr,String charset, Integer connTimeout, Integer readTimeout) throws ConnectTimeoutException, SocketTimeoutException, Exception{
return post(url,parameterStr,"application/x-www-form-urlencoded",charset,connTimeout,readTimeout);
}
public static String postParameters(String url, Map<String, String> params) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String postParameters(String url, Map<String, String> params, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
return postForm(url, params, null, connTimeout, readTimeout);
}
public static String get(String url) throws Exception {
return get(url, charset, null, null);
}
public static String get(String url, String charset) throws Exception {
return get(url, charset, connTimeout, readTimeout);
}
/**
* 发送一个 Post 请求, 使用指定的字符集编码.
*
* @param url
* @param body RequestBody
* @param mimeType 例如 application/xml "application/x-www-form-urlencoded" a=1&b=2&c=3
* @param charset 编码
* @param connTimeout 建立链接超时时间,毫秒.
* @param readTimeout 响应超时时间,毫秒.
* @return ResponseBody, 使用指定的字符集编码.
* @throws ConnectTimeoutException 建立链接超时异常
* @throws SocketTimeoutException 响应超时
* @throws Exception
*/
public static String post(String url, String body, String mimeType,String charset, Integer connTimeout, Integer readTimeout)
throws ConnectTimeoutException, SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
String result = "";
try {
if (StringUtils.isNotBlank(body)) {
HttpEntity entity = new StringEntity(body, ContentType.create(mimeType, charset));
post.setEntity(entity);
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
result = IOUtils.toString(res.getEntity().getContent(), charset);
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
return result;
}
/**
* 提交form表单
*
* @param url
* @param params
* @param connTimeout
* @param readTimeout
* @return
* @throws ConnectTimeoutException
* @throws SocketTimeoutException
* @throws Exception
*/
public static String postForm(String url, Map<String, String> params, Map<String, String> headers, Integer connTimeout,Integer readTimeout) throws ConnectTimeoutException,
SocketTimeoutException, Exception {
HttpClient client = null;
HttpPost post = new HttpPost(url);
try {
if (params != null && !params.isEmpty()) {
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
Set<Entry<String, String>> entrySet = params.entrySet();
for (Entry<String, String> entry : entrySet) {
formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
}
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formParams, Consts.UTF_8);
post.setEntity(entity);
}
if (headers != null && !headers.isEmpty()) {
for (Entry<String, String> entry : headers.entrySet()) {
post.addHeader(entry.getKey(), entry.getValue());
}
}
// 设置参数
Builder customReqConf = RequestConfig.custom();
if (connTimeout != null) {
customReqConf.setConnectTimeout(connTimeout);
}
if (readTimeout != null) {
customReqConf.setSocketTimeout(readTimeout);
}
post.setConfig(customReqConf.build());
HttpResponse res = null;
if (url.startsWith("https")) {
// 执行 Https 请求.
client = createSSLInsecureClient();
res = client.execute(post);
} else {
// 执行 Http 请求.
client = HttpClientUtils.client;
res = client.execute(post);
}
return IOUtils.toString(res.getEntity().getContent(), "UTF-8");
} finally {
post.releaseConnection();
if (url.startsWith("https") && client != null
&& client instanceof CloseableHttpClient) {
((CloseableHttpClient) client).close();
}
}
}
/**
* 发送一个 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;
}
/**
* 从 response 里获取 charset
*
* @param ressponse
* @return
*/
@SuppressWarnings("unused")
private static String getCharsetFromResponse(HttpResponse ressponse) {
// Content-Type:text/html; charset=GBK
if (ressponse.getEntity() != null && ressponse.getEntity().getContentType() != null && ressponse.getEntity().getContentType().getValue() != null) {
String contentType = ressponse.getEntity().getContentType().getValue();
if (contentType.contains("charset=")) {
return contentType.substring(contentType.indexOf("charset=") + 8);
}
}
return null;
}
/**
* 创建 SSL连接
* @return
* @throws GeneralSecurityException
*/
private static CloseableHttpClient createSSLInsecureClient() throws GeneralSecurityException {
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain,String authType) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new X509HostnameVerifier() {
@Override
public boolean verify(String arg0, SSLSession arg1) {
return true;
}
@Override
public void verify(String host, SSLSocket ssl)
throws IOException {
}
@Override
public void verify(String host, X509Certificate cert)
throws SSLException {
}
@Override
public void verify(String host, String[] cns,
String[] subjectAlts) throws SSLException {
}
});
return HttpClients.custom().setSSLSocketFactory(sslsf).build();
} catch (GeneralSecurityException e) {
throw e;
}
}
public static void main(String[] args) {
try {
String str= post("https://localhost:443/ssl/test.shtml","name=12&page=34","application/x-www-form-urlencoded", "UTF-8", 10000, 10000);
//String str= get("https://localhost:443/ssl/test.shtml?name=12&page=34","GBK");
/*Map<String,String> map = new HashMap<String,String>();
map.put("name", "111");
map.put("page", "222");
String str= postForm("https://localhost:443/ssl/test.shtml",map,null, 10000, 10000);*/
System.out.println(str);
} catch (ConnectTimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SocketTimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
3 创建回调的controller方法
//获取回调url
@GetMapping("/callback")
public String callback(String code,String state){
//1.打印出code和state
System.out.println("获取的code="+code);//code表示临时票据
System.out.println("获取的state="+state);
//2. 通过code获取access_token
String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//3.对请求地址进行赋值操作
baseAccessTokenUrl = String.format(baseAccessTokenUrl,
ConstantPropertiesUtil.WX_OPEN_APP_ID,
ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
code
);
try {
//4.获取openid和access_token
String result = HttpClientUtils.get(baseAccessTokenUrl);
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
4 启动程序访问测试
- 浏览器访问url:http://note.java.Xxx.cn/api/ucenter/wx/login
- 微信扫码授权确认回调url
- 由于访问的callback方法,自动进入后台访问回调的controller方法
控制台打印result值是一个json对象,包含:
{
"access_token":"Xxx...",
"expires_in":7200,
"refresh_token":"Xxx...",
"openid":"Xxx...",
"scope":"Xxx...",
"unionid":"Xxx..."
}
2.7 通过access_token和openid获取用户信息
1导入依赖
<!--thymeleaf依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2 补充callback方法
//根据code获取access_token ,目的是就是为了获取响应结果
@GetMapping("/callback")
public String callback(String code, String state, ModelMap modelMap){
//1.声明获取access_token 的请求地址
String baseUrl="https://api.weixin.qq.com/sns/oauth2/access_token"+
"?appid=%s"+
"&secret=%s"+
"&code=%s"+
"&grant_type=authorization_code";
//2.对请求地址进行赋值操作
baseUrl = String.format(baseUrl,
ConstantPropertiesUtil.WX_OPEN_APP_ID,
ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
code);
/*
3.发出请求来请求baseUrl,返回值是响应json数据,包含了access_token
{
access_token: "Xxx",
expires_in: 7200,
refresh_token: "Xxx",
openid: "Xxx",
scope: "Xxx",
unionid: "Xxx"
}
*/
try {
String result = HttpClientUtils.get(baseUrl);
//4.根据access_token和openid获取用户个人信息
//4.1 声明获取用户信息的api接口
String userInfoUrl="https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
//4.2取值操作
Gson gson=new Gson();
Map map = gson.fromJson(result, Map.class);
String access_token= (String) map.get("access_token");
String openid= (String) map.get("openid");
//4.3赋值操作
userInfoUrl=String.format(userInfoUrl,
access_token,
openid);
/*
4.4 请求该api接口
{
openid: "Xxx",
nickname: "Xxx",
sex: 1,
language: "zh_CN",
city: "Wuhan",
province: "Hubei",
country: "CN",
headimgurl: "Xxx",
privilege: [ ],
unionid: "Xxx"
}
*/
String userInfoResult = HttpClientUtils.get(userInfoUrl);
//4.5 把当前userInfoResult中的信息展示到页面上
Map gsonMap = gson.fromJson(userInfoResult, Map.class);
//4.6封装数据
modelMap.putAll(gsonMap);
return "index";
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
3 resources包下的templates包下创建index.html
注意:在html中必须使用thymeleaf语法
<!DOCTYPE html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
欢迎<font color="red" th:text="${nickname}"></font>登录成功!
<img th:src="${headimgurl}"/>
</body>
</html>
2.8 解决刷新页面后内容不存在问题
问题:
- 当刷新该页面的时候会发现access_token已经为空了,或者时间超过7200s(2小时)access_token也为空.
分析:
- 需要判断条件,然后去调用
刷新或续期access_token使用
接口方法
参考官方文档:刷新token
1 定义静态变量
//定义刷新令牌变量
private static String refresh_token;
2 修改callback回调函数
@GetMapping("/callback")
public String callback(String code, String state, ModelMap modelMap) {
//定义请求结果
String result;
try {
//------------------------------------------------------------------
/**
* 当刷新该页面的时候会发现access_token已经为空了,或者时间超过7200s(2小时)access_token也为空
* 判断access_token是否为null,然后去调用“刷新或续期access_token使用
* 也可以通过判断refresh_token是否为null
* 第一次访问refresh_token一定为null
* 再次刷新refresh_token一定不为null
*/
if (refresh_token != null) {
//刷新token
String refreshUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token?" +
"appid=%s" +
"&grant_type=refresh_token" +
"&refresh_token=%s";
refreshUrl = String.format(refreshUrl,
ConstantPropertiesUtil.WX_OPEN_APP_ID,
refresh_token);
result = HttpClientUtils.get(refreshUrl);
} else {
//------------------------------------------------------------------------------
//2 通过code获取access_token
String baseUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
"?appid=%s" +
"&secret=%s" +
"&code=%s" +
"&grant_type=authorization_code";
//3. 对请求地址进行赋值操作
baseUrl = String.format(baseUrl,
ConstantPropertiesUtil.WX_OPEN_APP_ID,
ConstantPropertiesUtil.WX_OPEN_APP_SECRET,
code);
//4 获取openid和access_token
result = HttpClientUtils.get(baseUrl);
}
//4.根据access_token和openid获取用户个人信息
//4.1 声明获取用户信息的api接口
String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
"?access_token=%s" +
"&openid=%s";
//4.2 取值操作
Gson gson = new Gson();
Map map = gson.fromJson(result, Map.class);
String access_token = (String) map.get("access_token");
String openid = (String) map.get("openid");
WxController.refresh_token = (String) map.get("refresh_token");
//4.3 赋值操作
userInfoUrl = String.format(userInfoUrl,
access_token,
openid);
//4.4 请求该api接口
String userInfoResult = HttpClientUtils.get(userInfoUrl);
//4.5 把当前userInfoResult中信息展示到页面上
Map gsonMap = gson.fromJson(userInfoResult, Map.class);
//4.6 封装数据
modelMap.putAll(gsonMap);
return "index";
} catch (Exception e) {
e.printStackTrace();
}
//System.out.println(result);
return null;
}
最后,祝大家国庆&中秋双倍快乐
202010012200