一、主要流程
实现第三方登录,从微信获取用户信息,用微信公众平台和微信开放平台,其实流程和原理都是一样的,就是调用的接口和对应的参数有点区别。
微信公众平台 和 微信开放平台 对应的官方文档如下:
微信开放文档
准备工作 | 微信开放文档
本次用 公众平台 来测试,主要流程如下:
1、重定向微信接口,微信扫码确认
点击“微信登录”按钮,系统接口返回一个跳转地址(地址微信公众平台提供的,部分参数需要拼接,公众平台的接口只能用微信打开)如下,参数的详细介绍可以参考官网:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
注意:
1.redirect_uri后面携带的参数是微信的回调地址,用户点击“同意”后,微信会请求这个地址,需要用 URLEncoder.encode(path,"utf-8") 进行编码
2、微信扫码确认后,调用回调接口
用手机微信访问上面返回的完整接口后,会提示是否同意登录,点击确认后,微信端会回调第一步中,接口里面设置的回调地址,即redirect_uri,并且携带code和state参数,如下:
redirect_uri?code=CODE&state=STATE
回调方法中,需要获取code以及state
注意:
1.验证当前state是否和跳转微信接口传递的state一致,来防止csrf攻击(可以不进行验证)
2.可以通过state来传递我们自己需要的参数
举例:设置的回调函数为:redirect_uri=http://www.aaa.com/getWeChatUserInfo
那么需要在后台定义一个接口,用来接受微信的回调,如下:
@RequestMapping("/getWeChatUserInfo")public String getWeChatUserInfo(HttpServletRequest request){//校验stateString state = request.getParameter("state");//微信返回的code授权码String code = request.getParameter("code");//请求tokenWeChatAccessToken token = weChatLoginService.getAccessTokenByCode(code);//根据token获取用户信息WeChatUserInfo userInfo = weChatLoginService.getWeixinUserInfo(token);System.out.println(JSON.toJSONString(userInfo));/*** 可以在此处,根据业务需求开发自己的功能*///重定向到其他页面return "redirect:http://www.baidu.com";}
3、根据code获得微信的access_token
上一步获取code后,携带code和appId等参数,调用微信接口,获得access_token,微信接口:
https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
返回结果如下:
{"access_token": "52_mgvyGIVQGJoVw-JUo-3GeCQggL5g11rtduGu9lwMOp5sZOIanEVH9rpuabAu9oo8nu89TcZszwz10eRq5DDDdoIuXZGPyfMEdcynGP1gS0U","expires_in": 7200,"refresh_token": "52_K6X9KzoYPlTUem-MCH05X0KCIzikZy-UiX4XdxhSW-pUgWjlAVn8BDGTfwmQ4mk1Bdhd0-SSxmP1f5HQYecxWBdcGLa7UhFer1pvDzUs1gU","openid": "oGy6Y5tp9qxP5XLBvRU2OCIxFdvQ","scope": "snsapi_userinfo"
}
4、获取到token后,再去调用微信接口,获取微信端用户的信息
https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID
返回结果如下:
{"openid": "oGy6Y5tp9qxP5XLBvRU2OCIxFdvQ","nickname": "A→枫火","sex": 0,"language": "","city": "","province": "","country": "","headimgurl": "https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKr27Hpfq957HPIXHoReLn1OvVXWFchNdW1UiaicBOugzz5ax7Hn8VzGceC50NRRQ02ibhA4ibFXZX3zg/132","privilege": []
}
二、准备工作
微信公众平台提供测试账号,而开放平台必须要有开发者资质认证,所以用公众平台来测试
1、获取APPID和appsecret
上面提到的一些参数,appId和appsecret,是需要获取的,打开链接,扫码登录
微信公众平台
2、 关注公众号
公众平台,转发的请求地址,只能用手机微信打开,而访问请求,必须先关注下面的测试公众号
3、设置微信回调域名
通过:网页服务-网页帐号-网页授权获取用户基本信息,点击修改,进行回调域名的配置
设置回调函数的时候,域名必须要和这里设置的域名相同
4、设置花生壳的内网穿透
因为在设置微信回调接口的时候,微信端是要访问我们设置的回调接口的,回调接口中的域名要和第三步中设置的域名保持一致
因此使用花生壳设置内网穿透,保证手机微信在调用此域名的时候,能够访问我们电脑端上的服务
三、代码实现
1、先创建一个springboot项目,结构如下
核心的代码是:ThirdLoginWeChatController 和 WeChatLoginService,其他的都是配置和工具类
2、具体对应的代码
application.yml,配置信息,
server:port: 9200
oauth:weixin:#微信公众平台的应用idappID: #微信公众平台的应用秘钥appsecret: #获取授权码的URLauthorizeUrl: https://open.weixin.qq.com/connect/oauth2/authorize#获取令牌的URLtokenUrl: https://api.weixin.qq.com/sns/oauth2/access_token#获取用户信息的URLuserInfoUrl: https://api.weixin.qq.com/sns/userinfo#回调地址的URLbackUrl: http://273mc88979.zicp.vip/wechat/getWeChatUserInfo
WeChatConfig,配置类,获取并封装配置文件中的信息,包括APPID,appsecret,访问微信的接口和回调域名等
@Data
@Configuration
@ConfigurationProperties(prefix = "oauth.weixin")
public class WeChatConfig {//微信公众平台的应用idprivate String appID;//微信公众平台的应用秘钥private String appsecret;//获取授权码的URLprivate String authorizeUrl;//获取令牌的URLprivate String tokenUrl;//获取用户信息的URLprivate String userInfoUrl;//回调地址的URLprivate String backUrl;
}
WeChatConstant,定义一些常量,主要是发送微信请求所需要的参数常量
@Data
public class WeChatConstant {public static final String SNSAPI_USERINFO = "snsapi_userinfo";public static final String RESPONSE_TYPE_CODE = "code";public static final String AUTHORIZATION_CODE = "authorization_code";//作为key,用来在redis中保存statepublic static final String STATE_KEY = "state_key";
}
WeChatAccessToken,实体类,封装微信返回的accesstoken
@Data
public class WeChatAccessToken {//授权用户唯一标识private String openid;//接口调用凭证private String access_token;//用户刷新access_tokenprivate String refresh_token;//用户授权的作用域,使用逗号(,)分隔private String scope;//凭证超时时间,单位(秒)private String expires_in;//当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段。private String unionid;
}
WeChatUserInfo,实体类,封装微信返回的用户信息
@Data
public class WeChatUserInfo {//授权用户唯一标识private String openId;//昵称private String nickName;//性别,1:男,2:女,0:未知private String sex;//头像地址private String headImgUrl;//国籍private String country;//省private String province;//城市private String city;//语言private String language;//特权private String privilege;//关联内部用户的idprivate String systemUserId;//关联内部用户的用户名private String systemUserName;
}
ThirdLoginWeChatController,controller,对外提供的访问接口和微信回调接口
@Controller
@RequestMapping("/wechat")
public class ThirdLoginWeChatController {@AutowiredWeChatLoginService weChatLoginService;/*** 点击第三方登录,使用微信登录* 跳转到微信扫码页面*/@GetMapping("/toLogin")@ResponseBodypublic String login(){//获取跳转的扫码地址String redirectUrl = weChatLoginService.getWeixinQRUrl();return redirectUrl;}/*** 回调函数,用户扫码同意后,跳转到该接口,并携带code和state* 1.获取code和state* 2.通过code获取token* 3.再根据token获取微信端的用户信息*/@RequestMapping("/getWeChatUserInfo")@ResponseBodypublic String getWeChatUserInfo(HttpServletRequest request){//校验stateString state = request.getParameter("state");//微信返回的code授权码String code = request.getParameter("code");System.out.println("===================code:"+request.getParameter("code"));System.out.println("===================state:"+request.getParameter("state"));//从redis获取state信息,与回调接受的state对比,可用于防止csrf攻击(跨站请求伪造攻击)
// String realState = redisService.getCacheObject(WeChatConstant.STATE_KEY);
// if(!state.equals(realState)){
// throw new ServiceException("微信回调地址,返回的state不一致");
// }//请求tokenWeChatAccessToken token = weChatLoginService.getAccessTokenByCode(code);//根据token获取用户信息WeChatUserInfo userInfo = weChatLoginService.getWeixinUserInfo(token);System.out.println(JSON.toJSONString(userInfo));/*** 可以在此处,根据业务需求开发自己的功能* 如:关联本地账户,用来实现登录逻辑* 1.本地有账户,直接登录,返回token* 2.本地没有账户,需要跳转页面,用户注册后绑定微信*/return JSON.toJSONString(userInfo);//重定向到其他页面//return "redirect:http://www.baidu.com";}
}
WeChatLoginService,service,用来获取token和用户信息等业务操作
@Service
public class WeChatLoginService {@AutowiredWeChatConfig authWinxinConfig;/*** 获取第三方登录页面,即显示微信扫码页面的地址*/public String getWeixinQRUrl(){//String redirectUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";//处理一下回调地址String backUrl = null;try {String path = authWinxinConfig.getBackUrl();backUrl = URLEncoder.encode(path,"utf-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}//生成随机的UUID,作为state,微信平台回调本地接口时,会携带此参数,可以校验此参数,可用于防止csrf攻击(跨站请求伪造攻击)
// String state = UUID.randomUUID().toString();
// redisService.setCacheObject(WeChatConstant.STATE_KEY,state);//拼接跳转的扫码地址StringBuffer redirectUrl = new StringBuffer();redirectUrl.append(authWinxinConfig.getAuthorizeUrl()).append("?appid=").append(authWinxinConfig.getAppID()).append("&redirect_uri=").append(backUrl).append("&scope=").append(WeChatConstant.SNSAPI_USERINFO).append("&response_type=").append(WeChatConstant.RESPONSE_TYPE_CODE).append("&state=").append("123").append("#wechat_redirect");return redirectUrl.toString();}/*** 根据code获取token和openid*/public WeChatAccessToken getAccessTokenByCode(String code){//https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_codeStringBuffer tokenUrl = new StringBuffer();tokenUrl.append(authWinxinConfig.getTokenUrl()).append("?appid=").append(authWinxinConfig.getAppID()).append("&secret=").append(authWinxinConfig.getAppsecret()).append("&code=").append(code).append("&grant_type=").append(WeChatConstant.AUTHORIZATION_CODE);String result = HttpUtils.get(tokenUrl.toString());System.out.println(result);WeChatAccessToken token = JSON.parseObject(result, WeChatAccessToken.class);return token;}/*** 根据token和openID获取用户信息*/public WeChatUserInfo getWeixinUserInfo(WeChatAccessToken token){//https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENIDStringBuffer userInfoUrl = new StringBuffer();userInfoUrl.append(authWinxinConfig.getUserInfoUrl()).append("?access_token=").append(token.getAccess_token()).append("&openid=").append(token.getOpenid());String userStr = HttpUtils.get(userInfoUrl.toString());System.out.println(userStr);WeChatUserInfo weChartUserInfo = JSON.parseObject(userStr, WeChatUserInfo.class);return weChartUserInfo;}
}
HttpUtils,简单的工具类,封装了HttpClient方法的实现
public class HttpUtils<T> {/*** get请求返回JSONObject* @param url* @return*/public static JSONObject getJSON(String url){String resultStr = get(url);JSONObject res = JSONObject.parseObject(resultStr);return res;}/*** post请求返回JSONObject* @param url* @return*/public static JSONObject postJSON(String url, String jsonStr){String resultStr = post(url, jsonStr);JSONObject res = JSONObject.parseObject(resultStr);return res;}/*** 发送 get 请求** @param url 请求地址* @return 请求结果*/public static String get(String url) {String result = null;CloseableHttpResponse response = null;CloseableHttpClient httpclient = HttpClients.createDefault();try {// 创建uriURIBuilder builder = new URIBuilder(url);URI uri = builder.build();// 创建http GET请求HttpGet httpGet = new HttpGet(uri);// 执行请求response = httpclient.execute(httpGet);if (response.getStatusLine().getStatusCode() == 200) {result = EntityUtils.toString(response.getEntity(), "UTF-8");}} catch (Exception e) {e.printStackTrace();}return result;}/*** 发送 post 请求** @param url 请求地址* @param jsonStr Form表单json字符串* @return 请求结果*/public static String post(String url, String jsonStr) {// 创建httpClientCloseableHttpClient httpClient = HttpClients.createDefault();// 创建post请求方式实例HttpPost httpPost = new HttpPost(url);// 设置请求头 发送的是json数据格式httpPost.setHeader("Content-type", "application/json;charset=utf-8");httpPost.setHeader("Connection", "Close");// 设置参数---设置消息实体 也就是携带的数据StringEntity entity = new StringEntity(jsonStr, Charset.forName("UTF-8"));// 设置编码格式entity.setContentEncoding("UTF-8");// 发送Json格式的数据请求entity.setContentType("application/json");// 把请求消息实体塞进去httpPost.setEntity(entity);// 执行http的post请求CloseableHttpResponse httpResponse;String result = null;try {httpResponse = httpClient.execute(httpPost);result = EntityUtils.toString(httpResponse.getEntity(), "UTF-8");} catch (IOException e) {e.printStackTrace();}return result;}
}