微信开放平台开发第三方授权登陆:微信扫码登录

一、概述

根据需求,需要拥有第三方微信登录功能,并获取到用户信息。
网站应用微信登录是基于OAuth2.0协议标准构建的微信OAuth2.0授权登录系统。

二、前期准备工作

1、注册邮箱账号。

2、根据邮箱账号注册微信开放平台账号,完善开发者资料。

3、申请开发者资质认证、填写相关资料、填写发票、支付认证金额。提交并等待认证结果

1)申请开发者资质认证

2)选定类型

3)填写“认证资料”

4)填写“管理员信息”

5)上传“企业基本信息”材料:

6)进入填写发票及支付费用

4、认证成功后,创建网站应用,填写基本信息、下载网站信息登记表填写并上传扫描件、填写授权回调域等。提交审核等待结果。

1)创建网站应用

2)创建移动应用

5、认证成功后,创建移动应用,至少选择安卓、IOS、WP8其中一种平台

6、创建应用成功后,申请微信登陆,等待审核结果,待审核通过后,可进行微信登陆的开发。

注:创建应用和开发者资质认证可同时进行
微信开放平台开发第三方授权登陆:微信扫码登录_第1张图片
准备工作大致流程图

三、开发流程

1)第三方发起微信授权登录请求,微信用户允许授权第三方应用后,微信会拉起应用或重定向到第三方网站,并且带上授权临时票据code参数;

2)通过code参数加上AppID和AppSecret等,通过API换取access_token;

3)通过access_token进行接口调用,获取用户基本数据资源或帮助用户实现基本操作。
微信开放平台开发第三方授权登陆:微信扫码登录_第2张图片

四、具体实现步骤

1、准备工作

1)添加依赖

<!-- 添加httpclient支持 -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.2</version>
    </dependency>
    <dependency>
        <groupId>net.sf.json-lib</groupId>
        <artifactId>json-lib</artifactId>
        <version>2.4</version>
        <classifier>jdk15</classifier>
    </dependency>

2)配置文件

## 微信开放平台
wechat:
 open:
   # APPID
   appid: ******
   # APPSECRET
   appsecret: ******
   # 回调地址
   redirect_uri: http://de43f7.39nat.com

3)实体类

/**
*
* @author: xxm
* 功能描述: access_token封装基础类
* @date: 2021/2/2 10:41
*/
@Data
public class Token {
 private String openid; //授权用户唯一标识
 private String accessToken; //接口调用凭证
 private Integer ExpiresIn; //access_token接口调用凭证超时时间,单位(秒)
}
/**
*
* @author: xxm
* 功能描述: 微信与网站绑定关系表
* @date: 2021/2/1 17:08
*/
@Data
@Table(name = "UserWeChat")
@NameStyle(Style.normal)
public class UserWeChat implements Serializable {
   private static final long serialVersionUID = 8997358443007506192L;
   @Id
   @GeneratedValue(generator = "JDBC")
   private Integer id;
   //用户id
   private Integer userId;
   //微信OpenId
   private String openId;
   //昵称
   private String nickName;
}
/**
*
* @author: xxm
* 功能描述: access_token封装基础类
* @date: 2021/2/2 10:41
*/
@Data
public class Token {
 private String openid; //授权用户唯一标识
 private String accessToken; //接口调用凭证
 private Integer ExpiresIn; //access_token接口调用凭证超时时间,单位(秒)
}

4)微信工具类

/**
*
* @author: xxm
* 功能描述: 微信登录相关接口工具类
* @date: 2021/2/23 16:14
* @param: 
* @return: 
*/
public class WeChatCommonUtil {

   private static Logger log = LoggerFactory.getLogger(WeChatCommonUtil.class);
   /**
    * @author: xxm
    * 功能描述: urlEncodeUTF8工具类
    * (用于将扫描二维码后重定向的资源url进行编码)
    * @date: 2021/2/22 15:55
    * @param:
    * @return:
    */
   public static String urlEncodeUTF8(String source) {
   	String result = source;
   	try {
   		result = java.net.URLEncoder.encode(source, "utf-8");
   	} catch (UnsupportedEncodingException e) {
   		e.printStackTrace();
   	}
   	return result;
   }
   /**
    *
    * @author: xxm
    * 功能描述: 获取openid等信息的方法
    * @date: 2021/2/22 17:52
    * @param: 
    * @return: 
    */
   public static Token getTokenWithOpenid(String appid, String appsecret, String code) {
   	String findAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
   	Token token = null;
   	// 发起GET请求获取凭证
   	String requestUrl = findAccessTokenUrl.replace("APPID", appid).replace("SECRET", appsecret).replace("CODE", code);
   	JSONObject jsonObject = JSONObject.fromObject(httpsRequest(requestUrl, "GET", null));

   	if (null != jsonObject) {
   		try {
   			token = new Token();
   			token.setOpenid(jsonObject.getString("openid"));
   			token.setAccessToken(jsonObject.getString("access_token"));
   			token.setExpiresIn(jsonObject.getInt("expires_in"));
   		} catch (JSONException e) {
   			token = null;
   			// 获取token失败
   			log.error("获取token失败 errcode:{} errmsg:{}", jsonObject.getInt("errcode"), jsonObject.getString("errmsg"));
   		}
   	}
   	return token;
   }
   /**
    *
    * @author: xxm
    * 功能描述: 根据openid获取用户信息的方法
    * @date: 2021/2/22 17:52
    * @param: 
    * @return: 
    */
   public static WechatUserInfo getUserinfo(String access_token, String openid) {
   	WechatUserInfo wxuse = new WechatUserInfo();
   	String findUseinfo = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID";
   	String requestUrl = findUseinfo.replace("ACCESS_TOKEN", access_token).replace("OPENID", openid);
   	JSONObject jsonObject = JSONObject.fromObject(httpsRequest(requestUrl, "GET", null));
   	if (null != jsonObject) {
   		try {
   			wxuse.setNickname(jsonObject.getString("nickname"));
   			wxuse.setHeadimgurl(jsonObject.getString("headimgurl"));
   			wxuse.setUnionid(jsonObject.getString("unionid"));
   			wxuse.setOpenid(jsonObject.getString("openid"));
   		} catch (JSONException e) {
   			e.printStackTrace();
   		}
   	}
   	return wxuse;
   }
   /**
    * 发送https请求
    *
    * @param requestUrl 请求地址
    * @param requestMethod 请求方式(GET、POST)
    * @param outputStr 提交的数据
    * @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
    * 返回微信服务器响应的信息
    */
   public static String httpsRequest(String requestUrl, String requestMethod, String outputStr) {
   	try {
   		// 创建SSLContext对象,并使用我们指定的信任管理器初始化
   		TrustManager[] tm = { new MyX509TrustManager() };
   		SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
   		sslContext.init(null, tm, new java.security.SecureRandom());
   		// 从上述SSLContext对象中得到SSLSocketFactory对象
   		SSLSocketFactory ssf = sslContext.getSocketFactory();
   		URL url = new URL(requestUrl);
   		HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
   		conn.setSSLSocketFactory(ssf);
   		conn.setDoOutput(true);
   		conn.setDoInput(true);
   		conn.setUseCaches(false);
   		// 设置请求方式(GET/POST)
   		conn.setRequestMethod(requestMethod);
   		conn.setRequestProperty("content-type", "application/x-www-form-urlencoded");
   		// 当outputStr不为null时向输出流写数据
   		if (null != outputStr) {
   			OutputStream outputStream = conn.getOutputStream();
   			// 注意编码格式
   			outputStream.write(outputStr.getBytes("UTF-8"));
   			outputStream.close();
   		}
   		// 从输入流读取返回内容
   		InputStream inputStream = conn.getInputStream();
   		InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
   		BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
   		String str = null;
   		StringBuffer buffer = new StringBuffer();
   		while ((str = bufferedReader.readLine()) != null) {
   			buffer.append(str);
   		}
   		// 释放资源
   		bufferedReader.close();
   		inputStreamReader.close();
   		inputStream.close();
   		conn.disconnect();
   		return buffer.toString();
   	} catch (ConnectException ce) {
   		log.error("连接超时:{}", ce);
   	} catch (Exception e) {
   		log.error("https请求异常:{}", e);
   	}
   	return null;
   }
   
}
/**
* @author: xxm
* @description:信任管理器
* @date: 2021/2/22 17:41
*/
public class MyX509TrustManager implements X509TrustManager {

   // 检查客户端证书
   @Override
   public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
   }

   // 检查服务器端证书
   @Override
   public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
   }

   // 返回受信任的X509证书数组
   @Override
   public X509Certificate[] getAcceptedIssuers() {
       return null;
   }
}

2、请求获取Code

前提:应用已经获取相应的网页授权作用域(scope=snsapi_login)

开发:第三方网站引导用户打开链接

https://open.weixin.qq.com/connect/qrconnect?
appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

参数说明:
微信开放平台开发第三方授权登陆:微信扫码登录_第3张图片

注意:若提示“该链接无法访问”,请检查参数是否填写错误,如redirect_uri的域名与审核时填写的授权域名不一致或scope不为snsapi_login。

/**
    *
    * @author: xxm
    * 功能描述: 跳转至登录授权页面(页面出现二维码)
    * @date: 2021/2/22 17:50
    * @param: 
    * @return: 
    */
   @RequestMapping("/login")
   public String openWeChatLogin() {
       // 防止csrf攻击(跨站请求伪造攻击)
       String state = UUID.randomUUID().toString().replaceAll("-", "");
       String url = "https://open.weixin.qq.com/connect/qrconnect?" +
               "appid=" +
               env.getProperty("wechat.open.appid").trim() +
               "&redirect_uri=" +
               WeChatCommonUtil.urlEncodeUTF8(env.getProperty("wechat.open.redirect_uri").trim()+"/wechat/weChatLogin_epf") +
               "&response_type=code" +
               "&scope=snsapi_login" +
               // 由后台自动生成
               "&state=" + state +
               "#wechat_redirect";

       return "redirect:" + url;
   }

3、用户同意授权与否

用户允许授权后,将会重定向到redirect_uri的网址上,并且带上code和state参数

redirect_uri?code=CODE&state=STATE

若用户禁止授权,则不会重定向到我们提供的回调地址中

成功授权后,将获得Code,通过Code可以获取access_token

4、获取access_token

通过上述方法获取的code获取access_token.

Http Get请求

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

参数说明:
微信开放平台开发第三方授权登陆:微信扫码登录_第4张图片

请求后,

返回成功的json串为(样例):

{
“access_token”:“ACCESS_TOKEN”, // 接口调用凭证
“expires_in”:7200, // access_token接口调用凭证超时时间,单位(秒)
“refresh_token”:“REFRESH_TOKEN”, //用户刷新access_token
“openid”:“OPENID”, //授权用户唯一标识
“scope”:“SCOPE”, //用户授权的作用域,使用逗号(,)分隔
“unionid”: “o6_bmasdasdsad6_2sgVt7hMZOPfL” //当且仅当该网站应用已获得该用户的userinfo授权时,才会出现该字段
}

失败返回的样例:

{“errcode”:40029,“errmsg”:“invalid code”}

失败可能原因: 暂时不明

Token tokenWithOpenid = WeChatCommonUtil.getTokenWithOpenid(loginAppid, loginSecrect, code);

5、通过access_token调用接口获取用户个人信息(UnionID机制)

前提:

  1. access_token有效且未超时;

  2. 微信用户已授权给第三方应用帐号相应接口作用域(scope)。

Http Get请求:

https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID

参数
微信开放平台开发第三方授权登陆:微信扫码登录_第5张图片

返回成功的json结果(样例):

{
“openid”:“OPENID”, //普通用户的标识,对当前开发者帐号唯一
“nickname”:“NICKNAME”, //普通用户昵称
“sex”:1, //普通用户性别,1为男性,2为女性
“province”:“PROVINCE”, //普通用户个人资料填写的省份
“city”:“CITY”, //普通用户个人资料填写的城市
“country”:“COUNTRY”, //国家,如中国为CN
“headimgurl”: “http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/0”, //用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空
“privilege”:[ //用户特权信息,json数组,如微信沃卡用户为(chinaunicom)
“PRIVILEGE1”,
“PRIVILEGE2”
],
“unionid”: " o6_bmasdasdsad6_2sgVt7hMZOPfL" //用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的
}

失败JSON样例:

{“errcode”:40003,“errmsg”:“invalid openid”}

注意:在用户修改微信头像后,旧的微信头像URL将会失效,因此开发者应该自己在获取用户信息后,将头像图片保存下来,避免微信头像URL失效后的异常情况

最好保存用户unionID信息,以便以后在不同应用中进行用户信息互通。

  //通过access_token调用接口
  WechatUserInfo wxuse = WeChatCommonUtil.getUserinfo(access_token, openid);

6、授权成功后台处理

获取到access_token,在通过access_token获取到微信用户信息WechatUserInfo,就可以判断微信账号是否已经与网站账号关联 ,如果关联了,则此处可以直接登录跳转到主页,但在此处,我还是用了Spring Security安全框架,所有需要微信登录需要经过Spring Security的url请求进行验证,才可以登录。
如果微信账号没有关联网站账号,则可以跳转到微信绑定页面,完成微信绑定。

  /**
     *
     * @author: xxm
     * 功能描述: 授权成功后
     * @date: 2021/2/22 17:50
     * @param:
     * @return:
     */
    @ResponseBody
    @RequestMapping("/weChatLogin_epf")
    public ModelAndView weChatLogin_epf(HttpServletRequest request, HttpSession session, RedirectAttributes attribute){
        ModelAndView model = new ModelAndView();
        //获取到code,这个code应该是微信那边定义的
        String code = request.getParameter("code");
        //第三方网站(即我们自己)自定义的参数,可以存储一些重要信息和防伪信息,因为这个接口是完全暴露的,所以必须要有防伪措施,防止恶意请求来搞事
        String state=request.getParameter("state");
        //如果这两个字段都为空值,就可以判定为而已请求
        if(StringUtils.isBlank(code)||StringUtils.isBlank(state)){
            logger.info("非法请求,缺少必要的参数");
            model.setViewName("redirect:/login");
            attribute.addFlashAttribute("errorInfo", "非法请求,缺少必要的参数!");
            attribute.addFlashAttribute("success", false);
            return model;
        }
        logger.info("获取到的code是 :" + code+",state="+state);
        //通过code获取access_token
        String loginAppid = env.getProperty("wechat.open.appid").trim();
        String loginSecrect = env.getProperty("wechat.open.appsecret").trim();
        try {
            Token tokenWithOpenid = WeChatCommonUtil.getTokenWithOpenid(loginAppid, loginSecrect, code);
            if (null != tokenWithOpenid) {
                String openid = tokenWithOpenid.getOpenid();
                String access_token = tokenWithOpenid.getAccessToken();
                //通过access_token调用接口
                WechatUserInfo wxuse = WeChatCommonUtil.getUserinfo(access_token, openid);
                logger.info("微信用户信息:" + wxuse);

                UserWeChatDto uwcDto = new UserWeChatDto();
                uwcDto.setOpenId(openid);
                List<UserWeChatDto> uwcList = userWeChatClient.getListByParam(uwcDto);
                if (null != uwcList && uwcList.size()==1) {
                    UserWeChatDto userWeChatDto = uwcList.get(0);
                    //微信账号已经与网站账号关联
                    //根据用户id查询用户
                    SysUser currentUser = userControllerClient.getUserById(userWeChatDto.getUserId());
                    session.setAttribute("username", currentUser.getUsername());
                    // 微信登录需要经过的url请求
                    model.setViewName("redirect:/wechat/weChatLogin");
                    model.addObject("openid",openid);
                    attribute.addFlashAttribute("success", true);
                } else {
                    //微信账号没有关联网站账号
                    String url = "redirect:/weChatBind";
                    model.setViewName(url);
                    attribute.addFlashAttribute("wechatUserInfo", wxuse);
                    attribute.addFlashAttribute("nickname", wxuse.getNickname());
                    attribute.addFlashAttribute("openid", wxuse.getOpenid());
                }

            } else {
                logger.error("获取token失败");
                model.setViewName("redirect:/login");
                attribute.addFlashAttribute("errorInfo", "获取token失败!");
                attribute.addFlashAttribute("success", false);
            }
        }catch (Exception e) {
            e.printStackTrace();
            model.setViewName("redirect:/login");
            attribute.addFlashAttribute("errorInfo", "微信授权登录失败!");
            attribute.addFlashAttribute("success", false);
        }finally {
            return model;
        }
    }

7、刷新access_token

由于access_token有效期(目前为2个小时)较短,当access_token超时后,可以使用refresh_token进行刷新,access_token刷新结果有两种:

  1. 若access_token已超时,那么进行refresh_token会获取一个新的access_token,新的超时时间;
  2. 若access_token未超时,那么进行refresh_token不会改变access_token,但超时时间会刷新,相当于续期access_token。

refresh_token拥有较长的有效期(30天),当refresh_token失效的后,需要用户重新授权。

请求方法:

获取第一步的code后,请求以下链接进行refresh_token:

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

参数说明:
微信开放平台开发第三方授权登陆:微信扫码登录_第6张图片
成功返回的结果:
{
“access_token”:“ACCESS_TOKEN”, //接口调用凭证
“expires_in”:7200, // access_token接口调用凭证超时时间,单位(秒)
“refresh_token”:“REFRESH_TOKEN”, //用户刷新access_token
“openid”:“OPENID”, //授权用户唯一标识
“scope”:“SCOPE” //用户授权的作用域,使用逗号(,)分隔
}

失败样例:

{“errcode”:40030,“errmsg”:“invalid refresh_token”}

注意:

1、Appsecret 是应用接口使用密钥,泄漏后将可能导致应用数据泄漏、应用的用户数据泄漏等高风险后果;存储在客户端,极有可能被恶意窃取(如反编译获取Appsecret);
2、access_token 为用户授权第三方应用发起接口调用的凭证(相当于用户登录态),存储在客户端,可能出现恶意获取access_token 后导致的用户数据泄漏、用户微信相关接口功能被恶意发起等行为;
3、refresh_token 为用户授权第三方应用的长效凭证,仅用于刷新access_token,但泄漏后相当于access_token 泄漏,风险同上。

建议将secret、用户数据(如access_token)放在App云端服务器,由云端中转接口调用请求。

五、测试结果

请自己测试

你可能感兴趣的:(微信登录,java,java,openid)