PC端实现微信第三方登录
泛域名ssl证书 239元1年送1个月、单域名39元1年,Sectigo(原Comodo证书)全球可信证书,强大的兼容性,高度安全性,如有问题7天内可退、可开发票
加微信VX 18718058521 备注SSL证书
【腾讯云】2核2G4M云服务器新老同享99元/年,续费同价
一、主要流程
实现第三方登录,从微信获取用户信息,用微信公众平台和微信开放平台,其实流程和原理都是一样的,就是调用的接口和对应的参数有点区别。
微信公众平台 和 微信开放平台 对应的官方文档如下:
微信开放文档
准备工作 | 微信开放文档
本次用 公众平台 来测试,主要流程如下:
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){ //校验state String state = request.getParameter("state"); //微信返回的code授权码 String code = request.getParameter("code"); //请求token WeChatAccessToken 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: #微信公众平台的应用id appID: #微信公众平台的应用秘钥 appsecret: #获取授权码的URL authorizeUrl: https://open.weixin.qq.com/connect/oauth2/authorize #获取令牌的URL tokenUrl: https://api.weixin.qq.com/sns/oauth2/access_token #获取用户信息的URL userInfoUrl: https://api.weixin.qq.com/sns/userinfo #回调地址的URL backUrl: http://273mc88979.zicp.vip/wechat/getWeChatUserInfo
WeChatConfig,配置类,获取并封装配置文件中的信息,包括APPID,appsecret,访问微信的接口和回调域名等
@Data @Configuration @ConfigurationProperties(prefix = "oauth.weixin") public class WeChatConfig { //微信公众平台的应用id private String appID; //微信公众平台的应用秘钥 private String appsecret; //获取授权码的URL private String authorizeUrl; //获取令牌的URL private String tokenUrl; //获取用户信息的URL private String userInfoUrl; //回调地址的URL private 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中保存state public static final String STATE_KEY = "state_key"; }
WeChatAccessToken,实体类,封装微信返回的accesstoken
@Data public class WeChatAccessToken { //授权用户唯一标识 private String openid; //接口调用凭证 private String access_token; //用户刷新access_token private 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; //关联内部用户的id private String systemUserId; //关联内部用户的用户名 private String systemUserName; }
ThirdLoginWeChatController,controller,对外提供的访问接口和微信回调接口
@Controller @RequestMapping("/wechat") public class ThirdLoginWeChatController { @Autowired WeChatLoginService weChatLoginService; /** * 点击第三方登录,使用微信登录 * 跳转到微信扫码页面 */ @GetMapping("/toLogin") @ResponseBody public String login(){ //获取跳转的扫码地址 String redirectUrl = weChatLoginService.getWeixinQRUrl(); return redirectUrl; } /** * 回调函数,用户扫码同意后,跳转到该接口,并携带code和state * 1.获取code和state * 2.通过code获取token * 3.再根据token获取微信端的用户信息 */ @RequestMapping("/getWeChatUserInfo") @ResponseBody public String getWeChatUserInfo(HttpServletRequest request){ //校验state String 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不一致"); // } //请求token WeChatAccessToken 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 { @Autowired WeChatConfig 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_code StringBuffer 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=OPENID StringBuffer 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 { // 创建uri URIBuilder 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) { // 创建httpClient CloseableHttpClient 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; } }