zoukankan      html  css  js  c++  java
  • Fabric go sdk初始化所需证书解析

    fabric sdk go 提供的官方文档少之又少,要想入门,主要就靠研究官方的e2e系列示例,这真的是一件挺无奈的事情。没法子,只能硬着头皮上了。研究发现,e2e这个例子是通过cryptogen生成网络所需的所有证书及密钥的。一旦你按着操作后,你会发现你被大堆目录淹没,不知所措,根本不知道他们都是干嘛的。

    那么,这篇文章的作用,就是告诉你,他们都是干嘛的,哪些是初始化sdk所需的。

    让我们开始吧!

    生成证书配置

    首先,用于生成证书目录的配置文件crypto-config.yaml内容如下所示

    # "OrdererOrgs" - Definition of organizations managing orderer nodes
    OrdererOrgs:
      - Name: Mango
        Domain: mango.com
        # Specs is an array of Spec entries.  Each Spec entry consists of two fields : Hostname and CommonName
        Specs:
          - Hostname: orderer
    # "PeerOrgs" - Definition of organizations managing peer nodes
    PeerOrgs:
      - Name: Mango
        Domain: mango.com
        # Allows for the definition of 1 or more hosts that are created sequentially
        # from a template. By default, this looks like "peer%d" from 0 to Count-1.
        # You may override the number of nodes (Count), the starting index (Start)
        # or the template used to construct the name (Hostname).
        Template:
          Count: 2
    

    证书目录解析

    我们根据上述配置来生成证书

    mkdir -p crypto-config
    ./bin/cryptogen generate --config=./crypto-config.yaml
    

    每个目录和对应文件的功能如下:

    crypto-config
    ├── ordererOrganizations
    │   └── mango.com # domain
    │       ├── ca
    │       │   ├── ca.mango.com-cert.pem
    │       │   └── ed9854ea794ed178750ab5fff0b8f7a4c4938721e85de25a65e171dac4dadf41_sk
    │       ├── msp
    │       │   ├── admincerts
    │       │   │   └── Admin@mango.com-cert.pem
    │       │   ├── cacerts
    │       │   │   └── ca.mango.com-cert.pem
    │       │   └── tlscacerts
    │       │       └── tlsca.mango.com-cert.pem
    │       ├── orderers
    │       │   └── orderer.mango.com
    │       │       ├── msp
    │       │       │   ├── admincerts
    │       │       │   │   └── Admin@mango.com-cert.pem
    │       │       │   ├── cacerts
    │       │       │   │   └── ca.mango.com-cert.pem
    │       │       │   ├── keystore
    │       │       │   │   └── b1ee6269b624746f682843f566dcfdbb9b1318ea36d605becc13843f52d13e2c_sk
    │       │       │   ├── signcerts
    │       │       │   │   └── orderer.mango.com-cert.pem
    │       │       │   └── tlscacerts
    │       │       │       └── tlsca.mango.com-cert.pem
    │       │       └── tls
    │       │           ├── ca.crt
    │       │           ├── server.crt
    │       │           └── server.key
    │       ├── tlsca
    │       │   ├── 30fe7e37c6b416049d567b99d07d4d0fca282d28cf51819a8ba9cb12a4b1b5c1_sk
    │       │   └── tlsca.mango.com-cert.pem
    │       └── users
    │           └── Admin@mango.com
    │               ├── msp
    │               │   ├── admincerts
    │               │   │   └── Admin@mango.com-cert.pem
    │               │   ├── cacerts
    │               │   │   └── ca.mango.com-cert.pem
    │               │   ├── keystore
    │               │   │   └── 749735ce27f18879efa1ffdf76bf9d800ccb0298c826cee2c2d68217eb731610_sk
    │               │   ├── signcerts
    │               │   │   └── Admin@mango.com-cert.pem
    │               │   └── tlscacerts
    │               │       └── tlsca.mango.com-cert.pem
    │               └── tls
    │                   ├── ca.crt
    │                   ├── client.crt
    │                   └── client.key
    └── peerOrganizations
        └── org1.mango.com
            ├── ca  # 存放组织的根证书和对应的私钥文件,默认采用EC算法,证书为自签名。组织内的实体将基于该证书作为证书根。
            │   ├── 2c3c2cde48c8f1b5eb0fd54e0b1865d6b5bc3269ebba8cb5ac5a7dfbbd303367_sk
            │   └── ca.org1.mango.com-cert.pem
            ├── msp # 存放代表该组织的身份信息。
            │   ├── admincerts # 组织管理员的身份验证证书,被根证书签名。
            │   │   └── Admin@org1.mango.com-cert.pem
            │   ├── cacerts # 组织的根证书,同ca目录下文件。
            │   │   └── ca.org1.mango.com-cert.pem
            │   └── tlscacerts # 用于TLS的ca证书,自签名。
            │       └── tlsca.org1.mango.com-cert.pem
            ├── peers # 存放属于该组织的所有peer节点。
            │   ├── peer0.org1.mango.com # 第一个peer的信息,包括其msp证书和TLS证书两类。
            │   │   ├── msp
            │   │   │   ├── admincerts # 组织管理员的身份验证证书。peer将基于这些证书来认证交易签署这是否为管理员身份。
            │   │   │   │   └── Admin@org1.mango.com-cert.pem
            │   │   │   ├── cacerts # 组织的根证书.
            │   │   │   │   └── ca.org1.mango.com-cert.pem
            │   │   │   ├── keystore # 本节点的身份私钥,用来签名。
            │   │   │   │   └── d35fbb340c84ad06ffd6d58addaf694a62e23adf53066b9a287f86edbf6dd476_sk
            │   │   │   ├── signcerts # 验证本节点签名的证书,被组织根证书签名。
            │   │   │   │   └── peer0.org1.mango.com-cert.pem
            │   │   │   └── tlscacerts # TLS连接用的身份证书,即组织TLS证书。
            │   │   │       └── tlsca.org1.mango.com-cert.pem
            │   │   └── tls # 存放tls相关的证书和私钥
            │   │       ├── ca.crt # 组织的根证书
            │   │       ├── server.crt # 验证本节点签名的证书,被组织根证书签名。
            │   │       └── server.key # 本节点的身份私钥,用来签名。
            │   └── peer1.org1.mango.com # 第二个peer的信息,结构类似。(省略)
            │       ├── msp
            │       │   ├── admincerts
            │       │   │   └── Admin@org1.mango.com-cert.pem
            │       │   ├── cacerts
            │       │   │   └── ca.org1.mango.com-cert.pem 
            │       │   ├── keystore
            │       │   │   └── ecfeefdab7f95113064a00227d7af5cb3ae448ec1334a932f768010b95dc32f3_sk
            │       │   ├── signcerts
            │       │   │   └── peer1.org1.mango.com-cert.pem
            │       │   └── tlscacerts
            │       │       └── tlsca.org1.mango.com-cert.pem
            │       └── tls
            │           ├── ca.crt
            │           ├── server.crt
            │           └── server.key
            ├── tlsca 存放组织tls连接用的根证书和私钥文件。(TLS是传输层安全协议,其实就是SSL,现在叫TLS了)
            │   ├── 65b65931318b4aa19ae15d1da75413c68371d1506237311fc7244c8e46c1ba8a_sk
            │   └── tlsca.org1.mango.com-cert.pem # TLS根证书
            └── users 存放属于该组织的用户的实体。
                └── Admin@org1.mango.com # 管理员用户的信息,包括其msp证书和tls证书两类。
                    ├── msp
                    │   ├── admincerts # 组织根证书作为管理者身份验证证书。
                    │   │   └── Admin@org1.mango.com-cert.pem 
                    │   ├── cacerts # 组织的根证书.
                    │   │   └── ca.org1.mango.com-cert.pem 
                    │   ├── keystore # 本用户的身份私钥,用来签名。
                    │   │   └── 22e70042f7dd79f35ea92a285d2377a673163805f85c9bf160c87de82db52a4c_sk
                    │   ├── signcerts # 管理员用户的身份验证证书,被组织根证书签名。要被某个Peer认可,则必须放到该peer的msp/admincerts下。
                    │   │   └── Admin@org1.mango.com-cert.pem
                    │   └── tlscacerts # TLS连接用的身份证书,即组织TLS证书。
                    │       └── tlsca.org1.mango.com-cert.pem 
                    └── tls # 存放tls相关的证书和私钥
                        ├── ca.crt # 组织的根证书
                        ├── client.crt # 管理员的用户身份验证证书,被组织根证书签名。
                        └── client.key # 管理员用户的身份私钥,用来签名。
    

    证书目录精简

    一大堆文件是不是?其实经过分析后,可以发现,很多目录的证书或者密钥都是重复的。真正用到的主要是

    • 组织中 Admin用户的证书和密钥
    • 组织中的tls证书

    如果你启用了其他用户,比如User1之类的,就还会有User1@mango.com这样的目录

    精简目录

    让我们删除多余目录,精简后目录如下所示:

    fixtures/crypto/
    ├── ordererOrganizations
    │   └── mango.com
    │       ├── tlsca
    │          └── tlsca.org1.mango.com-cert.pem
    └── peerOrganizations
        └── org1.mango.com
            ├── tlsca
            │   └── tlsca.org1.mango.com-cert.pem
            └── users
                └── Admin@org1.mango.com
                    └── msp
                        ├── keystore
                        │   └── 22e70042f7dd79f35ea92a285d2377a673163805f85c9bf160c87de82db52a4c_sk
                        └── signcerts
                            └── Admin@org1.mango.com-cert.pem
    

    怎么样,是不是清爽多了。

    证书目录再精简

    本节内容依个人业务差异,仅供参考

    我们发现证书的路径六面都带有mango.com这样的ID字段,不方便重用,我们可以选择去除这些ID,精简目录。

    此外,密钥文件22e70042f7dd79f35ea92a285d2377a673163805f85c9bf160c87de82db52a4c_sk名称前面还有一长串的字符,看上去很奇怪是不是。这个一长串的字符,其实就是ski。我们也可以去除这个ski。

    当然,如果你直接去除这些东西,那么sdk会报找不到响应文件的错误,所以我们需要去修改sdk源码。

    主要修改的文件如下:

    fabric-go-sdk/pkg/msp/filecertstore.go

    原先代码

    // NewFileCertStore ...
    func NewFileCertStore(cryptoConfigMSPPath string) (core.KVStore, error) {
    	_, orgName := path.Split(path.Dir(path.Dir(path.Dir(cryptoConfigMSPPath))))
    	opts := &keyvaluestore.FileKeyValueStoreOptions{
    		Path: cryptoConfigMSPPath,
    		KeySerializer: func(key interface{}) (string, error) {
    			ck, ok := key.(*msp.IdentityIdentifier)
    			if !ok {
    				return "", errors.New("converting key to CertKey failed")
    			}
    			if ck == nil || ck.MSPID == "" || ck.ID == "" {
    				return "", errors.New("invalid key")
    			}
    
    			// TODO: refactor to case insensitive or remove eventually.
    			r := strings.NewReplacer("{userName}", ck.ID, "{username}", ck.ID)
    			certDir := path.Join(r.Replace(cryptoConfigMSPPath), "signcerts")
    			return path.Join(certDir, fmt.Sprintf("%s@%s-cert.pem", ck.ID, orgName)), nil
    		},
    	}
    	return keyvaluestore.New(opts)
    }
    

    修改后代码

    // NewFileCertStore ...
    func NewFileCertStore(cryptoConfigMSPPath string) (core.KVStore, error) {
    	opts := &keyvaluestore.FileKeyValueStoreOptions{
    		Path: cryptoConfigMSPPath,
    		KeySerializer: func(key interface{}) (string, error) {
    			ck, ok := key.(*msp.IdentityIdentifier)
    			if !ok {
    				return "", errors.New("converting key to CertKey failed")
    			}
    			if ck == nil || ck.MSPID == "" || ck.ID == "" {
    				return "", errors.New("invalid key")
    			}
    			// TODO: refactor to case insensitive or remove eventually.
    			// 文件夹去除Mspid名称
    			r := strings.NewReplacer("{userName}", ck.ID)
    			certDir := path.Join(r.Replace(cryptoConfigMSPPath), "signcerts")
    			// 文件名精简为cert.pem
    			return path.Join(certDir, "cert.pem"), nil
    		},
    	}
    	return keyvaluestore.New(opts)
    }
    

    fabric-go-sdk/pkg/msp/filecertstore.go

    原先代码

    // NewFileKeyStore ...
    func NewFileKeyStore(cryptoConfigMSPPath string) (core.KVStore, error) {
    	opts := &keyvaluestore.FileKeyValueStoreOptions{
    		Path: cryptoConfigMSPPath,
    		KeySerializer: func(key interface{}) (string, error) {
    			pkk, ok := key.(*msp.PrivKeyKey)
    			if !ok {
    				return "", errors.New("converting key to PrivKeyKey failed")
    			}
    			if pkk == nil || pkk.MSPID == "" || pkk.ID == "" || pkk.SKI == nil {
    				return "", errors.New("invalid key")
    			}
    
    			// TODO: refactor to case insensitive or remove eventually.
    			r := strings.NewReplacer("{userName}", pkk.ID, "{username}", pkk.ID)
    			keyDir := path.Join(r.Replace(cryptoConfigMSPPath), "keystore")
    
    			return path.Join(keyDir, hex.EncodeToString(pkk.SKI)+"_sk"), nil
    		},
    	}
    	return keyvaluestore.New(opts)
    }
    

    修改后代码

    // NewFileKeyStore ...
    func NewFileKeyStore(cryptoConfigMSPPath string) (core.KVStore, error) {
    	opts := &keyvaluestore.FileKeyValueStoreOptions{
    		Path: cryptoConfigMSPPath,
    		KeySerializer: func(key interface{}) (string, error) {
    			pkk, ok := key.(*msp.PrivKeyKey)
    			if !ok {
    				return "", errors.New("converting key to PrivKeyKey failed")
    			}
    			if pkk == nil || pkk.MSPID == "" || pkk.ID == "" || pkk.SKI == nil {
    				return "", errors.New("invalid key")
    			}
    
    			// TODO: refactor to case insensitive or remove eventually.
    			r := strings.NewReplacer("{userName}", pkk.ID, "{username}", pkk.ID)
    			keyDir := path.Join(r.Replace(cryptoConfigMSPPath), "keystore")
    			// 文件名统一为_sk
    			return path.Join(keyDir, "_sk"), nil
    		},
    	}
    	return keyvaluestore.New(opts)
    }
    

    再次精简后的目录

    如此修改后,对应的证书目录,我们就可以改成如下所示:

    fixtures/crypto/
    ├── ordererOrganizations
    │   └── tlsca
    │       └── tlsca-cert.pem
    └── peerOrganizations
        ├── tlsca
        │   └── tlsca-cert.pem
        └── users
            └── Admin
                └── msp
                    ├── keystore
                    │   └── _sk
                    └── signcerts
                        └── cert.pem
    

    怎么样,是不是又精简了很多?

    初始化SDK配置文件

    最后附上相应的初始化sdk所需的config.yaml文件

    name: "mango-service-network"
    #
    # Schema version of the content. Used by the SDK to apply the corresponding parsing rules.
    #
    version: 1.0.0
    #
    # The client section used by GO SDK.
    #
    client:
    
      # Which organization does this application instance belong to? The value must be the name of an org
      # defined under "organizations"
      organization: org1
    
      logging:
        level: info
    
      # Global configuration for peer, event service and orderer timeouts
      # if this this section is omitted, then default values will be used (same values as below)
    #  peer:
    #    timeout:
    #      connection: 10s
    #      response: 180s
    #      discovery:
    #        # Expiry period for discovery service greylist filter
    #        # The channel client will greylist peers that are found to be offline
    #        # to prevent re-selecting them in subsequent retries.
    #        # This interval will define how long a peer is greylisted
    #        greylistExpiry: 10s
    #  eventService:
    #    # Event service type (optional). If not specified then the type is automatically
    #    # determined from channel capabilities.
    #    type: (deliver|eventhub)
        # the below timeouts are commented out to use the default values that are found in
        # "pkg/fab/endpointconfig.go"
        # the client is free to override the default values by uncommenting and resetting
        # the values as they see fit in their config file
    #    timeout:
    #      connection: 15s
    #      registrationResponse: 15s
    #  orderer:
    #    timeout:
    #      connection: 15s
    #      response: 15s
    #  global:
    #    timeout:
    #      query: 180s
    #      execute: 180s
    #      resmgmt: 180s
    #    cache:
    #      connectionIdle: 30s
    #      eventServiceIdle: 2m
    #      channelConfig: 30m
    #      channelMembership: 30s
    #      discovery: 10s
    #      selection: 10m
    
      # Root of the MSP directories with keys and certs.
      cryptoconfig:
        path: ${GOPATH}/src/github.com/hyperledger/fabric-sdk-go-api/fixtures/crypto
    
      # Some SDKs support pluggable KV stores, the properties under "credentialStore"
      # are implementation specific
      credentialStore:
        path: /tmp/mango-service-store
    
        # [Optional]. Specific to the CryptoSuite implementation used by GO SDK. Software-based implementations
        # requiring a key store. PKCS#11 based implementations does not.
        cryptoStore:
          path: /tmp/mango-service-msp
    
       # BCCSP config for the client. Used by GO SDK.
      BCCSP:
        security:
         enabled: true
         default:
          provider: "SW"
         hashAlgorithm: "SHA2"
         softVerify: true
         level: 256
    
      tlsCerts:
        # [Optional]. Use system certificate pool when connecting to peers, orderers (for negotiating TLS) Default: false
        systemCertPool: false
    
        # [Optional]. Client key and cert for TLS handshake with peers and orderers
        client:
          keyfile:
          certfile:
    
    #
    # [Optional]. But most apps would have this section so that channel objects can be constructed
    # based on the content below. If an app is creating channels, then it likely will not need this
    # section.
    #
    channels:
      # name of the channel
      mychannel:
        # Required. list of orderers designated by the application to use for transactions on this
        # channel. This list can be a result of access control ("org1" can only access "ordererA"), or
        # operational decisions to share loads from applications among the orderers.  The values must
        # be "names" of orgs defined under "organizations/peers"
        # deprecated: not recommended, to override any orderer configuration items, entity matchers should be used.
        # orderers:
        #  - orderer.example.com
    
        # Required. list of peers from participating orgs
        peers:
          peer0.org1.mango.com:
            # [Optional]. will this peer be sent transaction proposals for endorsement? The peer must
            # have the chaincode installed. The app can also use this property to decide which peers
            # to send the chaincode install request. Default: true
            endorsingPeer: true
    
            # [Optional]. will this peer be sent query proposals? The peer must have the chaincode
            # installed. The app can also use this property to decide which peers to send the
            # chaincode install request. Default: true
            chaincodeQuery: true
    
            # [Optional]. will this peer be sent query proposals that do not require chaincodes, like
            # queryBlock(), queryTransaction(), etc. Default: true
            ledgerQuery: true
    
            # [Optional]. will this peer be the target of the SDK's listener registration? All peers can
            # produce events but the app typically only needs to connect to one to listen to events.
            # Default: true
            eventSource: true
    
          peer1.org1.mango.com:
    
        policies:
          #[Optional] options for retrieving channel configuration blocks
          queryChannelConfig:
            #[Optional] min number of success responses (from targets/peers)
            minResponses: 1
            #[Optional] channel config will be retrieved for these number of random targets
            maxTargets: 1
            #[Optional] retry options for query config block
            retryOpts:
              #[Optional] number of retry attempts
              attempts: 5
              #[Optional] the back off interval for the first retry attempt
              initialBackoff: 500ms
              #[Optional] the maximum back off interval for any retry attempt
              maxBackoff: 5s
              #[Optional] he factor by which the initial back off period is exponentially incremented
              backoffFactor: 2.0
    
    
    #
    # list of participating organizations in this network
    #
    organizations:
      org1:
        mspid: org1.mango.com
        cryptoPath: peerOrganizations/users/{userName}/msp
        peers:
          - peer0.org1.mango.com
          - peer1.org1.mango.com
    
        # [Optional]. Certificate Authorities issue certificates for identification purposes in a Fabric based
        # network. Typically certificates provisioning is done in a separate process outside of the
        # runtime network. Fabric-CA is a special certificate authority that provides a REST APIs for
        # dynamic certificate management (enroll, revoke, re-enroll). The following section is only for
        # Fabric-CA servers.
        certificateAuthorities:
          - ca.org1.mango.com
    
    #
    # List of orderers to send transaction and channel create/update requests to. For the time
    # being only one orderer is needed. If more than one is defined, which one get used by the
    # SDK is implementation specific. Consult each SDK's documentation for its handling of orderers.
    #
    orderers:
      orderer.mango.com:
        url: localhost:7050
    
        # these are standard properties defined by the gRPC library
        # they will be passed in as-is to gRPC client constructor
        grpcOptions:
          ssl-target-name-override: orderer.mango.com
          # These parameters should be set in coordination with the keepalive policy on the server,
          # as incompatible settings can result in closing of connection.
          # When duration of the 'keep-alive-time' is set to 0 or less the keep alive client parameters are disabled
          keep-alive-time: 0s
          keep-alive-timeout: 20s
          keep-alive-permit: false
          fail-fast: false
          # allow-insecure will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs
          allow-insecure: false
    
        tlsCACerts:
          # Certificate location absolute path
          path: ${GOPATH}/src/github.com/hyperledger/fabric-sdk-go-api/fixtures/crypto/ordererOrganizations/tlsca/tlsca-cert.pem
    #
    # List of peers to send various requests to, including endorsement, query
    # and event listener registration.
    #
    peers:
      peer0.org1.mango.com:
        # this URL is used to send endorsement and query requests
        url: localhost:7051
        # eventUrl is only needed when using eventhub (default is delivery service)
        eventUrl: localhost:7053
    
        grpcOptions:
          ssl-target-name-override: peer0.org1.mango.com
          # These parameters should be set in coordination with the keepalive policy on the server,
          # as incompatible settings can result in closing of connection.
          # When duration of the 'keep-alive-time' is set to 0 or less the keep alive client parameters are disabled
          keep-alive-time: 0s
          keep-alive-timeout: 20s
          keep-alive-permit: false
          fail-fast: false
          # allow-insecure will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs
          allow-insecure: false
    
        tlsCACerts:
          # Certificate location absolute path
          path: ${GOPATH}/src/github.com/hyperledger/fabric-sdk-go-api/fixtures/crypto/peerOrganizations/tlsca/tlsca-cert.pem
    
      peer1.org1.mango.com:
        # this URL is used to send endorsement and query requests
        url: localhost:8051
        # eventUrl is only needed when using eventhub (default is delivery service)
        eventUrl: localhost:8053
    
        grpcOptions:
          ssl-target-name-override: peer1.org1.mango.com
          # These parameters should be set in coordination with the keepalive policy on the server,
          # as incompatible settings can result in closing of connection.
          # When duration of the 'keep-alive-time' is set to 0 or less the keep alive client parameters are disabled
          keep-alive-time: 0s
          keep-alive-timeout: 20s
          keep-alive-permit: false
          fail-fast: false
          # allow-insecure will be taken into consideration if address has no protocol defined, if true then grpc or else grpcs
          allow-insecure: false
    
        tlsCACerts:
          # Certificate location absolute path
          path: ${GOPATH}/src/github.com/hyperledger/fabric-sdk-go-api/fixtures/crypto/peerOrganizations/tlsca/tlsca-cert.pem
    
    
    entityMatchers:
      peer:
        - pattern: (w*)peer0.org1.mango.com(w*)
          urlSubstitutionExp: localhost:7051
          eventUrlSubstitutionExp: localhost:7053
          sslTargetOverrideUrlSubstitutionExp: peer0.org1.mango.com
          mappedHost: peer0.org1.mango.com
    
        - pattern: (w*)peer1.org1.mango.com(w*)
          urlSubstitutionExp: localhost:8051
          eventUrlSubstitutionExp: localhost:8053
          sslTargetOverrideUrlSubstitutionExp: peer1.org1.mango.com
          mappedHost: peer1.org1.mango.com
    
      orderer:
        - pattern: (w*)orderer.mango.com(w*)
          urlSubstitutionExp: localhost:7050
          sslTargetOverrideUrlSubstitutionExp: orderer.mango.com
          mappedHost: orderer.mango.com
    

    当然,这部分内容,也是存在优化空间的,还没研究那么深入,后面有空再改。

    参考文章

    搭建基于hyperledger fabric的联盟社区(八) --Fabric证书解析

  • 相关阅读:
    10 shell test命令
    9 shell 退出状态
    8 shell if else
    7 shell 数学运算
    6-x3 declare和typeset命令:设置变量属性
    6-x1 read命令:从键盘读取数据
    Bootstrap 有一个 class 属性叫做 well,它的作用是为设定的列创造出一种视觉上的深度感
    form-control给input添加这个class类后就会使用bootstrap自带的input框
    bootstrap文字居中!
    img-responsive class图片响应式
  • 原文地址:https://www.cnblogs.com/laolieren/p/fabric_cryptogen_certificates_content.html
Copyright © 2011-2022 走看看