onenet推送消息到本应用,接收参数并设置参数

This commit is contained in:
邱贞招 2023-11-21 16:35:16 +08:00
parent f95130aa1c
commit a11875bad1
9 changed files with 417 additions and 72 deletions

View File

@ -12,19 +12,12 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64;
import java.util.List; import java.util.List;
/** /**
@ -170,64 +163,4 @@ public class CommonController
} }
} }
/**
* 功能描述 消息摘要计算
*
* @param msg 平台推送消息
* @param signature 根据token生成的签名
* @param nonce 平台生成的随机字符串
* @return
* @throws Exception
*/
@RequestMapping("/signature")
public String signature(String msg, String nonce, String signature){
log.info("接收到参数msg="+msg+",token="+token+",nonce="+nonce+",signature="+signature);
if(StringUtils.isNotEmpty(msg)){
// 第一步 计算 MD5 并编码为 Base64 字符串
String calculatedSignature = calculateBase64MD5(token + nonce + msg);
// 第二步 进行 URL 解码
calculatedSignature = urlDecode(calculatedSignature);
// 第三步 比较计算得到的签名与请求参数中的签名是否相等
if (calculatedSignature.equals(signature)) {
System.out.println("Token verification successful!");
} else {
System.out.println("Token verification failed!");
}
// 第4步 将URL Decode编码后的值与请求参数signature的值进行对比
if (StringUtils.isNotEmpty(signature) && signature.equals(calculatedSignature)) {
log.info("签名验证正确返回msg=【{}】",msg);
return msg;
}
log.info("加密后的base64:【{}】返回invalid token",calculatedSignature);
return "invalid token";
}else{
return msg;
}
}
// 计算 MD5 并编码为 Base64 字符串
private static String calculateBase64MD5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] md5Bytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
// 编码为 Base64 字符串
return Base64.getEncoder().encodeToString(md5Bytes);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// URL 解码
private static String urlDecode(String input) {
try {
return URLDecoder.decode(input, StandardCharsets.UTF_8.toString());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
} }

View File

@ -6,9 +6,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://localhost:3306/autosprout?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://117.50.163.143:3306/autosprout?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root username: autosprout
password: 123456 password: 2fT5hHLbj8Nis6fD
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭

View File

@ -133,6 +133,24 @@
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.10</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20160212</version>
</dependency>
<!-- mybatis-plus 增强CRUD --> <!-- mybatis-plus 增强CRUD -->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>

View File

@ -111,7 +111,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
// 过滤请求 // 过滤请求
.authorizeRequests() .authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问 // 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage","/common/signature").permitAll() .antMatchers("/login", "/register", "/captchaImage","/common/receive").permitAll()
// 静态资源可匿名访问 // 静态资源可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()

View File

@ -13,5 +13,11 @@ public class IotLogTask {
public void getDeviceLog() public void getDeviceLog()
{ {
System.out.println("获取浇花器日志"); System.out.println("获取浇花器日志");
/**1.获取到浇花器日志*/
/**2.根据最新时间保存数据*/
/**3.*/
} }
} }

View File

@ -0,0 +1,29 @@
package com.ruoyi.device.iot.domain;
import lombok.Data;
/**
* onenet接收到的body对象
*
* @author ruoyi
*/
@Data
public class BodyObj {
/** 设备推送数据,包括设备的生命周期,数据点,物模型属性、事件、服务等 */
private Object msg;
/** 用于计算签名字符的随机串 */
private String nonce;
/** 加密签名,用以校验推送客户端身份合法性,校验方法见实例验证 */
private String signature;
/** 推送时间戳(毫秒) */
private Long time;
/** 消息ID */
private String id;
}

View File

@ -0,0 +1,31 @@
package com.ruoyi.device.iot.domain;
import lombok.Data;
/**
* 数据点数据
*
* @author ruoyi
*/
@Data
public class DataPonit {
/** 设备名称 */
private String dev_name;
/** 设备上报的时间戳 */
private Long at;
/** 产品id */
private String pid;
/** 固定值1 */
private Integer type;
/** 数据点id */
private String ds_id;
/** 消息值 */
private String value;
}

View File

@ -0,0 +1,187 @@
package com.ruoyi.device.iot.receive;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.device.iot.domain.BodyObj;
import com.ruoyi.device.iot.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.io.UnsupportedEncodingException;
/**
* 接收硬件参数
*
* @author ruoyi
*/
@RestController
@RequestMapping("/common")
public class ReceiveController {
private static final Logger log = LoggerFactory.getLogger(ReceiveController.class);
@Value(value = "${watering.token}")
private String token;
/**
* 功能描述第三方平台数据接收<p>
* <ul>:
* <li>1.OneNet平台为了保证数据不丢失有重发机制如果重复数据对业务有影响数据接收端需要对重复数据进行排除重复处理</li>
* <li>2.OneNet每一次post数据请求后等待客户端的响应都设有时限在规定时限内没有收到响应会认为发送失败
* 接收程序接收到数据时尽量先缓存起来再做业务逻辑处理</li>
* </ul>
* @param body 数据消息
* @return 任意字符串OneNet平台接收到http 200的响应才会认为数据推送成功否则会重发
*/
@RequestMapping(value = "/receive",method = RequestMethod.POST)
@ResponseBody
public String receive(@RequestBody String body){
log.info("receive方法接收到参数: body String --- " +body);
/************************************************
* 解析数据推送请求非加密模式
* 如果是明文模式使用以下代码
**************************************************/
/*************明文模式 start****************/
BodyObj obj = Util.resolveBody(body, false);
log.info("receive方法解析对象: body Object --- " + JSON.toJSONString(obj));
if (obj != null){
boolean dataRight = Util.checkSignature(obj, token);
if (dataRight){
log.info("receive方法验证签名正确: content" + JSON.toJSONString(obj));
Object msg = obj.getMsg();
/**TODO 接收到msg 更新设备参数*/
// msg.
log.info("receive方法-获取到消息体: msg---" + JSON.toJSONString(msg));
}else {
log.info("receive方法验证签名错误: signature error");
}
}else {
log.info("receive方法参数为空: body empty error");
}
/*************明文模式 end****************/
/********************************************************
* 解析数据推送请求加密模式
*
* 如果是加密模式使用以下代码
********************************************************/
/*************加密模式 start****************/
// Util.BodyObj obj1 = Util.resolveBody(body, true);
// logger.info("data receive: body Object--- " +obj1);
// if (obj1 != null){
// boolean dataRight1 = Util.checkSignature(obj1, token);
// if (dataRight1){
// String msg = Util.decryptMsg(obj1, aeskey);
// logger.info("data receive: content" + msg);
// }else {
// logger.info("data receive: signature error " );
// }
// }else {
// logger.info("data receive: body empty error" );
// }
/*************加密模式 end****************/
return "ok";
}
/**
* 功能说明 URL&Token验证接口如果验证成功返回msg的值否则返回其他值
* @param msg 验证消息
* @param nonce 随机串
* @param signature 签名
* @return msg值
*/
@RequestMapping(value = "/receive", method = RequestMethod.GET)
@ResponseBody
public String check(@RequestParam(value = "msg") String msg,
@RequestParam(value = "nonce") String nonce,
@RequestParam(value = "signature") String signature) throws UnsupportedEncodingException {
log.info("check方法接收到参数: msg:{} nonce{} signature:{}",msg,nonce,signature);
if (Util.checkToken(msg,nonce,signature,token)){
return msg;
}else {
return "error";
}
}
// public static void main(String[] args) throws Exception {
// SpringApplication.run(ReceiverDemo.class, args);
// }
// /**
// * 功能描述 消息摘要计算
// *
//// * @param requestData 平台推送消息
// * @return
// * @throws Exception
// */
// @RequestMapping("/signature")
// public String signature(@RequestParam String nonce,@RequestParam String msg, @RequestParam String signature){// @RequestBody String msg, String nonce, String signature
//// log.info("接收到参数requestData=【{}】", JSON.toJSON(requestData));
// log.info("接收到参数msg="+msg+",token="+token+",nonce="+nonce+",signature="+signature);
//// log.info("接收到参数requestData=【{}】", requestData);
//
//// String signature = requestData.getSignature();
//// String msg = requestData.getMsg();
//// if(Objects.nonNull(requestData)){
// if(StringUtils.isNotEmpty(msg)){
// // 第一步 计算 MD5 并编码为 Base64 字符串
// String calculatedSignature = calculateBase64MD5(token + nonce + msg);
//
// // 第二步 进行 URL 解码
// calculatedSignature = urlDecode(calculatedSignature);
//
// // 第三步 比较计算得到的签名与请求参数中的签名是否相等
// if (calculatedSignature.equals(signature)) {
// System.out.println("Token verification successful!");
// } else {
// System.out.println("Token verification failed!");
// }
//
// // 第4步 将URL Decode编码后的值与请求参数signature的值进行对比
// if (StringUtils.isNotEmpty(signature) && signature.equals(calculatedSignature)) {
// log.info("签名验证正确返回msg=【{}】",msg);
//
// /** TODO 处理msg
// *
// * */
// return msg;
// }
// log.info("加密后的base64:【{}】返回invalid token",calculatedSignature);
// return "invalid token";
// }else{
// log.info("msg,nonce为空返回msg=【{}】",msg);
// return msg;
// }
// }
//
// // 计算 MD5 并编码为 Base64 字符串
// private static String calculateBase64MD5(String input) {
// try {
// MessageDigest md = MessageDigest.getInstance("MD5");
// byte[] md5Bytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
// // 编码为 Base64 字符串
// return Base64.getEncoder().encodeToString(md5Bytes);
// } catch (Exception e) {
// e.printStackTrace();
// return null;
// }
// }
//
// // URL 解码
// private static String urlDecode(String input) {
// try {
// return URLDecoder.decode(input, StandardCharsets.UTF_8.toString());
// } catch (Exception e) {
// e.printStackTrace();
// return null;
// }
// }
}

View File

@ -0,0 +1,141 @@
package com.ruoyi.device.iot.util;
import com.ruoyi.device.iot.domain.BodyObj;
import org.apache.commons.codec.binary.Base64;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.*;
/**
* 功能描述: OneNet数据推送接收程序工具类
*
* Created by Roy on 2017/5/17.
*
*/
public class Util {
private static Logger logger = LoggerFactory.getLogger(Util.class);
private static MessageDigest mdInst;
static {
try {
mdInst = MessageDigest.getInstance("MD5");
Security.addProvider(new BouncyCastleProvider());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
/**
* 功能描述:在OneNet平台配置数据接收地址时平台会发送URL&token验证请求<p>
* 使用此功能函数验证token
* @param msg 请求参数 <msg>的值
* @param nonce 请求参数 <nonce>的值
* @param signature 请求参数 <signature>的值
* @param token OneNet平台配置页面token的值
* @return token检验成功返回truetoken校验失败返回false
*/
public static boolean checkToken(String msg,String nonce,String signature, String token) throws UnsupportedEncodingException {
byte[] paramB = new byte[token.length() + 8 + msg.length()];
System.arraycopy(token.getBytes(), 0, paramB, 0, token.length());
System.arraycopy(nonce.getBytes(), 0, paramB, token.length(), 8);
System.arraycopy(msg.getBytes(), 0, paramB, token.length() + 8, msg.length());
String sig = com.sun.org.apache.xerces.internal.impl.dv.util.Base64.encode(mdInst.digest(paramB));
logger.info("url&token validation: result {}, detail receive:{} calculate:{}", sig.equals(signature.replace(' ','+')),signature,sig);
return sig.equals(signature.replace(' ','+'));
}
/**
* 功能描述: 检查接收数据的信息摘要是否正确<p>
* 方法非线程安全
* @param obj 消息体对象
* @param token OneNet平台配置页面token的值
* @return
*/
public static boolean checkSignature(BodyObj obj, String token) {
//计算接受到的消息的摘要
//token长度 + 8B随机字符串长度 + 消息长度
byte[] signature = new byte[token.length() + 8 + obj.getMsg().toString().length()];
System.arraycopy(token.getBytes(), 0, signature, 0, token.length());
System.arraycopy(obj.getNonce().getBytes(), 0, signature, token.length(), 8);
System.arraycopy(obj.getMsg().toString().getBytes(), 0, signature, token.length() + 8, obj.getMsg().toString().length());
String calSig = Base64.encodeBase64String(mdInst.digest(signature));
logger.info("check signature: result:{} receive sig:{},calculate sig: {}",calSig.equals(obj.getSignature()),obj.getSignature(),calSig);
return calSig.equals(obj.getSignature());
}
/**
* 功能描述 解密消息
* @param obj 消息体对象
* @param encodeKey OneNet平台第三方平台配置页面为用户生成的AES的BASE64编码格式秘钥
* @return
* @throws NoSuchPaddingException
* @throws NoSuchAlgorithmException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
*/
public static String decryptMsg(BodyObj obj, String encodeKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
byte[] encMsg = Base64.decodeBase64(obj.getMsg().toString());
byte[] aeskey = Base64.decodeBase64(encodeKey + "=");
SecretKey secretKey = new SecretKeySpec(aeskey, 0, 32, "AES");
Cipher cipher = null;
cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(aeskey, 0, 16));
byte[] allmsg = cipher.doFinal(encMsg);
byte[] msgLenBytes = new byte[4];
System.arraycopy(allmsg, 16, msgLenBytes, 0, 4);
int msgLen = getMsgLen(msgLenBytes);
byte[] msg = new byte[msgLen];
System.arraycopy(allmsg, 20, msg, 0, msgLen);
return new String(msg);
}
/**
* 功能描述 解析数据推送请求生成code>BodyObj</code>消息对象
* @param body 数据推送请求body部分
* @param encrypted 表征是否为加密消息
* @return 生成的<code>BodyObj</code>消息对象
*/
public static BodyObj resolveBody(String body, boolean encrypted) {
JSONObject jsonMsg = new JSONObject(body);
BodyObj obj = new BodyObj();
obj.setNonce(jsonMsg.getString("nonce"));
obj.setSignature(jsonMsg.getString("signature"));
if (encrypted) {
if (!jsonMsg.has("enc_msg")) {
return null;
}
obj.setMsg(jsonMsg.getString("enc_msg"));
} else {
if (!jsonMsg.has("msg")) {
return null;
}
obj.setMsg(jsonMsg.get("msg"));
}
return obj;
}
private static int getMsgLen(byte[] arrays) {
int len = 0;
len += (arrays[0] & 0xFF) << 24;
len += (arrays[1] & 0xFF) << 16;
len += (arrays[2] & 0xFF) << 8;
len += (arrays[3] & 0xFF);
return len;
}
}