pc端网站建设哪里有,建筑设计公司起名大全,自学编程入门先学什么,定制软件公司网络安全实验——安全通信软件safechat的设计 仅供参考#xff0c;请勿直接抄袭#xff0c;抄袭者后果自负。
仓库地址#xff1a;
后端地址#xff1a;https://github.com/yijunquan-afk/safechat-server
前端地址#xff1a; https://github.com/yijunquan-afk/safec…网络安全实验——安全通信软件safechat的设计 仅供参考请勿直接抄袭抄袭者后果自负。
仓库地址
后端地址https://github.com/yijunquan-afk/safechat-server
前端地址 https://github.com/yijunquan-afk/safechat-client CosUpload.java中的COS设置需要自己配 1 设计要求
结合所学安全机制设计实现一个简单的安全通信软件包含机密性消息认证等基本功能。并考虑其中涉及的密钥分配方式与机密性算法等相关问题的解决.实现方法不限使用机制不限。
要求
1、 独立完成
2、 具有完整的流程设计报文格式等相关分析。
3、 具备自圆其说的安全性设计思考
2 设计分工
3 设计原理
SHA-2
SHA-2名称来自于安全散列算法2英语Secure Hash Algorithm 2的缩写一种密码散列函数算法标准由美国国家安全局研发[3]由美国国家标准与技术研究院NIST在2001年发布。属于SHA算法之一是SHA-1的后继者。其下又可再分为六个不同的算法标准包括了SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256。
RSA
RSA加密算法是一种非对称加密算法在公开密钥加密和电子商业中被广泛使用。RSA是由罗纳德·李维斯特Ron Rivest、阿迪·萨莫尔Adi Shamir和伦纳德·阿德曼Leonard Adleman在1977年一起提出的。当时他们三人都在麻省理工学院工作。RSA 就是他们三人姓氏开头字母拼在一起组成的。
对极大整数做因数分解的难度决定了 RSA 算法的可靠性。换言之对一极大整数做因数分解愈困难RSA 算法愈可靠。假如有人找到一种快速因数分解的算法的话那么用 RSA 加密的信息的可靠性就会极度下降。但找到这样的算法的可能性是非常小的。今天只有短的 RSA 钥匙才可能被强力方式破解。到2020年为止世界上还没有任何可靠的攻击RSA算法的方式。只要其钥匙的长度足够长用RSA加密的信息实际上是不能被破解的。
WebSocket协议
WebSocket是双向的在客户端-服务器通信的场景中使用的全双工协议与HTTP不同它以ws://或wss://开头。它是一个有状态协议这意味着客户端和服务器之间的连接将保持活动状态直到被任何一方客户端或服务器终止。在通过客户端和服务器中的任何一方关闭连接之后连接将从两端终止。
以客户端-服务器通信为例每当启动客户端和服务器之间的连接时客户端-服务器进行握手随后创建一个新的连接该连接将保持活动状态直到被他们中的任何一方终止。建立连接并保持活动状态后客户端和服务器将使用相同的连接通道进行通信直到连接终止。
新建的连接被称为WebSocket。一旦通信链接建立和连接打开后消息交换将以双向模式进行客户端-服务器之间的连接会持续存在。如果其中任何一方客户端服务器宕掉或主动关闭连接则双方均将关闭连接。套接字的工作方式与HTTP的工作方式略有不同状态代码101表示WebSocket中的交换协议。 JWT
JWT就是通过JSON形式作为Web应用中的令牌用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密签名等相关处理。
基于JWT认证
首先前端通过Wb表单将自己的用戶名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。
2、后端核对用戶名和密码成功后将用戶的id等其他信息作为JWT Payload(负载)将其与头部分别进行Base64编码拼接后签名形成一个JWT(Token)。形成的JWT就是一个形同11.Zzz.xx的字符串。token head.payload.signature
3、后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage.上退出登录时前端删除保存的JWT即可。
4、前端在每次请求时将JWT放入HTTP Header中的Authorization位。解决XSS和XSRF问题
5、后端检查JWT是否存在如存在验证JWT的有效性。检查签名是否正确检查Token是否过期检查Token的接收方是否是自己可选
JWT结构
jwt生成的字符串包含有三部分
1、 jwt头信息部分header标头通常由两部分组成令牌的类型即JWT所使用的签名算法例如HMAC、SHA256或RSA。它会使用Base64编码组成JWT结构的第一部分。
2、 在效载荷Payload令牌的第二部分是有效负载其中包含声明。声明是有关实体通常是用戶和其他数据的声明。同样的它会使用Ba$64编码组成JWT结构的第二部分
3、 签名哈希Signatureheader和payload都是结果Base64编码过的中间用.隔开第三部分就是前面两部分合起来做签名密钥绝对自己保管好签名值同样做Base64编码拼接在JWT后面。签名并编码
AES
高级加密标准英语Advanced Encryption Standard缩写AES又称Rijndael加密法荷兰语发音 [ˈrɛindaːl]音似英文的“Rhine doll”是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES已经被多方分析且广为全世界所使用。经过五年的甄选流程高级加密标准由美国国家标准与技术研究院NIST于2001年11月26日发布于FIPS PUB 197并在2002年5月26日成为有效的标准。现在高级加密标准已然成为对称密钥加密中最流行的算法之一。
严格地说AES和Rijndael加密法并不完全一样虽然在实际应用中两者可以互换因为Rijndael加密法可以支持更大范围的区块和密钥长度AES的区块长度固定为128比特密钥长度则可以是128192或256比特而Rijndael使用的密钥和区块长度均可以是128192或256比特。加密过程中使用的密钥是由Rijndael密钥生成方案产生。
大多数AES计算是在一个特别的有限域完成的。
AES加密过程是在一个4×4的字节矩阵上运作这个矩阵又称为“体state”其初值就是一个明文区块矩阵中一个元素大小就是明文区块中的一个Byte。Rijndael加密法因支持更大的区块其矩阵的“列数Row number”可视情况增加加密时各轮AES加密循环除最后一轮外均包含4个步骤
① AddRoundKey—矩阵中的每一个字节都与该次回合密钥round key做XOR运算每个子密钥由密钥生成方案产生。
② SubBytes—透过一个非线性的替换函数用查找表的方式把每个字节替换成对应的字节。
③ ShiftRows—将矩阵中的每个横列进行循环式移位。
④ MixColumns—为了充分混合矩阵中各个直行的操作。这个步骤使用线性转换来混合每内联的四个字节。最后一个加密循环中省略MixColumns步骤而以另一个AddRoundKey取代。
4 整体设计方案
网络协议
本次设计中我使用了HTTP协议处理一般的网络请求如登录、注册、好友列表获取、个人信息获取、头像更新等功能。
而好友之间点对点的通信为了持续快速地沟通我是用WebSocket协议来处理信息发送请求。
客户端技术选型
客户端负责的是与用户进行交互因此在实用之外还需要考虑到界面美观整洁以给用户带来良好的使用体验。因此前端选择使用 vue AntDesign 组件库进行界面构建。另一方面由于需要建立 WebSocket 连接发送 WebSocket 请求因此需要引入 WebSocket 相关功能的实现。这里使用的是 socket.io 这一 NodeJS 第三方模块。 服务端技术选型
对于服务端采用了 Java SpringBoot 为大框架来进行服务端的开发。数据库采用的是经典的关系型数据库 MySql。同时为了建立 WebSocket 连接处理 WebSocket 请求选择了 socket.io 的一个 Java 移植版本 netty-socketio。netty-socketio是一个开源的Socket.io服务器端的一个java的实现它基于Netty框架可用于服务端推送消息给客户端。 整体功能说明
本系统主要包含六个大的功能模块登陆注册、用户信息获取、信息发送、好友列表显示、头像上传以及退出系统。其中信息发送是本次课程设计最重要的部分是安全通信的主要体现。 5 安全加密部分代码说明
整体设计 HTTP加密
Token产生
private static String sign(String userId,String password){Algorithm algorithm Algorithm.HMAC256(password);String token JWT.create().withClaim(CLAIM_USERID_NAME,userId).withExpiresAt(new Date(System.currentTimeMillis()EXPIRED_TIME/2)).sign(algorithm);return token;
}/**
* 生成一个登录token
* param userId
* param password
* return
*/
public static String loginSign(String userId,String password){String token sign(userId,password);cache.putToken(token,token);return token;
}每次登录产生Token并存储在前端的localStorage中每次发送HTTP的POST和GET请求时加在HTTP Header中的Authorization位。解决XSS和XSRF问题
Token认证
后端接收HTTP请求时需要认证Token。
如此做可以认证发送HTTP请求的用户身份适用于所有HTTP请求 /*** 验证客户端传来token是否有效* 验证逻辑顺序如下* 1. token是否为空* 2. token中账号是否存在* 3. 根据token中账号从数据库中获取真实密码等用户信息并验证用户信息是否有效*/public static void verifyToken(String clientToken, stu.software.chatroom.common.CommonService commonService){if(!StringUtils.hasText(clientToken)){//token为空throw new RuntimeException(无登录令牌);}//从客户端登录令牌中获取当前用户账号String userId JWT.decode(clientToken).getClaim(CLAIM_USERID_NAME).asString();if(!StringUtils.hasText(userId)){//token中账号不存在throw new RuntimeException(登录令牌失效);}//取出缓存中的登录令牌String cacheToken cache.getToken(clientToken);if(!StringUtils.hasText(cacheToken)){//缓存中没有登录令牌throw new RuntimeException(登录令牌失效);}User user commonService.getUserById(userId);if(usernull){//用户不存在throw new RuntimeException(用户不存在);}//验证Token有效性try{Algorithm algorithm Algorithm.HMAC256(user.getU_pwd());JWTVerifier jwtVerifier JWT.require(algorithm).withClaim(CLAIM_USERID_NAME,userId).build();//构建验证器jwtVerifier.verify(cacheToken);}catch(TokenExpiredException e){//令牌过期刷新令牌String newToken sign(userId,user.getU_pwd());cache.putToken(clientToken,newToken);}catch(Exception e){e.printStackTrace();//令牌验证未通过throw new RuntimeException(令牌错误请登录。);}注册密码加密
使用SHA256加密注册时用户使用的密码数据库中存的是密文这样可防止数据库被攻击导致密码泄露。 /***
* 利用Apache的工具类实现SHA-256加密
* return str 加密后的报文
*/
public static String getSHA256Str(String str) {MessageDigest messageDigest;String encodeSir str;try {messageDigest MessageDigest.getInstance(SHA-256);byte[] hash messageDigest.digest(str.getBytes(StandardCharsets.UTF_8));encodeSir Hex.encodeHexString(hash);} catch (NoSuchAlgorithmException e) {e.printStackTrace();}return encodeSir;
}/**
* 通过该方法将密码加密实际上并没有
*/
public static String encodePwd(String u_pwd) {// 密码通过此方法解密并再加密return getSHA256Str(u_pwd);
}登录密码加密
登录时前端输入明文密码使用SHA256加密该密码以后再加数据发送到后端。后端根据该加密后的密码与数据库比对从而验证用户身份。
此做法避免了前端请求数据被拦截导致密码泄露。 import { sha256 } from js-sha256;/*** 加密方法*/
export function PASSWORD(str) {let encodedStr str;encodedStr sha256(encodedStr);return encodedStr;
}
const login () {post(/user/login, {u_name: u_name.value,u_pwd: PASSWORD(u_pwd.value),}).then((res) {tip.success(res.message);let token res.data;setLocalToken(token);router.push({ name: Room, query: { usr: u_name.value } });}).catch((err) {tip.error(账号密码错误);});
};
密钥分配——使用Keytool 参考教程 https://blog.csdn.net/m0_59579040/article/details/124811147 keytool 是个密钥和证书管理工具。它使用户能够管理自己的公钥/私钥对及相关证书用于通过数字签名自我认证用户向别的用户/服务认证自己或数据完整性以及认证服务。它还允许用户储存他们的通信对等者的公钥以证书形式。
在计算机网络上OpenSSL是一个开放源代码的软件库包应用程序可以使用这个包来进行安全通信避免窃听同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。
通过如下步骤可以产生证书和公钥
keytool -genkeypair -storetype PKCS12 -alias yjq - -keyalg RSA -keysize 1024 -dname CNxxx, OUxxx, Oxxx, Lxx, STxx, CCN -keystore D:\mygit\大三下笔记\网安课设\safechat-server\src\main\resources\keys-and-certs\yjq.keystore -keypass 123456 -storepass 123456 -validity 36500 -v 产生二进制文件yjq.keystore以上部分可由脚本生成。
经过KeyStore的相关操作生成公钥、证书和私钥 当用户需要公钥和私钥时只需要调用相关方法即可。
public static void genKeyPair(String name) throws Exception { //以 PKCS12 规格创建 KeyStore KeyStore keyStore KeyStore.getInstance(PKCS12); path keys-and-certs/ name .keystore; //载入 jks 和该 jks 的密码 到 KeyStore 内 keyStore.load(new FileInputStream(new ClassPathResource(keys-and-certs/yjq.keystore).getFile()), 123456.toCharArray()); // 要获取 key需要提供 KeyStore 的别名 和该 KeyStore 的密码 // 获取 keyStore 内所有别名 alias EnumerationString aliases keyStore.aliases(); String alias null; alias aliases.nextElement(); char[] keyPassword 123456.toCharArray(); keyPairString.clear(); //私钥 privateKey (PrivateKey) keyStore.getKey(alias, keyPassword); keyPairString.put(PR, new String(Base64.getEncoder().encode(privateKey.getEncoded()))); //证书 Certificate certificate keyStore.getCertificate(alias); //公钥 publicKey certificate.getPublicKey(); keyPairString.put(PU, new String(Base64.getEncoder().encode(publicKey.getEncoded()))); }
使用公钥加密保证消息认证和机密性 参考教程https://blog.csdn.net/m0_59579040/article/details/124811147. A和B进行通信首先使用A的私钥对报文M进行加密——数字签名然后A用B的公钥对上述结果进行加密——保证了保密性。
B收到消息后用B的私钥解密再用A的公钥验证签名。 这里我使用RSA作为加密算法、SHA1WithRSA作为签名算法签名和加密的操作实现在类RSAUtils.java中。
签名
/**
* 私钥签名
* param content 字符串
* param priKey 私钥
* return
* throws Exception
*/
public static byte[] sign(String content, PrivateKey priKey) throws Exception { Signature signature Signature.getInstance(SIGALG); signature.initSign(priKey); signature.update(content.getBytes()); return signature.sign();
} /**
* 公钥验证签名
* param content 字符串
* param sign 签名
* param pubKey 公钥
* return 身份是否真实
* throws Exception
*/
public static boolean verify(String content, byte[] sign, PublicKey pubKey) throws Exception { Signature signature Signature.getInstance(SIGALG); signature.initVerify(pubKey); signature.update(content.getBytes()); return signature.verify(sign);
}
加密解密
/**
* RSA公钥加密
*
* param content 加密字符串
* param publicKey 公钥
* return 密文
* throws Exception 加密过程中的异常信息
*/
public static String encrypt(String content, String publicKey) throws Exception { //base64编码的公钥 byte[] decoded Base64.getMimeDecoder().decode(publicKey); RSAPublicKey pubKey (RSAPublicKey) KeyFactory.getInstance(KEYALG).generatePublic(new X509EncodedKeySpec(decoded)); System.out.println(pubKey.getAlgorithm()); //RSA加密 Cipher cipher Cipher.getInstance(KEYALG); cipher.init(Cipher.ENCRYPT_MODE, pubKey); String outStr Base64.getEncoder().encodeToString(cipher.doFinal(content.getBytes(UTF-8))); return outStr;
} /**
* RSA私钥解密
*
* param content 加密字符串
* param privateKey 私钥
* return 明文
* throws Exception 解密过程中的异常信息
*/
public static String decrypt(String content, String privateKey) throws Exception { //64位解码加密后的字符串 byte[] inputByte Base64.getMimeDecoder().decode(content); // //base64编码的私钥 byte[] decoded Base64.getMimeDecoder().decode(privateKey); RSAPrivateKey priKey (RSAPrivateKey) KeyFactory.getInstance(RSA).generatePrivate(new PKCS8EncodedKeySpec(decoded)); //RSA解密 Cipher cipher Cipher.getInstance(RSA); cipher.init(Cipher.DECRYPT_MODE, priKey); String outStr new String(cipher.doFinal(inputByte)); return outStr;
}
使用AES加密消息
因为公钥加密的消息认证比较费时间所以当两个用户建立消息通信时由一方产生会话密钥使用公钥加密来传送会话密钥并认证身份。身份认证完成后使用该会话密钥加密消息其中使用对称加密技术AES加密消息。
消息报文格式如下 1、 id报文标识id;
2、 time报文发送时间
3、 content报文内容加密
4、 type报文类型会话密钥消息/公钥消息
5、 sender_name发送者
6、 receiver_name接收者
7、 sign发送者签名。
加密过程如下
public final class AESUtils{ private static final String ALGORITHM AES; public static String genAesSecret(){ try { KeyGenerator kg KeyGenerator.getInstance(AES); //下面调用方法的参数决定了生成密钥的长度可以修改为128, 192或256 kg.init(256); SecretKey sk kg.generateKey(); byte[] b sk.getEncoded(); String secret Base64.encodeBase64String(b); return secret; } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException(没有此算法); } } /** * 根据密钥对指定的明文plainText进行加密. * * param plainBytes 明文 * param keyBytes 密码 * return 加密后的密文. * since 0.0.8 */ public static byte[] encrypt(byte[] plainBytes, byte[] keyBytes) { try { SecretKey secretKey getSecretKey(keyBytes); Cipher cipher Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); return cipher.doFinal(plainBytes); } catch (Exception e) { throw new RuntimeException(e); } } /** * 根据密钥对指定的密文 cipherBytes 进行解密. * * param cipherBytes 加密密文 * param keyBytes 秘钥 * return 解密后的明文. * since 0.0.8 */ public static byte[] decrypt(byte[] cipherBytes, byte[] keyBytes) { try { SecretKey secretKey getSecretKey(keyBytes); Cipher cipher Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); return cipher.doFinal(cipherBytes); } catch (Exception e) { throw new RuntimeException(e); } } /** * 获取加密 key * param keySeed seed * return 结果 * since 0.0.8 */ private static SecretKey getSecretKey(byte[] keySeed) { try { // 避免 linux 系统出现随机的问题 SecureRandom secureRandom SecureRandom.getInstance(SHA1PRNG); secureRandom.setSeed(keySeed); KeyGenerator generator KeyGenerator.getInstance(AES); generator.init(secureRandom); return generator.generateKey(); } catch (Exception e) { throw new RuntimeException(e); } }
}
服务端加密
结合RSA与AES的加密如下
先用公钥加密RSA发送对称加密使用的会话密钥然后再用会话密钥进行AES对称加密通信。 // 监听客户端发送消息
socketIOServer.addEventListener(Constants.EVENT_MESSAGE_TO_SERVER, String.class, (client, data, ackSender) - { String sender_name getParamsByClient(client, u_name); ObjectMapper mapper new ObjectMapper(); Message message mapper.readValue(data, Message.class); String receiver_name message.getReceiver_name(); if (message.getType().equals(Constants.MASTER_MESSAGE)) { //使用公钥加密传送会话密钥 if (AesKey.equals()) { log.info(用户 sender_name 生成会话密钥); AesKey AESUtils.genAesSecret(); message.setContent(AesKey); log.info(用户 sender_name 使用用户 sender_name 的私钥对会话密钥进行签名); String sign new String(RSAUtils.sign(message.getContent(), RSAUtils.getPrivateKey()), ISO-8859-1); message.setSign(sign); String result RSAUtils.encrypt(message.getContent(), publicKeyStringMap.get(receiver_name)); log.info(使用用户 receiver_name 的公钥对会话密钥进行加密 result); message.setContent(result); sendMessageToFriend(message.getReceiver_name(), message); } else { return; } } else { //使用会话密钥发送消息 byte[] bytes AESUtils.encrypt(message.getContent().getBytes(), AesKey.getBytes()); String encrypt new String(bytes, ISO-8859-1); log.info(用户 sender_name 使用会话密钥加密消息); message.setContent(encrypt); sendMessageToFriend(message.getReceiver_name(), message); }
});
//
//GBK, GB2312UTF-8等一些编码方式为多字节或者可变长编码原来的字节数组就被改变了再转回原来的byte[]数组就会发生错误了。
//ISO-8859-1通常叫做Latin-1Latin-1包括了书写所有西方欧洲语言不可缺少的附加字符其中 0~127的字符与ASCII码相同
// 它是单字节的编码方式在来回切换时不会出现错误。 // 监听客户端接收消息
socketIOServer.addEventListener(receive_triger, String.class, (client, data, ackSender) - { ObjectMapper mapper new ObjectMapper(); Message message mapper.readValue(data, Message.class); String sender_name message.getSender_name(); String receiver_name message.getReceiver_name(); if (message.getType().equals(Constants.MASTER_MESSAGE)) { log.info(收到来自 sender_name 发送给 message.getReceiver_name() 的消息: message.getContent()); String result RSAUtils.decrypt(message.getContent(), RSAUtils.getKeyPair().get(PR)); log.info(用户 receiver_name 使用用户 receiver_name 的私钥对消息进行解密); message.setContent(result); log.info(用户 receiver_name 使用用户 sender_name 的公钥对消息进行验证签名); Boolean sign (RSAUtils.verify(message.getContent(), message.getSign().getBytes(ISO-8859-1), publicKeyMap.get(sender_name))); if (sign) { log.info(签名验证成功身份无误); } else { throw new Exception(签名错误); } receiveMessageFromFriend(message.getReceiver_name(), message); } else { log.info(收到来自 sender_name 发送给 message.getReceiver_name() 的消息: message.getContent()); String text new String(AESUtils.decrypt(message.getContent().getBytes(ISO-8859-1), AesKey.getBytes()), UTF-8); log.info(用户 receiver_name 使用会话密钥进行解密); message.setContent(text); receiveMessageFromFriend(message.getReceiver_name(), message); }
});
6 演示
登录 进入主页面
可以看到好友列表 同时获取本地密钥库中的公私钥并将其加入公钥库 选择好友进行私聊
选择好友进行私聊进入聊天界面。 发送消息
在输入框中输入消息点击发送接收者和发送者的聊天框都会出现相应的消息。此消息是经过后端AES对称加密解密得到的。