➡️ » 第三方授权 -- OAuth 2.0
By chaochao on 02 Jul 2022

第三方授权

OAuth 2.0 了解

http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html

下面是系统内获取微信授权令牌accessToke的代码:

OAuth2.0授权的基本流程:
1、用户打开客户端以后,客户端要求用户给予授权。
2、用户同意给予客户端授权。
3、客户端使用上一步获得的授权,认证服务器申请令牌。
4、认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
5、客户端使用令牌,向资源服务器申请获取资源。
6、资源服务器确认令牌无误,同意向客户端开放资源。
上面的用户授权其实就是表现在第三方登录时的用户在QQ、或者微博提供的授权页面进行登录并勾选授权上。
像qq、微博、微信之类的授权一般都是第一次授权之后便在以后每次登陆中默认同意授权,除非另外在其提供的授权管理入口处取消对某些网站的授权之后,才会要求另外授权。

OAuth2.0提供的客户端授权模式包括:
1、授权码模式(authorization code)。
2、简化模式(implicit)。
3、密码模式(resource owner password credentials)。
4、客户端模式(client credentials)。
一般像我们常用的主要是授权码模式,包括QQ、微信、微博等一些主要的社交平台所提供的用户信息接口都是采用OAuth2.0的授权码模式进行用户资源授权。

OAuth2.0授权码模式授权简介:
1、用户访问客户端,后者将前者导向认证服务器。
2、用户选择是否给予客户端授权。
3、假设用户给予授权,认证服务器将用户导向客户端事先指定的”重定向URI”(redirection URI),同时附上一个授权码。
4、客户端收到授权码,附上早先的”重定向URI”,向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
5、认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

授权码模式授权流程图 oauth_2_0简单授权码流程图

针对第1、 2步,引导用户进入授权页面的话,以系统中现有的QQ、weibo、wechat授权登录为例,主要是通过携带参数跳转至第三方应用提供的授权服务页面进行一个用户的授权确认:

https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101186927&redirect_uri=http://www.alltuu.com/uqqlogin&scope=get_user_info

https://open.weixin.qq.com/connect/qrconnect?appid=wxd07aba8cc284db8f&redirect_uri=http%3a%2f%2fwww.alltuu.com%2fuwxlogin&response_type=code&scope=snsapi_login#wechat_redirect

https://api.weibo.com/oauth2/authorize?client_id=1346240843&response_type=code&redirect_uri=http://www.alltuu.com/uwblogin

以上携带的参数中包括:授权返回值类型response_type、应用标识appid、回调地址redirect_uri、请求资源类型scope。
当用户去人授权时,第三方应用的授权服务器会根据之前提供的回调地址redirect_uri发送回调请求,并携带授权码返回。一般这个授权码根据授权操作包含过期时间、请求资源等信息。
那么我们的服务器拿到这个授权码之后,便可以携带授权码到第三方用户提供的令牌授权地址去请求授权令牌了,拿到这个授权令牌也就等于这次的OAuth授权完成了,可以根据这个授权按令牌在一个有效时间内调用相关的接口获取用户资源了。

下面是系统内获取微信授权令牌accessToke的代码:

public Map<String,String> getWXAccessToken(String code, Integer type) {
		Map<String,String> map = new HashMap<String,String>();
		String urlStr = Constants.BASE_WX_AUTH_TOKEN_URL;
		if (Constants.WX_BY_GZ.equals(type)) {
			urlStr += "appid=" + Constants.WX_GZ_APP_ID +
					"&secret=" + Constants.WX_GZ_APP_SECRET;
		} else if (Constants.WX_BY_KF.equals(type)) {
			urlStr += "appid=" + Constants.WX_KF_WEB_APP_ID +
					"&secret=" + Constants.WX_KF_WEB_APP_SECRET;
		} else if (Constants.WX_BY_APP.equals(type)) {
			urlStr += "appid=" + Constants.WX_KF_APP_APP_ID +
					"&secret=" + Constants.WX_KF_APP_APP_SECRET;
		}
		urlStr += "&code=" + code + "&grant_type=" + Constants.WX_GRANT_TYPE;
		String result = HttpUtils.doRequest(urlStr);
		logger.info(result);
//		JSONObject jo = null;
//		try {
//			jo = new JSONObject(result);
//		} catch (JSONException e) {
//			e.printStackTrace();
//		}
		JSONObject jo = JSONObject.parseObject(result);
		String accesstoken = null;
		String openId = null;
		if(jo != null){
			accesstoken = jo.getString("access_token");
			openId = jo.getString("openid");
			logger.info("access_token:" + accesstoken + ",openid:" + openId + "。");
		}
		map.put("accesstoken", accesstoken);
		map.put("openId", openId);
		return map;
	}

根据accessToken调用接口获取微信用户基本信息:

public WXUserDTO getWXUserInfo(String accessToken, String openId){
		String urlStr = Constants.BASE_WX_AUTH_INFO_URL + "access_token=" + accessToken +
				"&openid=" + openId + "&lang=zh_CN";
		String result = HttpUtils.doRequest(urlStr);
		WXUserDTO userInfo = null;
		if(result != null){
			logger.info(result);
			userInfo = JSON.parseObject(result, WXUserDTO.class);
			logger.info(userInfo.getUnionid());
		}
		return userInfo;
	}

获取到的用户基本信息包括以下:

  //用户的唯一标识
	private String openid;
	//用户昵称
	private String nickname;
	//用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
	private String sex;
	//省份
	private String province;
	//城市
	private String city;
	//国家
	private String country;
	//用户头像
	private String headimgurl;
	//用户特权信息
	private String privilege;
	//多平台对应的同一个用户
	private String unionid;

调用第三方授权接口除了需要了解相关授权协议外,还需要特别注意第三方接口的调用方式(是否需要uri编码、参数的大小写、参数的提交方式等)。仔细阅读理解官网API文档是完成三方支付的关键。

第三方支付引入

第三方支付主要是通过支付接口

微信H5网页支付

微信H5网页支付时序图 微信H5网页支付时序图

以微信支付为例,首先通过调用我们后台接口生成支付参数,然后再将后台返回的拼接请求参数传回给前台页面。页面继续调用微信客户端提供的sdk来唤起支付操作。

拼接微信支付请求参数:

public Map<Object,Object> buildWecharPaym(PsOrder order, Integer userId, String userIp, String code, String url) {
		//生成签名拼接的package,由于前台js穿参数时需要的时间戳参数需要全部小写,但是后台生成签名时间戳参数中的s又要求大写
		//这里分成两个参数map,后面接触微信支付的同学注意及时看官方文档,好像这个参数大小写官方文档说还会再调整
//		SortedMap<String, String> signPackage = new TreeMap<String, String>();
		//获取prepay_id后,拼接最后请求支付所需要的package,及前台获取的数据
		SortedMap<String, Object> finalpackage = new TreeMap<String, Object>();
		Integer errorCode = ErrorCodes.SUCCESS;
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		PsUser user = psUserDao.findRecordByProperty(PsUser.FIELD_ID, userId);
		if (user != null) {
			//如果code不为空,说明微信用户还没有对改商户应用进行授权,需要得到用户的openId进行记录
			if (code != null && !"".equals(code)) {
				logger.info("code -->:" + code);
				Map<String,String> access = psUserService.getWXAccessToken(code, Constants.WX_BY_GZ);
				logger.info("openId -->:" + JSONObject.toJSONString(access));
				String wxOpenId = access.get("openId");
				if (wxOpenId != null) {
					user.setWxOpenId(wxOpenId);
					user = psUserDao.update(user);
				} else {
					logger.info("授权登陆失败!");
				}
			}
			String wxOpenId = user.getWxOpenId();  //获取微信用户对应公众号的唯一标识
			logger.info("wxOpenId-->:" + wxOpenId + "userIp -->:" + userIp);
			String noncestr = WXUtil.getNonceStr();  //获取随机字符串
			String attach = String.valueOf(userId);  // 附加数据 原样返回
			logger.info("precost -->:" + order.getCost());
			Integer cost =  (int)(order.getCost() * 100);	//订单总金额
			logger.info("finalcost -->:" + cost);

			//用以生成签名的map数组
			SortedMap<String, Object> packageParams = new TreeMap<String, Object>();
			packageParams.put("appid", Constants.WX_GZ_APP_ID);
			packageParams.put("mch_id", Constants.MCH_ID);
			packageParams.put("nonce_str", noncestr);
//			packageParams.put("body", order.getProNames());
			packageParams.put("body", "Reward By Alltuu");
			packageParams.put("attach", attach);
			packageParams.put("out_trade_no", String.valueOf(order.getId()) + sdf.format(new Date()));  //订单号
			packageParams.put("total_fee", String.valueOf(cost));  //商品金额,以分为单位
			packageParams.put("spbill_create_ip", "123.12.12.123");
			packageParams.put("notify_url", Constants.WXM_DEFAULT_DNOTIFY_URL);
			packageParams.put("trade_type", Constants.WX_JSAPI_PAY);
			packageParams.put("openid", wxOpenId);

			RequestHandler reqHandler = new RequestHandler(null, null);
			reqHandler.init(Constants.WX_GZ_APP_ID, Constants.WX_GZ_APP_SECRET, Constants.WX_API_KEY);

			String sign = reqHandler.createSign(packageParams);  //生成获取预支付签名
			logger.info(reqHandler.getKey());
			packageParams.put("sign", sign);
			String requestXML = reqHandler.getRequestXml(packageParams);  //将参数转换成xml,根节点为<xml>
			//获取prepay_id
			String prepay_id = new GetWxOrderno().getPayNo(Constants.UNIFIED_ORDER_URL, requestXML);
			logger.info("prepay_id-->:" +prepay_id);

			//拼接支付接入的参数
			if (prepay_id != null && prepay_id != "") {
				String timestamp = Sha1Util.getTimeStamp();
				String packages = "prepay_id="+prepay_id;
				finalpackage.put("appId", Constants.WX_GZ_APP_ID);
				finalpackage.put("timeStamp", timestamp);
				finalpackage.put("nonceStr", noncestr);
				finalpackage.put("package", packages);
				finalpackage.put("signType", "MD5");
				String finalsign = reqHandler.createSign(finalpackage);  //重新生成签名
				finalpackage.put("paySign", finalsign);
				logger.info("appId -->:" + Constants.WX_GZ_APP_ID + ", timeStamp -->:" + timestamp + ", nonceStr -->:" + noncestr + ", package -->:" + packages + ", signType -->:MD5, paySign -->:" + finalsign);
			} else {
				errorCode = ErrorCodes.PREPAYID_NOT_FOUND;
			}
		} else {
			errorCode = ErrorCodes.LOGIN_FAILED;
		}
		//微信预支付订单完成,传回参数前台接入微信最后支付请求
		Map<Object,Object> resInfo = new HashMap<Object, Object>();
		resInfo.put("errorCode", errorCode);
		resInfo.put("lists", finalpackage);
		return resInfo;
	}

OSS文件直传实践

OSS文件直传实践流程图 OSS文件直传实践时序图

oss直传参数获取:

public Integer uploadExampleByRecruit(String photoIds, Integer userId, Map<String,Object> retMap) {
		 Integer errorCode = ErrorCodes.SUCCESS;
		 //记录每张图片对应的数据库id
		 Map<String, String> imgIdMap = new HashMap<String, String>();
		 //记录每张图片对应的ossURL
		 Map<String, String> imgUrlMap = new HashMap<String, String>();
	     //这里的fileId是前台穿回来的针对每张图片的唯一标识
		 String[] idStrs = photoIds.split(",");
		 if (idStrs.length > 0) {
			 ImageClient client = new OSSImageClient();
			 for (int i = 0; i < idStrs.length; i++) {
				 AtRecruitExample example = new AtRecruitExample(userId);
				 example = atRecruitExampleDao.update(example);
				 if (example == null) {
						errorCode = ErrorCodes.DB_INSERT_ERROR;
						break;
					} else {
						imgIdMap.put(idStrs[i], example.getId().toString());
						//提前拿到图片获取的url,这里图片缩略图还没有上传到oss,只是将原图上传,之后回调时到oss下载图片裁剪之后再上传到此路径。
						String url = client.getPublicImage(OSSImageClient.URL_PUBLIC,
								OSSKeyGen.getPhotoKeyOfRecruitExample(userId, example.getId()), OSSImageClient.STYLE_PHOTO_UPLOAD_FREE);
						imgUrlMap.put(idStrs[i], url);
					}
			 }
			//摄影师oss操作域
			String dir = OSSKeyGen.getPhotokeyOfProductExampleByDirect(userId);
			DirectUpload upload = new OSSDirectUpload();
			Map<String, Object> OSSParams = upload.getUploadParams(dir, OSSDirectUpload.BUCKET_PUBLIC, dir, "uexnotify");
			retMap.put("imgIds", imgIdMap);
			retMap.put("imgUrls", imgUrlMap);
			retMap.put("OSSParams", OSSParams);
		 } else {
			 errorCode = ErrorCodes.UPLOAD_NO_FILE;
		 }
		 return errorCode;
	 }

关键参数拼接:

public Map<String, Object> getUploadParams(String dir, String bucket, String key, String notifyUrl) {
		long expireTime = 300;
		long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
		String host = "http://" + bucket + "." + endPoint;
		Date expiration = new Date(expireEndTime);
		PolicyConditions policyConds = new PolicyConditions();
		Map<String, Object> paramMap = new LinkedHashMap<String, Object>();
//		String OSSParams = null;
		 try {
        	 policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
             policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

             String postPolicy = client.generatePostPolicy(expiration, policyConds);
             byte[] binaryData = postPolicy.getBytes("utf-8");
             String encodedPolicy = BinaryUtil.toBase64String(binaryData);
             String postSignature = client.calculatePostSignature(postPolicy);

             paramMap.put("accessid", OSS_ID);
             paramMap.put("key", key);
             paramMap.put("policy", encodedPolicy);
             paramMap.put("signature", postSignature);
             paramMap.put("dir", dir);
             paramMap.put("host", host);
             paramMap.put("expire", String.valueOf(expireEndTime / 1000));
             Map<String, String> callbackParam = new LinkedHashMap<String, String>();
             callbackParam.put("callbackUrl", "www.alltuu.com/" + notifyUrl);
             callbackParam.put("callbackHost", "www.alltuu.com");
             callbackParam.put("callbackBody", "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
             callbackParam.put("callbackBodyType", "application/x-www-form-urlencoded");
             String callbackStr = JSONObject.toJSONString(callbackParam);
             String callback = BinaryUtil.toBase64String(callbackStr.getBytes("utf-8"));
             paramMap.put("callback", callback);
//          OSSParams = JSONObject.toJSONString(paramMap);
        } catch (Exception e) {
        	Assert.fail(e.getMessage());
        }
		 logger.info(JSONObject.toJSONString(paramMap));
		return paramMap;
	}

未完待续…

感谢