设为首页 收藏本站
查看: 2185|回复: 0

[经验分享] 微信开发 企业号(二)-- 回调模式之Tooken验证 .net/python

[复制链接]

尚未签到

发表于 2015-4-21 12:15:04 | 显示全部楼层 |阅读模式
  在企业号开发者中心中,有加密解密源代码,供给开发者使用。(加解密库下载)
  由于官方只提供了python2.*的类库,使用python3.*的朋友可以再最后下载我修改后的py文件(仅修改验证Tooken代码)。
  
  加解密库分析
  一、需要用到的几个数据
  在企业号中配置/获取到的数据



string sToken = "QDG6eK";
string sCorpID = "wx5823bf96d3bd56c7";
string sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
DSC0000.png
DSC0001.png
  通过URL中获取到的参数



// string sVerifyMsgSig = Request("msg_signature");
string sVerifyMsgSig = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
// string sVerifyTimeStamp = Request("timestamp");
string sVerifyTimeStamp = "1409659589";
// string sVerifyNonce = Request("nonce");
string sVerifyNonce = "263014780";
// string sVerifyEchoStr = Request("echostr");
string sVerifyEchoStr = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
  例如:



http://127.0.0.1/?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==
  
  二、环境搭建
  1、C#
  只需要在官网下载C#库后将CS文件复制到工程,或编译后引用DLL文件即可。
  2、Python
  需要pycrypto第三方库。
  I、linux
  只需要运行:pip install pycrypto安装即可
  II、windows
  安装比较麻烦,网上有说pip install pycrypto、easy_install pycrypto。
  由于我电脑没安装VS2008,安装的是VS2013。按照网上各种方法都无法对下载的包进行编译。
  最后找到了编译好的exe文件,直接安装即可http://www.voidspace.org.uk/python/modules.shtml#pycrypto
  
  三、修改地方
  1、C#
  无修改
  2、Python3.*
  删除



reload(sys)
sys.setdefaultencoding('utf-8')


将所有
try:
except Exception,e:
print e
修改为
try:
except Exception as e:
print(e)


第51行
sha.update("".join(sortlist))
改为
sha.update("".join(sortlist).encode("ascii"))


第174行
pad = ord(plain_text[-1])
改为
pad = plain_text[-1]


第182行
from_corpid = content[xml_len+4:]
改为
from_corpid = content[xml_len+4:].decode("utf8")
  四、分析
  1、实例化



//C#
Tencent.WXBizMsgCrypt wxcpt = new Tencent.WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);


#Python
wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID)
  
  2、调用URL验证接口



//c#
int ret = 0;
string sEchoStr = "";
ret = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr, ref sEchoStr);


#Python
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
  
  3、signature验证
  I、将token, timestamp, nonce, encrypt的内容按照大小字母顺序排列
  II、按顺序将列表中排序号的内容拼接成一个字符串,并对其进行ASCII转码
  III、对ASCII转码后的数组做SHA1加密生成 signature
  IV、生成的signature和URL中获取到的sMsgSignature进行比对,如果一致则继续,否则返回错误。



//C#
public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt ,ref string sMsgSignature)
{
ArrayList AL = new ArrayList();
AL.Add(sToken);
AL.Add(sTimeStamp);
AL.Add(sNonce);
AL.Add(sMsgEncrypt);
AL.Sort(new DictionarySort());
string raw = "";
for (int i = 0; i < AL.Count; ++i)
{
raw += AL;
}
SHA1 sha;
ASCIIEncoding enc;
string hash = "";
try
{
sha = new SHA1CryptoServiceProvider();
enc = new ASCIIEncoding();
byte[] dataToHash = enc.GetBytes(raw);
byte[] dataHashed = sha.ComputeHash(dataToHash);
hash = BitConverter.ToString(dataHashed).Replace("-", "");
hash = hash.ToLower();
}
catch (Exception)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error;
}
sMsgSignature = hash;
return 0;
}

public class DictionarySort : System.Collections.IComparer
{
public int Compare(object oLeft, object oRight)
{
string sLeft = oLeft as string;
string sRight = oRight as string;
int iLeftLength = sLeft.Length;
int iRightLength = sRight.Length;
int index = 0;
while (index < iLeftLength && index < iRightLength)
{
if (sLeft[index] < sRight[index])
return -1;
else if (sLeft[index] > sRight[index])
return 1;
else
index++;
}
return iLeftLength - iRightLength;
}
}

//调用
string hash = "";
int ret = 0;
ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash);
if (ret != 0)
return ret;
if (hash == sSigture)
return 0;


#Python
def getSHA1(self, token, timestamp, nonce, encrypt):
"""用SHA1算法生成安全签名
@param token:  票据
@param timestamp: 时间戳
@param encrypt: 密文
@param nonce: 随机字符串
@return: 安全签名
"""
try:
sortlist = [token, timestamp, nonce, encrypt]
sortlist.sort()
sha = hashlib.sha1()
sha.update("".join(sortlist).encode("ascii"))
return  ierror.WXBizMsgCrypt_OK, sha.hexdigest()
except Exception as e:
print(e)
return  ierror.WXBizMsgCrypt_ComputeSignature_Error, None
  
  4、解密
  I、在sEncodingAESKey接入加入等号(“=”)
  sEncodingAESKey = sEncodingAESKey+"="
  
  II、再用Base64对其进行编码



byte[] Key;
Key = Convert.FromBase64String(EncodingAESKey + "=");


Key = base64.b64decode(sEncodingAESKey+"=")
  
  
  III、根据Key生成AES加密所需要的偏移量IV



//C#
byte[] Iv = new byte[16];
Array.Copy(Key, Iv, 16);


#python
Iv = Key[:16]
  
  IV、解密方法



private static byte[] AES_decrypt(String Input, byte[] Iv, byte[] Key)
{
RijndaelManaged aes = new RijndaelManaged();
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
aes.Key = Key;
aes.IV = Iv;
var decrypt = aes.CreateDecryptor(aes.Key, aes.IV);
byte[] xBuff = null;
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write))
{
byte[] xXml = Convert.FromBase64String(Input);
byte[] msg = new byte[xXml.Length + 32 - xXml.Length % 32];
Array.Copy(xXml, msg, xXml.Length);
cs.Write(xXml, 0, xXml.Length);
}
xBuff = decode2(ms.ToArray());
}
return xBuff;
}

DSC0002.gif DSC0003.gif


# -*- coding: utf-8 -*-
#
#  Cipher/blockalgo.py
#
# ===================================================================
# The contents of this file are dedicated to the public domain.  To
# the extent that dedication to the public domain is not available,
# everyone is granted a worldwide, perpetual, royalty-free,
# non-exclusive license to exercise all rights associated with the
# contents of this file for any purpose whatsoever.
# No rights are reserved.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
# ===================================================================
"""Module with definitions common to all block ciphers."""
import sys
if sys.version_info[0] == 2 and sys.version_info[1] == 1:
from Crypto.Util.py21compat import *
from Crypto.Util.py3compat import *
#: *Electronic Code Book (ECB)*.
#: This is the simplest encryption mode. Each of the plaintext blocks
#: is directly encrypted into a ciphertext block, independently of
#: any other block. This mode exposes frequency of symbols
#: in your plaintext. Other modes (e.g. *CBC*) should be used instead.
#:
#: See `NIST SP800-38A`_ , Section 6.1 .
#:
#: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
MODE_ECB = 1
#: *Cipher-Block Chaining (CBC)*. Each of the ciphertext blocks depends
#: on the current and all previous plaintext blocks. An Initialization Vector
#: (*IV*) is required.
#:
#: The *IV* is a data block to be transmitted to the receiver.
#: The *IV* can be made public, but it must be authenticated by the receiver and
#: it should be picked randomly.
#:
#: See `NIST SP800-38A`_ , Section 6.2 .
#:
#: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
MODE_CBC = 2
#: *Cipher FeedBack (CFB)*. This mode is similar to CBC, but it transforms
#: the underlying block cipher into a stream cipher. Plaintext and ciphertext
#: are processed in *segments* of **s** bits. The mode is therefore sometimes
#: labelled **s**-bit CFB. An Initialization Vector (*IV*) is required.
#:
#: When encrypting, each ciphertext segment contributes to the encryption of
#: the next plaintext segment.
#:
#: This *IV* is a data block to be transmitted to the receiver.
#: The *IV* can be made public, but it should be picked randomly.
#: Reusing the same *IV* for encryptions done with the same key lead to
#: catastrophic cryptographic failures.
#:
#: See `NIST SP800-38A`_ , Section 6.3 .
#:
#: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
MODE_CFB = 3
#: This mode should not be used.
MODE_PGP = 4
#: *Output FeedBack (OFB)*. This mode is very similar to CBC, but it
#: transforms the underlying block cipher into a stream cipher.
#: The keystream is the iterated block encryption of an Initialization Vector (*IV*).
#:
#: The *IV* is a data block to be transmitted to the receiver.
#: The *IV* can be made public, but it should be picked randomly.
#:
#: Reusing the same *IV* for encryptions done with the same key lead to
#: catastrophic cryptograhic failures.
#:
#: See `NIST SP800-38A`_ , Section 6.4 .
#:
#: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
MODE_OFB = 5
#: *CounTeR (CTR)*. This mode is very similar to ECB, in that
#: encryption of one block is done independently of all other blocks.
#: Unlike ECB, the block *position* contributes to the encryption and no
#: information leaks about symbol frequency.
#:
#: Each message block is associated to a *counter* which must be unique
#: across all messages that get encrypted with the same key (not just within
#: the same message). The counter is as big as the block size.
#:
#: Counters can be generated in several ways. The most straightword one is
#: to choose an *initial counter block* (which can be made public, similarly
#: to the *IV* for the other modes) and increment its lowest **m** bits by
#: one (modulo *2^m*) for each block. In most cases, **m** is chosen to be half
#: the block size.
#:
#: Reusing the same *initial counter block* for encryptions done with the same
#: key lead to catastrophic cryptograhic failures.
#:
#: See `NIST SP800-38A`_ , Section 6.5 (for the mode) and Appendix B (for how
#: to manage the *initial counter block*).
#:
#: .. _`NIST SP800-38A` : http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf
MODE_CTR = 6
#: OpenPGP. This mode is a variant of CFB, and it is only used in PGP and OpenPGP_ applications.
#: An Initialization Vector (*IV*) is required.
#:
#: Unlike CFB, the IV is not transmitted to the receiver. Instead, the *encrypted* IV is.
#: The IV is a random data block. Two of its bytes are duplicated to act as a checksum
#: for the correctness of the key. The encrypted IV is therefore 2 bytes longer than
#: the clean IV.
#:
#: .. _OpenPGP: http://tools.ietf.org/html/rfc4880
MODE_OPENPGP = 7
def _getParameter(name, index, args, kwargs, default=None):
"""Find a parameter in tuple and dictionary arguments a function receives"""
param = kwargs.get(name)
if len(args)>index:
if param:
raise ValueError("Parameter '%s' is specified twice" % name)
param = args[index]
return param or default
class BlockAlgo:
"""Class modelling an abstract block cipher."""
def __init__(self, factory, key, *args, **kwargs):
self.mode = _getParameter('mode', 0, args, kwargs, default=MODE_ECB)
self.block_size = factory.block_size
if self.mode != MODE_OPENPGP:
self._cipher = factory.new(key, *args, **kwargs)
self.IV = self._cipher.IV
else:
# OPENPGP mode. For details, see 13.9 in RCC4880.
#
            # A few members are specifically created for this mode:
#  - _encrypted_iv, set in this constructor
#  - _done_first_block, set to True after the first encryption
#  - _done_last_block, set to True after a partial block is processed
            
self._done_first_block = False
self._done_last_block = False
self.IV = _getParameter('iv', 1, args, kwargs)
if not self.IV:
raise ValueError("MODE_OPENPGP requires an IV")
# Instantiate a temporary cipher to process the IV
IV_cipher = factory.new(key, MODE_CFB,
b('\x00')*self.block_size,      # IV for CFB
segment_size=self.block_size*8)
# The cipher will be used for...
if len(self.IV) == self.block_size:
# ... encryption
self._encrypted_IV = IV_cipher.encrypt(
self.IV + self.IV[-2:] +        # Plaintext
b('\x00')*(self.block_size-2)   # Padding
)[:self.block_size+2]
elif len(self.IV) == self.block_size+2:
# ... decryption
self._encrypted_IV = self.IV
self.IV = IV_cipher.decrypt(self.IV +   # Ciphertext
b('\x00')*(self.block_size-2)       # Padding
)[:self.block_size+2]
if self.IV[-2:] != self.IV[-4:-2]:
raise ValueError("Failed integrity check for OPENPGP IV")
self.IV = self.IV[:-2]
else:
raise ValueError("Length of IV must be %d or %d bytes for MODE_OPENPGP"
% (self.block_size, self.block_size+2))
# Instantiate the cipher for the real PGP data
self._cipher = factory.new(key, MODE_CFB,
self._encrypted_IV[-self.block_size:],
segment_size=self.block_size*8)
def encrypt(self, plaintext):
"""Encrypt data with the key and the parameters set at initialization.
The cipher object is stateful; encryption of a long block
of data can be broken up in two or more calls to `encrypt()`.
That is, the statement:
>>> c.encrypt(a) + c.encrypt(b)
is always equivalent to:
>>> c.encrypt(a+b)
That also means that you cannot reuse an object for encrypting
or decrypting other data with the same key.
This function does not perform any padding.
- For `MODE_ECB`, `MODE_CBC`, and `MODE_OFB`, *plaintext* length
(in bytes) must be a multiple of *block_size*.
- For `MODE_CFB`, *plaintext* length (in bytes) must be a multiple
of *segment_size*/8.
- For `MODE_CTR`, *plaintext* can be of any length.
- For `MODE_OPENPGP`, *plaintext* must be a multiple of *block_size*,
unless it is the last chunk of the message.
:Parameters:
plaintext : byte string
The piece of data to encrypt.
:Return:
the encrypted data, as a byte string. It is as long as
*plaintext* with one exception: when encrypting the first message
chunk with `MODE_OPENPGP`, the encypted IV is prepended to the
returned ciphertext.
"""
if self.mode == MODE_OPENPGP:
padding_length = (self.block_size - len(plaintext) % self.block_size) % self.block_size
if padding_length>0:
# CFB mode requires ciphertext to have length multiple of block size,
# but PGP mode allows the last block to be shorter
if self._done_last_block:
raise ValueError("Only the last chunk is allowed to have length not multiple of %d bytes",
self.block_size)
self._done_last_block = True
padded = plaintext + b('\x00')*padding_length
res = self._cipher.encrypt(padded)[:len(plaintext)]
else:
res = self._cipher.encrypt(plaintext)
if not self._done_first_block:
res = self._encrypted_IV + res
self._done_first_block = True
return res
return self._cipher.encrypt(plaintext)
def decrypt(self, ciphertext):
"""Decrypt data with the key and the parameters set at initialization.
The cipher object is stateful; decryption of a long block
of data can be broken up in two or more calls to `decrypt()`.
That is, the statement:
>>> c.decrypt(a) + c.decrypt(b)
is always equivalent to:
>>> c.decrypt(a+b)
That also means that you cannot reuse an object for encrypting
or decrypting other data with the same key.
This function does not perform any padding.
- For `MODE_ECB`, `MODE_CBC`, and `MODE_OFB`, *ciphertext* length
(in bytes) must be a multiple of *block_size*.
- For `MODE_CFB`, *ciphertext* length (in bytes) must be a multiple
of *segment_size*/8.
- For `MODE_CTR`, *ciphertext* can be of any length.
- For `MODE_OPENPGP`, *plaintext* must be a multiple of *block_size*,
unless it is the last chunk of the message.
:Parameters:
ciphertext : byte string
The piece of data to decrypt.
:Return: the decrypted data (byte string, as long as *ciphertext*).
"""
if self.mode == MODE_OPENPGP:
padding_length = (self.block_size - len(ciphertext) % self.block_size) % self.block_size
if padding_length>0:
# CFB mode requires ciphertext to have length multiple of block size,
# but PGP mode allows the last block to be shorter
if self._done_last_block:
raise ValueError("Only the last chunk is allowed to have length not multiple of %d bytes",
self.block_size)
self._done_last_block = True
padded = ciphertext + b('\x00')*padding_length
res = self._cipher.decrypt(padded)[:len(ciphertext)]
else:
res = self._cipher.decrypt(ciphertext)
return res
return self._cipher.decrypt(ciphertext)
Python AES 解密,已存在与blockalgo.py文件中  
  V、AES解密,并对解密后的数据进行拆分



//c#
byte[] btmpMsg = AES_decrypt(Input, Iv, Key); //调用解密方法解密
//返回由字节数组中指定位置的四个字节转换来的 32 位有符号整数
int len = BitConverter.ToInt32(btmpMsg, 16);
//将数字由网络字节顺序转换为主机字节顺序。
len = IPAddress.NetworkToHostOrder(len);
byte[] bMsg = new byte[len];
byte[] bCorpid = new byte[btmpMsg.Length - 20 - len];
Array.Copy(btmpMsg, 20, bMsg, 0, len);
Array.Copy(btmpMsg, 20+len , bCorpid, 0, btmpMsg.Length - 20 - len);
string oriMsg = Encoding.UTF8.GetString(bMsg);
corpid = Encoding.UTF8.GetString(bCorpid); //用来和m_sCorpID验证,解密是否正确


        try:
pad = plain_text[-1]
# 去除16位随机字符串
content = plain_text[16:-pad]
#struct.unpack("I",content[ : 4])[0]    返回由字节数组中指定位置的四个字节转换来的 32 位有符号整数
#socket.ntohl 将数字由网络字节顺序转换为主机字节顺序。
xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
xml_content = content[4 : xml_len+4]
from_corpid = content[xml_len+4:].decode("utf8")
print(from_corpid)
except Exception as e:
print(e)
return  ierror.WXBizMsgCrypt_IllegalBuffer,None   
  
  5、调用解密方法,返回明文及cpid



//C#
sReplyEchoStr = Cryptography.AES_decrypt(sEchoStr, m_sEncodingAESKey, ref cpid);


#python
pc = Prpcrypt(self.key)
ret,sReplyEchoStr = pc.decrypt(sEchoStr,self.m_sCorpid)
  
  6、解密后需要比对m_sCorpID和cpid是否一致
  
  五、疑问
  由于技术有限,并没做过太多Socket编程,并不很了解如下内容
  如有高手路过,请指点一下,谢谢。



//返回由字节数组中指定位置的四个字节转换来的 32 位有符号整数
int len = BitConverter.ToInt32(btmpMsg, 16);
//将数字由网络字节顺序转换为主机字节顺序。
len = IPAddress.NetworkToHostOrder(len);


xml_len = socket.ntohl(struct.unpack("I",content[ : 4])[0])
  
  六、Python3.*版本WXBizMsgCrypt文件下载
  修改后Python3.* 对应WXBizMsgCrypt.2014.09.28.zip文件。
  该版本仅使用本文中修改方法进行修改,并且仅测试过URL验证方法,其他方法暂时可能存在问题,后续慢慢完善。
  如有高手愿意提供WXBizMsgCrypt.py,请直接留言,谢谢。
  
  七、Python3.*版本 测试代码



from http.server import BaseHTTPRequestHandler
from http.server import HTTPServer
from socketserver import ThreadingMixIn
import urllib.parse
from WXBizMsgCrypt import WXBizMsgCrypt
import xml.etree.cElementTree as ET

hostIP = ''
portNum = 8080
serverMessage = "msg_signature"
sToken="QDG6eK"
sEncodingAESKey="jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C"
sCorpID="wx5823bf96d3bd56c7"
class mySoapServer( BaseHTTPRequestHandler ):
def do_head( self ):
pass
def do_GET( self ):
try:           
if self.path.find(serverMessage) == -1:
self.send_error( 404, message = None )
return
#解析请求
query=GetQuery(self.path)
sVerifyMsgSig=query["msg_signature"][0]
sVerifyTimeStamp=query["timestamp"][0]
sVerifyNonce=query["nonce"][0]
sVerifyEchoStr=query["echostr"][0]
wxcpt=WXBizMsgCrypt(sToken,sEncodingAESKey,sCorpID)
ret,sEchoStr=wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,sVerifyNonce,sVerifyEchoStr)
self.send_response( 200, message = None )
self.send_header( 'Content-type', 'text/html' )
self.end_headers()            
if ret == 0:               
res=sEchoStr.decode("utf8")
else:
res = "%s" % ret
self.wfile.write( res.encode( encoding = 'utf_8', errors = 'strict' ) )
except IOError:
self.send_error( 404, message = None )
def GetQuery(str):
params = str[str.index("?")+1:]
parsed_result = {}
list = [param for param in params.split('&')]
for item in list:
if item.find("=") > -1:
name = item[:item.index("=")]
value = urllib.parse.unquote(item[item.index("=")+1:])
else:
name = item
value = ""
if name in parsed_result:
parsed_result[name].append(value)
else:
parsed_result[name] = [value]     
return parsed_result  #urllib.parse.parse_qs(temp)

class ThreadingHttpServer( ThreadingMixIn, HTTPServer ):
pass
myServer = ThreadingHttpServer( ( hostIP, portNum ), mySoapServer )
print("Server Started ....")
myServer.serve_forever()
myServer.server_close()
  

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-59226-1-1.html 上篇帖子: 用Python,将 Excel 数据导出到 xml 下篇帖子: python中那纠结的os.system()与空格处理
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表