根据官方文档在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头,也不需要加具体的项目名,在域名空间的根目录放一个txt文件才能验证通过
一、两种scope授权方式
- 以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(往往是业务页面)
- 以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息
二、关于特殊场景下的静默授权
- 上面已经提到,对于以snsapi_base为scope的网页授权,就静默授权的,用户无感知
- 对于已关注公众号的用户,如果用户从公众号的会话或者自定义菜单进入本公众号的网页授权页,即使是scope为snsapi_userinfo,也是静默授权,用户无感知
三、用户同意授权,获取code
请求链接https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect
package com.phil.wechatauth.model.req; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.util.Map; import java.util.TreeMap; import com.phil.common.config.SystemConfig; import com.phil.common.params.AbstractParams; import com.phil.common.util.HttpReqUtil; /** * 获取授权code请求参数 * * @author phil * @date 2017年7月2日 * */ public class AuthCodeParams extends AbstractParams implements Serializable{ /** * */ private static final long serialVersionUID = 1L; public static final String SCOPE_SNSAPIBASE = "snsapi_base"; // snsapi_base(不需要弹出授权页面,只能获取openid) public static final String SCOPE_SNSPAIUSERINFO = "snsapi_userinfo"; // 弹出授权页面(获取用户基本信息) private String appid; private String redirect_uri; // 使用urlencode对链接进行处理 private String response_type = "code"; private String scope; private String state; public AuthCodeParams() { super(); } public AuthCodeParams(String appid, String redirect_uri, String response_type, String scope, String state) { super(); this.appid = appid; this.redirect_uri = redirect_uri; this.response_type = response_type; this.scope = scope; this.state = state; } /** * 参数组装 * * @return */ public Map<String, String> getParams() throws UnsupportedEncodingException { Map<String, String> params = new TreeMap<String, String>(); params.put("appid", this.appid); params.put("redirect_uri", HttpReqUtil.urlEncode(this.redirect_uri, SystemConfig.CHARACTER_ENCODING)); params.put("response_type", this.response_type); params.put("scope", this.scope); params.put("state", this.state); return params; } public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getRedirect_uri() { return redirect_uri; } public void setRedirect_uri(String redirect_uri) { this.redirect_uri = redirect_uri; } public String getResponse_type() { return response_type; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } public String getState() { return state; } public void setState(String state) { this.state = state; } }
组装请求链接
/** * 获取授权请求path * @param basic * @param path * @return * @throws Exception */ public String getAuthPath(AbstractParams basic, String path) throws Exception{ Map<String,String> params = basic.getParams(); path = HttpRequestUtil.setParmas(params, path,"")+"#wechat_redirect"; return path; }
尤其注意:由于授权操作安全等级较高,所以在发起授权请求时,微信会对授权链接做正则强匹配校验,如果链接的参数顺序不对,授权页面将无法正常访问
请在微信客户端中打开此链接
如果用户同意授权,页面将跳转至redirect_uricode=CODE&state=STATE ,可以用request.getParameter("code")直接获取到code,在此之后检验state是否发生变化。code作为换取access_token的票据,每次用户授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期,可用redis、mc等缓存
四、通过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获取用户信息等步骤,也必须从服务器发起。
请求参数封装
package com.phil.wechatauth.model.req; import java.io.Serializable; import java.util.Map; import java.util.TreeMap; import com.phil.common.params.AbstractParams; /** * 获取授权请求token的请求参数 * @author phil * @date 2017年7月2日 * */ public class AuthTokenParams extends AbstractParams implements Serializable{ /** * */ private static final long serialVersionUID = 4652953400751046159L; private String appid; //公众号的唯一标识 private String secret; //公众号的appsecret private String code; //填写第一步获取的code参数 private String grant_type = "authorization_code"; public AuthTokenParams() { super(); } public AuthTokenParams(String appid, String secret, String code, String grant_type) { super(); this.appid = appid; this.secret = secret; this.code = code; this.grant_type = grant_type; } /** * 参数组装 * @return */ public Map<String, String> getParams() { Map<String, String> params = new TreeMap<String, String>(); params.put("appid", this.appid); params.put("secret", this.secret); params.put("code", this.code); params.put("grant_type", this.grant_type); return params; } public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getSecret() { return secret; } public void setSecret(String secret) { this.secret = secret; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getGrant_type() { return grant_type; } }
返回的json封装
package com.phil.wechatauth.model.resp; /** * 网页授权access_token * * @author phil * @date 2017年7月2日 * */ public class AuthAccessToken extends AccessToken { private String refresh_token; // 用户刷新access_token private String openid; // 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID private String scope; // 用户授权的作用域,使用逗号(,)分隔 public String getRefresh_token() { return refresh_token; } public void setRefresh_token(String refresh_token) { this.refresh_token = refresh_token; } public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getScope() { return scope; } public void setScope(String scope) { this.scope = scope; } }
获取access_token方法
/** * 获取网页授权凭证 * @param basic * @param path * @return */ public AuthAccessToken getAuthAccessToken(AbstractParams basic, String path) { AuthAccessToken authAccessToken = null; //获取网页授权凭证 try { String result = HttpReqUtil.HttpsDefaultExecute(HttpReqUtil.GET_METHOD, path, basic.getParams(), null); if(result != null){ authAccessToken = JsonUtil.fromJson(result, AuthAccessToken.class); } } catch (Exception e) { authAccessToken = null; logger.info("error"+e.getMessage()); } return authAccessToken; }
获取授权进入页面
// 获取token的链接 private final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"; @RequestMapping(value = "bindWxPhone", method = { RequestMethod.GET }) public String prize(HttpServletRequest request, HttpServletResponse response, String url) throws Exception { AuthAccessToken authAccessToken = null; // 用户同意授权后可以获得code,检验state String code = request.getParameter("code"); if(code==null){ return null; } String state = request.getParameter("state"); if(state.equals(MD5Util.MD5Encode("ceshi", ""))){ AuthTokenParams authTokenParams = new AuthTokenParams(); authTokenParams.setAppid(""); authTokenParams.setSecret(""); authTokenParams.setCode(code); authAccessToken = oAuthService.getAuthAccessToken(authTokenParams, ACCESS_TOKEN_URL); } if(authAccessToken!=null){ logger.info("网页授权的accessToken=" + authAccessToken.getAccess_token()); logger.info("正在绑定的openid=" + authAccessToken.getOpenid()); } return "/system/wxuserprize/bindPhone"; }
五、刷新access_token(如果需要)
由于access_token拥有较短的有效期,当access_token超时后,可以使用refresh_token进行刷新,refresh_token有效期为30天,当refresh_token失效之后,需要用户重新授权。
请求参数封装
package com.phil.wechatauth.model.req; import java.io.Serializable; import java.util.Map; import java.util.TreeMap; import com.phil.common.params.AbstractParams; /** * 刷新token请求 * @author phil * @date 2017年7月2日 * */ public class RefreshTokenParams extends AbstractParams implements Serializable{ /** * */ private static final long serialVersionUID = -7200815808171378571L; private String appid; private String grant_type = "refresh_token"; private String refresh_token; public RefreshTokenParams(String appid, String grant_type, String refresh_token) { super(); this.appid = appid; this.grant_type = grant_type; this.refresh_token = refresh_token; } /** * 参数组装 * * @return */ public Map<String, String> getParams() { Map<String, String> params = new TreeMap<String, String>(); params.put("appid", this.appid); params.put("grant_type", this.grant_type); params.put("refresh_token", this.refresh_token); return params; } public String getAppid() { return appid; } public void setAppid(String appid) { this.appid = appid; } public String getGrant_type() { return grant_type; } public String getRefresh_token() { return refresh_token; } public void setRefresh_token(String refresh_token) { this.refresh_token = refresh_token; } }
/** * 刷新网页授权验证 * @param basic 参数 * @param path 请求路径 * @return */ public String refreshAuthAccessToken(AbstractParams basic, String path) { AuthAccessToken authAccessToken = null; //刷新网页授权凭证 try { String result = HttpReqUtil.HttpsDefaultExecute(HttpReqUtil.GET_METHOD, path, basic.getParams(), null); if(result != null){ authAccessToken = JsonUtil.fromJson(result, AuthAccessToken.class); } } catch (Exception e) { authAccessToken = null; logger.info("error"+e.getMessage()); } return authAccessToken==null?null:authAccessToken.getAccess_token(); }
六、拉取用户信息
如果网页授权作用域为snsapi_userinfo,则此时可以通过access_token和openid拉取用户信息了
通过网页授权获取的用户信息
/** * 通过网页授权获取的用户信息 * * @author phil */ public class AuthUserInfo { // 用户标识 private String openid; // 用户昵称 private String nickname; // 性别(1是男性,2是女性,0是未知) private String sex; // 国家 private String country; // 省份 private String province; // 城市 private String city; // 用户头像链接 private String headimgurl; // 用户特权信息 private List<String> privilege; // 只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段 private String unionid; public String getOpenid() { return openid; } public void setOpenid(String openid) { this.openid = openid; } public String getNickname() { return nickname; } public void setNickname(String nickname) { this.nickname = nickname; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getHeadimgurl() { return headimgurl; } public void setHeadimgurl(String headimgurl) { this.headimgurl = headimgurl; } public List<String> getPrivilege() { return privilege; } public void setPrivilege(List<String> privilege) { this.privilege = privilege; } public String getUnionid() { return unionid; } public void setUnionid(String unionid) { this.unionid = unionid; } }
获取方法
//获取授权用户信息 private final String USERINFO_URL = "https://api.weixin.qq.com/sns/userinfo"; /** * 通过网页授权获取用户信息 * @param accessToken * @param openid * @return */ public AuthUserInfo getAuthUserInfo(String accessToken, String openid) { AuthUserInfo authUserInfo = null; //通过网页授权获取用户信息 Map<String, String> params = new TreeMap<String, String>(); params.put("openid", openid); params.put("access_token", accessToken); String result = HttpReqUtil.HttpsDefaultExecute(HttpReqtUtil.GET_METHOD, USERINFO_URL, params, null); if(null != result){ try { authUserInfo = JsonUtil.fromJson(result, AuthUserInfo.class); } catch (JsonSyntaxException e) { logger.info("transfer exception"); } } return authUserInfo; }
七、检验授权凭证(access_token)是否有效
//判断用户accessToken是否有效 private final String AUTH_URL = "https://api.weixin.qq.com/sns/auth"; /** * 检验授权凭证(access_token)是否有效 * @param accessToken 网页授权接口调用凭证 * @param openid 用户的唯一标识 * @return { "errcode":0,"errmsg":"ok"}表示成功 { "errcode":40003,"errmsg":"invalid openid"}失败 */ public ResultState authToken(String accessToken,String openid){ ResultState state = null; Map<String,String> params = new TreeMap<String,String>(); params.put("access_token",accessToken); params.put("openid", openid); String json = HttpReqUtil.HttpsDefaultExecute(HttpReqUtil.GET_METHOD, AUTH_URL, params,""); if(json!=null){ state = JsonUtil.fromJson(json, ResultState.class); } return state; }