All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hn.utils.weixin.offiaccount.WebAuthorization Maven / Gradle / Ivy

There is a newer version: 1.0.18
Show newest version
package com.hn.utils.weixin.offiaccount;

import cn.hutool.cache.CacheUtil;
import cn.hutool.cache.impl.TimedCache;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.hn.config.HnConfigUtils;
import com.hn.config.exception.ConfigException;
import com.hn.utils.AssertUtils;
import com.hn.utils.weixin.ErrorCode;
import com.hn.utils.weixin.exception.WxException;
import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * 描述: 微信网页授权
 *
 * @author fei
 */
public class WebAuthorization {

    private static final Log log = LogFactory.get();

    /**
     * 由于access_token拥有较短的有效期,当access_token超时后,
     * 可以使用refresh_token进行刷新,refresh_token有效期为30天,当refresh_token失效之后,需要用户重新授权。
     */
    private static TimedCache refreshTokenCache = CacheUtil.newTimedCache(30 * DateUnit.DAY.getMillis());

    /**
     * 若提示“该链接无法访问”,请检查参数是否填写错误,是否拥有scope参数对应的授权作用域权限。
     * 10003	redirect_uri域名与后台配置不一致
     * 10004	此公众号被封禁
     * 10005	此公众号并没有这些scope的权限
     * 10006	必须关注此测试号
     * 10009	操作太频繁了,请稍后重试
     * 10010	scope不能为空
     * 10011	redirect_uri不能为空
     * 10012	appid不能为空
     * 10013	state不能为空
     * 10015	公众号未授权第三方平台,请检查授权状态
     * 10016	不支持微信开放平台的Appid,请使用公众号Appid
     */
    private static final String WEB_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize" +
            "?appid={APPID}&redirect_uri={REDIRECT_URI}&response_type=code&scope={SCOPE}&state={STATE}#wechat_redirect";

    /**
     * 通过code换取网页授权access_token URL
     */
    private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";

    /**
     * 拉取用户信息(需scope为 snsapi_userinfo) URL
     */
    private static final String USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo";

    /**
     * 检验授权凭证(access_token)是否有效 URL
     */
    private static final String CHECK_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/auth";

    /**
     * 刷新access_token URL
     */
    private static final String REFRESH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/refresh_token";

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

    /**
     * 不弹出授权页面,直接跳转,只能获取用户openid
     *
     * @param redirectUri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
     * @return 授权链接
     */
    public static String authorizeBaseUrl(String redirectUri) {
        return WEB_AUTHORIZE_URL
                .replace("{APPID}", getAppId())
                .replace("{REDIRECT_URI}", redirectUri)
                .replace("{SCOPE}", Scope.SNSAPI_BASE.scope)
                // 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
                .replace("{STATE}", "");
    }

    /**
     * 弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息
     *
     * @param redirectUri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
     * @return 授权链接
     */
    public static String authorizeUserInfoUrl(String redirectUri) {
        return WEB_AUTHORIZE_URL
                .replace("{APPID}", getAppId())
                .replace("{REDIRECT_URI}", redirectUri)
                .replace("{SCOPE}", Scope.SNSAPI_USERINFO.scope)
                // 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
                .replace("{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获取用户信息等步骤,也必须从服务器发起。 * * @param code code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。 * @return JSONObject * access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 * expires_in access_token接口调用凭证超时时间,单位(秒) * refresh_token 用户刷新access_token * openid 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID * scope 用户授权的作用域,使用逗号(,)分隔 */ private static JSONObject accessToken(String code) { Map map = new HashMap<>(); // 公众号的唯一标识 map.put("appid", getAppId()); // 公众号的appsecret map.put("secret", getAppSecret()); // 填写第一步获取的code参数 map.put("code", code); // authorization_code map.put("grant_type", "authorization_code"); String resultStr = HttpUtil.get(ACCESS_TOKEN_URL, map); log.info("通过code换取网页授权access_token:{}", resultStr); JSONObject resultObj = JSONUtil.parseObj(resultStr); Integer errCode = resultObj.getInt("errcode"); if (ObjectUtil.isNotNull(errCode)) { AssertUtils.isTrue(errCode == 0, WxException.exception(errCode, resultObj.getStr("errmsg"))); } refreshTokenCache.put(resultObj.getStr("openid"), resultObj.getStr("refresh_token")); return resultObj; } /** * 第三步:刷新access_token(如果需要) * 由于access_token拥有较短的有效期,当access_token超时后, * 可以使用refresh_token进行刷新,refresh_token有效期为30天,当refresh_token失效之后,需要用户重新授权。 * * @param refreshToken 通过access_token获取到的refresh_token参数 * @return access_token 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 * expires_in access_token接口调用凭证超时时间,单位(秒) * refresh_token 用户刷新access_token * openid 用户唯一标识 * scope 用户授权的作用域,使用逗号(,)分隔 */ private static JSONObject refreshToken(String refreshToken) { Map map = new HashMap<>(); // 公众号的唯一标识 map.put("appid", getAppId()); map.put("refresh_token", refreshToken); map.put("grant_type", "refresh_token"); String resultStr = HttpUtil.get(REFRESH_ACCESS_TOKEN_URL, map); log.info("刷新access_token :{}", resultStr); JSONObject resultObj = JSONUtil.parseObj(resultStr); Integer errCode = resultObj.getInt("errcode"); if (ObjectUtil.isNotNull(errCode)) { AssertUtils.isTrue(errCode == 0, WxException.exception(errCode, resultObj.getStr("errmsg"))); } return resultObj; } /** * 获取进入页面的用户的openid的 * * @param code code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。 * @return 用户的openid */ public static String getOpenId(String code) { return accessToken(code).getStr("openid"); } /** * 获取微信用户信息 * * @param code code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。 * @return JSONObject */ public static UserInfo getUserInfo(String code) { JSONObject accessTokenResult = accessToken(code); String accessToken = accessTokenResult.getStr("access_token"); String openId = accessTokenResult.getStr("openid"); return getUserInfo(accessToken, openId); } /** * 获取微信用户信息(需scope为 snsapi_userinfo) * @param openId 用户的唯一标识 * @return UserInfo */ public static UserInfo getUserInfoByOpenId(String openId) { JSONObject refreshTokenObj = refreshToken(refreshTokenCache.get(openId)); String accessToken = refreshTokenObj.getStr("access_token"); openId = refreshTokenObj.getStr("openid"); return getUserInfo(accessToken, openId); } /** * 第四步:拉取用户信息(需scope为 snsapi_userinfo) * 如果网页授权作用域为snsapi_userinfo,则此时开发者可以通过access_token和openid拉取用户信息了。 * * @param accessToken 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 * @param openId 用户的唯一标识 * @return userInfo */ public static UserInfo getUserInfo(String accessToken, String openId) { // 检验授权凭证是否有效 if (!checkAccessToken(accessToken, openId)) { JSONObject refreshTokenObj = refreshToken(refreshTokenCache.get(openId)); accessToken = refreshTokenObj.getStr("access_token"); openId = refreshTokenObj.getStr("openid"); } Map map = new HashMap<>(); // 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 map.put("access_token", accessToken); // 用户的唯一标识 map.put("openid", openId); //返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语 map.put("lang", "zh_CN"); String resultStr = HttpUtil.get(USER_INFO_URL, map); log.info("微信拉取用户信息:{}", resultStr); UserInfo userInfo = JSONUtil.toBean(resultStr,UserInfo.class); Integer errCode = userInfo.getErrcode(); if (ObjectUtil.isNotNull(errCode)) { AssertUtils.isTrue(errCode == 0, WxException.exception(errCode, userInfo.getErrmsg())); } return userInfo; } /** * 检验授权凭证(access_token)是否有效 * * @param accessToken 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 * @param openId 用户的唯一标识 * @return false 已失效 true 有效 */ public static boolean checkAccessToken(String accessToken, String openId) { Map map = new HashMap<>(); // 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同 map.put("access_token", accessToken); // 用户的唯一标识 map.put("openid", openId); String resultStr = HttpUtil.get(CHECK_ACCESS_TOKEN_URL, map); log.info("微信检验授权凭证是否有效:{}", resultStr); JSONObject resultObj = JSONUtil.parseObj(resultStr); Integer errCode = resultObj.getInt("errcode"); if (ObjectUtil.isNotNull(errCode) && errCode == 0) { return true; } return false; } /** * 应用授权作用域 */ public enum Scope { /** * snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid) */ SNSAPI_BASE("snsapi_base"), /** * snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 ) */ SNSAPI_USERINFO("snsapi_userinfo"); Scope(String scope) { this.scope = scope; } public String scope; } @Data public class UserInfo extends ErrorCode { /** * 用户的唯一标识 */ private String openid; /** * 用户昵称 */ private String nickname; /** * 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知 */ private Integer sex; /** * 用户个人资料填写的省份 */ private String province; /** * 普通用户个人资料填写的城市 */ private String city; /** * 国家,如中国为CN */ private String country; /** * 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像), * 用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。 */ private String headimgurl; /** * 用户特权信息,json 数组,如微信沃卡用户为(chinaunicom) */ private String privilege; /** * 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。 */ private String unionid; } /** * 配置前缀名 */ public static final String CONFIG_KEY = "wx.public.account"; private static String getAppId(){ return getParam().get("appId"); } private static String getAppSecret(){ return getParam().get("appSecret"); } private static Map getParam(){ String appId = AssertUtils.notNull(HnConfigUtils.getConfig(CONFIG_KEY + ".appId"), ConfigException.exception("微信公众号appId未配置")); String appSecret = AssertUtils.notNull(HnConfigUtils.getConfig(CONFIG_KEY + ".appSecret"), ConfigException.exception("微信公众号appSecret未配置")); Map map = new HashMap<>(); map.put("appId",appId); map.put("appSecret",appSecret); return map; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy