5.微信登录

在微信官方文档配置东西

  • 关注测试接口
  • 修改 网页服务->网页账号->修改(改成内网穿透的,不能带有http协议)

微信公众平台 (qq.com)

微信开放文档 - 网页开发(qq.com)

配置内网穿透

NATAPP-内网穿透 基于ngrok的国内高速内网映射工具

NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具

创建项目

  1. Idea创建web项目,创建包:
    • constant(因为用了统一返回,所以需要)
    • controller
    • util

接口调用几条比较关键的URL(简):

1.用户同意授权,获取code(发)

https://open.weixin.qq.com/connect/oauth2/authorizeappid=wxf0e81c3bee622d60&redirect_uri=http%3A%2F%2Fnba.bluewebgame.com%2Foauth_response.php&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect

2.如果用户同意授权,页面将跳转至(收)

redirect_uri/?code=CODE&state=STATE

3.通过code换取网页授权access_token(发)

https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

正确时返回的JSON数据包如下

{
  "access_token":"ACCESS_TOKEN",
  "expires_in":7200,
  "refresh_token":"REFRESH_TOKEN",
  "openid":"OPENID",
  "scope":"SCOPE" 
}

4.刷新access_token(如果需要)

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

正确时返回的JSON数据包如下:

{ 
  "access_token":"ACCESS_TOKEN",
  "expires_in":7200,
  "refresh_token":"REFRESH_TOKEN",
  "openid":"OPENID",
  "scope":"SCOPE" 
}

5.拉取用户信息(需scope为 snsapi_userinfo)

http:GET(请使用https协议) https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

正确时返回的JSON数据包如下:

{   
  "openid": "OPENID",
  "nickname": NICKNAME,
  "sex": 1,
  "province":"PROVINCE",
  "city":"CITY",
  "country":"COUNTRY",
  "headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
  "privilege":[ "PRIVILEGE1" "PRIVILEGE2"     ],
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

6.附:检验授权凭证(access_token)是否有效

http:GET(请使用https协议) https://api.weixin.qq.com/sns/auth?access_token=ACCESS_TOKEN&openid=OPENID

正确的JSON返回结果:

{ "errcode":0,"errmsg":"ok"}

错误时的JSON返回示例:

{ "errcode":40003,"errmsg":"invalid openid"}

依赖:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.3.6</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.62</version>
</dependency>

配置:

# 端口
server.port=8089
# 微信授权自定义配置信息
wechat.auth.appId=wx3cb31fff9cffcfee
wechat.auth.appsecret=875ff42bdcb7d32f3f4f44fe68cf6d0b
wechat.auth.codeUri=https://open.weixin.qq.com/connect/oauth2/authorize
wechat.auth.redirectUrl=http://nftvi9.natappfree.cc/wechatAuthCode/backCode
wechat.auth.accessTokenUri=https://api.weixin.qq.com/sns/oauth2/access_token
wechat.auth.userinfoUri=https://api.weixin.qq.com/sns/userinfo

Controller设置

package com.lcywings.sbt.controller;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.lcywings.sbt.util.HttpClient4Util;
import com.lcywings.sbt.util.RequestResult;
import com.lcywings.sbt.util.ResultBuildUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.servlet4preview.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created on 2021/8/6.
 * <p>
 * Author : Lcywings
 * <p>
 * Description : 微信授权操作入口
 */
@RestController
@Slf4j
@RequestMapping("/wechatAuthCode")
public class WechatAuthController {


    @Value("${wechat.auth.appId}")
    private String appId;

    @Value("${wechat.auth.appsecret}")
    private String appsecret;

    @Value("${wechat.auth.codeUri}")
    private String codeUri;

    @Value("${wechat.auth.redirectUrl}")
    private String redirectUri;

    @Value("${wechat.auth.accessTokenUri}")
    private String accessTokenUri;

    @Value("${wechat.auth.userinfoUri}")
    private String userinfoUri;


    /**
     * @author : Lcywings
     * @date : 2021/8/6 9:56
     * @acl : true
     * @description : 请求微信公众号测试平台,通过用户授权,获取code值
     * 注意:code不是同步接口返回的,而是异步更加提供的回调地址,进行回调的
     */
    @RequestMapping("/gainCode")
    public RequestResult<String> wechatAuthCode() {
        // 根据官方实例,生成访问微信官方获取Code的请求地址
        //https://open.weixin.qq.com/connect/oauth2/authorize?
        // appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
        StringBuffer sbf = new StringBuffer(codeUri);
        sbf.append("?appid=").append(appId)
                .append("&redirect_uri=").append(redirectUri)
                .append("&response_type=code")
                .append("&scope=snsapi_userinfo")
                .append("&state=STATE#wechat_redirect");

        log.info("------ 1 请求微信公众号测试平台,通过用户授权获取code的请求地址:{} ------", sbf.toString());

        // 返回请求地址
        return ResultBuildUtil.success(sbf.toString());
    }

    /**
     * @author : Lcywings
     * @date : 2021/8/6 11:01
     * @acl : true
     * @description : 微信官方回调的code值,需要根据此code换取access_token
     */
    @RequestMapping("/backCode")
    public RequestResult<String> wechatAuthCodeBack(HttpServletRequest request) {
        // 微信官方回调的实例:如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE
        // code说明 : code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用
        // 一次,5分钟未被使用自动过期。

        // 获取code值
        String backCode = request.getParameter("code");
        log.info("------ 2 微信官方回调的code值:{} ------", backCode);

        // 验证code是否合法
        if (StringUtils.isEmpty(backCode)) {
            log.info("------ 微信回调code为空 ------");
            return ResultBuildUtil.fail("10001", "微信回调code为空");
        }

        // 通过获取的code值,到微信官方获取access_token
        // 官方的请求实例: https://api.weixin.qq.com/sns/oauth2/access_token
        // ?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
        StringBuffer accessTokenURL = new StringBuffer(accessTokenUri);
        accessTokenURL.append("?appid=").append(appId)
                .append("&secret=").append(appsecret)
                .append("&code=").append(backCode)
                .append("&grant_type=authorization_code");
        log.info("------ 3 通过回调的code值,请求微信官方测试平台,换取access_token,请求地址:{}", accessTokenURL.toString());

        // 根据请求地址,访问获取access_token,注意,此处是同步返回的,不是异步回调
        //返回的内容是json字符串,官方正确实例:{"access_token" : "ACCESS_TOKEN" , expires_in":7200, " refresh_token'
        //通过程序发送http请求
        String backDetail = HttpClient4Util.getResponse4GetAsString(accessTokenURL.toString(), "utf-8");

        log.info("------ 4 请求微信官方,换取access_token,同步返回结果:{}------ ", backDetail);

        // 解析返回的json字符串,获取拉取用户信息,需要的accesss_token
        JSONObject jsonObject = JSON.parseObject(backDetail);

        // 是否返回正确的结果
        if (!StringUtils.isEmpty(jsonObject.getString("errcode"))) {
            return ResultBuildUtil.fail(jsonObject.getString("errcode"), jsonObject.getString("errmsg"));
        }

        // 返回正确,获取拉去用户信息需要的access_token和openid
        String accessToken = jsonObject.getString("access_token");
        String openId = jsonObject.getString("openid");

        // 如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了。
        // 请求方法:Get,请求实例:https://api.weixin.qq.com/sns/userinfo?
        // access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN
        StringBuffer pullUserInfoUrl = new StringBuffer(userinfoUri);
        pullUserInfoUrl.append("?access_token=").append(accessToken)
                .append("&openid=").append(openId)
                .append("&lang=zh_CN");

        log.info("------ 5 通过获取的access_token和openId值,请求微信官方测试平台,拉取用户信息,请求地址:{} ------", pullUserInfoUrl);

        // 访问拉取用户信息的完整地址,获取用户信息
        String userInfo = HttpClient4Util.getResponse4GetAsString(pullUserInfoUrl.toString(), "utf-8");
        log.info("------ 6 请求微信官方,拉取的用户信息为:{} ------", userInfo);

        //回调成功
        return ResultBuildUtil.success();
    }

}

HttpClient4Util类:

package com.lcywings.sbt.util;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

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.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Created on 2020-09-29.
 * <p>
 * Author: zhukang
 * <p>
 * Description: http请求工具类
 */
public final class HttpClient4Util {
    /**
     * 注释内容
     */
    private static Logger log = LoggerFactory.getLogger(HttpClient4Util.class);

    /**
     * 连接超时时间
     */
    public static final int CONNECT_TIMEOUT = 10000;

    /**
     * 读取超时时间
     */
    public static final int SO_TIMEOUT = 30000;

    private HttpClient4Util() {
    }

    /**
     * <HttpClient直接连接接口> <功能详细描述>
     *
     * @param url      接口URL
     * @param params   NameValuePair参数
     * @param encoding 编码
     * @return
     * @see [类、类#方法、类#成员]
     */
    public static String getResponse4PostByMap(String url, Map<String, Object> params, String encoding) {
        return post(url, params, encoding);
    }

    /**
     * <HttpClient直接连接接口> <功能详细描述>
     *
     * @param url      接口URL
     * @param params   NameValuePair参数
     * @param encoding 编码
     * @return
     * @see [类、类#方法、类#成员]
     */
    public static String getResponse4PostByString(String url, String params, String encoding) {
        return post(url, params, encoding);
    }

    /**
     * <HttpClient直接连接接口> <功能详细描述>
     *
     * @param url
     * @param encoding
     * @return
     * @throws Exception
     * @see [类、类#方法、类#成员]
     */
    public static String getResponse4GetAsString(String url, String encoding) {
        return get(url, encoding);
    }

    /**
     * HttpClient直接连接接口,直接返回数据
     *
     * @param url      接口URL
     * @param params   NameValuePair参数
     * @param encoding 编码
     * @return
     * @throws Exception
     */
    private static String post(String url, Map<String, Object> params, String encoding) {
        log.info("执行Http Post请求,地址: " + url + ",参数: " + params);

        String response = null;
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClientBuilder.create().build();
            HttpPost httpPost = new HttpPost(url);

            List<NameValuePair> nvps = new ArrayList<>(params.size());
            if (!params.isEmpty()) {
                Set<Entry<String, Object>> entrySet = params.entrySet();
                for (Entry<String, Object> entry : entrySet) {
                    nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue().toString()));
                }
            }

            // 请求方式为key=value的方式
            httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");
            httpPost.addHeader("Accept-Language", "zh-cn");
            // 及时释放连接,不缓存连接(防止close_wait)
            httpPost.addHeader("Connection", "close");

            httpPost.setEntity(new UrlEncodedFormEntity(nvps, encoding));

            RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(SO_TIMEOUT).setConnectTimeout(CONNECT_TIMEOUT).build();
            httpPost.setConfig(requestConfig);

            HttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();
            if (httpEntity != null) {
                response = EntityUtils.toString(httpEntity, encoding).replaceAll("\r\n", "");
                EntityUtils.consume(httpEntity);
            }

            httpPost.abort();
        } catch (Exception e) {
            log.error("执行Http Post请求失败! Exception: " + e.getMessage());
        } finally {
            if (null != httpClient) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        log.info("Http Post执行后响应内容: " + response);
        return response;
    }

    /**
     * HttpClient直接连接接口,直接返回数据,
     *
     * @param url      接口URL
     * @param params   xml字符串参数
     * @param encoding 编码
     * @return
     * @throws Exception
     */
    private static String post(String url, String params, String encoding) {
        log.info("执行Http Post请求,地址: " + url + ",参数: " + params);

        String response = null;
        CloseableHttpClient httpClient = null;
        try {
            httpClient = HttpClients.createDefault();
            HttpPost httpPost = new HttpPost(url);

            RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(SO_TIMEOUT).setConnectTimeout(CONNECT_TIMEOUT).build();
            httpPost.setConfig(requestConfig);

            StringEntity postEntity = new StringEntity(params, "UTF-8");

            //httpPost.addHeader("Content-Type", "application/x-www-form-urlencoded");
            // httpPost.addHeader("Content-Type", "application/json");
            httpPost.addHeader("Content-Type", "text/xml");
            // 及时释放连接,不缓存连接(防止close_wait)
            httpPost.addHeader("Connection", "close");
            httpPost.setEntity(postEntity);

            HttpResponse httpResponse = httpClient.execute(httpPost);
            HttpEntity httpEntity = httpResponse.getEntity();
            if (httpEntity != null) {
                response = EntityUtils.toString(httpEntity, encoding).replaceAll("\r\n", "");
                EntityUtils.consume(httpEntity);
            }

            httpPost.abort();
        } catch (Exception e) {
            log.error("执行Http Post请求失败! Exception: " + e.getMessage());
        } finally {
            if (null != httpClient) {
                try {
                    httpClient.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        log.info("Http Post执行后响应内容: " + response);
        return response;
    }


    /**
     * HttpClient直接连接接口,直接返回数据
     *
     * @param url      接口URL
     * @param encoding 编码
     * @return
     * @throws Exception
     */
    private static String get(String url, String encoding) {
        log.info("执行Http get请求,地址: " + url);

        String response = null;
        HttpClient httpClient = null;
        try {
            httpClient = HttpClientBuilder.create().build();
            HttpGet httpGet = new HttpGet(url);

            RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(SO_TIMEOUT).setConnectTimeout(CONNECT_TIMEOUT).build();
            httpGet.setConfig(requestConfig);

            httpGet.addHeader("Content-Type", "application/x-www-form-urlencoded");
            httpGet.addHeader("Accept-Language", "zh-cn");
            // 及时释放连接,不缓存连接(防止close_wait)
            httpGet.addHeader("Connection", "close");


            HttpResponse httpResponse = httpClient.execute(httpGet);
            HttpEntity httpEntity = httpResponse.getEntity();
            if (httpEntity != null) {
                response = EntityUtils.toString(httpEntity, encoding).replaceAll("\r\n", "");
                EntityUtils.consume(httpEntity);
            }

            httpGet.abort();
        } catch (Exception e) {
            e.printStackTrace();
            log.error("执行Http GET请求失败! Exception: " + e.getMessage());
        }
        log.info("Http GET执行后响应内容: " + response);
        return response;
    }

}

课后作业

授权成功后,将access_token存入redis,增加一个入口,检验授权凭证(access_token)是否有效,如果无效,主动刷新access_token

Q.E.D.