Neo4j圖數據庫管理系統開發筆記之三:構建安全的RMI Service(Server)


 

RMI Server(服務端)主要包括以下功能:遠程用戶權限驗證管理、遠程服務接口實現類、Neo4j實體映射轉換等。項目目錄結構如下圖所示:

rmi_server_project

3.2.1 遠程用戶權限驗證管理
3.2.1.1 用戶權限驗證機制

用戶權限驗證機制分為三個層級。

第一級,遠程主機IP地址驗證。檢查是否允許遠程主機IP地址訪問RMI服務。

第二級,遠程用戶信息驗證。檢查用戶名稱和密碼是否正確,用戶是否啟用等。

第三級,遠程服務及接口方法驗證。檢查用戶是否有權訪問某個RMI服務以及服務下的指定接口方法。

3.2.1.2 遠程用戶配置信息

遠程用戶配置信息在文件remote.users.config.xml中,內容格式如下表所示:

<?xml version="1.0"?>
<remote-users>
    <remote-user user-id="1" login-name="admin" password="admin" user-name="管理員用戶" enabled="true"></remote-user>
    <remote-user user-id="2" login-name="test" password="test" user-name="測試用戶" enabled="true"></remote-user>
</remote-users>
3.2.1.3 遠程主機配置信息

遠程主機配置信息在文件remote.hosts.config.xml中,內容格式如下表所示:

<?xml version="1.0"?>
<hosts>
    <allow-hosts>
        <host>*</host>
    </allow-hosts>
    <forbid-hosts>
        <host></host>
    </forbid-hosts>
</hosts>
3.2.1.4 用戶權限配置信息

用戶權限配置信息在文件remote.users.permission.xml中,內容格式如下表所示:

<?xml version="1.0"?>
<remote-users>
    <remote-user login-name="admin,test">
        <remote-service name="neo4j-graph-manage-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-node-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-index-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-path-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
        <remote-service name="neo4j-graph-cypher-service">
            <allow-methods>
                <method>*</method>
            </allow-methods>
            <forbid-methods>
                <method></method>
            </forbid-methods>
        </remote-service>
    </remote-user>
</remote-users>
3.2.2 遠程服務接口實現類

絕大部分的非業務類工作都是在遠程服務基礎接口實現類BaseRemoteServiceImpl中完成了,譬如,獲取圖數據庫服務對象實例、用戶權限驗證、日志記錄、Neo4j實體映射轉換等。如下表所示:

package com.hnepri.neo4j.rmi.service;

import java.rmi.RemoteException;
import java.rmi.server.RemoteServer;
import java.rmi.server.ServerNotActiveException;
import java.rmi.server.UnicastRemoteObject;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang.StringUtils;
import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.Label;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Path;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;

import com.hnepri.common.util.DateTimeUtil;
import com.hnepri.neo4j.client.rmi.bean.GDirection;
import com.hnepri.neo4j.client.rmi.bean.GNode;
import com.hnepri.neo4j.client.rmi.bean.GPage;
import com.hnepri.neo4j.client.rmi.bean.GPath;
import com.hnepri.neo4j.client.rmi.bean.GRelationship;
import com.hnepri.neo4j.client.rmi.service.IBaseRemoteService;
import com.hnepri.neo4j.client.rmi.util.RemoteClientFactory;
import com.hnepri.neo4j.client.rmi.util.RemoteLoginStatusUtil;
import com.hnepri.neo4j.common.model.GraphPageModel;
import com.hnepri.neo4j.common.util.GraphTemplate;
import com.hnepri.neo4j.rmi.bean.RemoteUser;
import com.hnepri.neo4j.rmi.util.RemoteHostUtil;
import com.hnepri.neo4j.rmi.util.RemoteServerFactory;
import com.hnepri.neo4j.rmi.util.RemoteUserPermissionUtil;
import com.hnepri.neo4j.rmi.util.RemoteUserUtil;

/**
 * Description: 遠程服務基類實現類<br>
 * Copyright: Copyright (c) 2015<br>
 * Company: 河南電力科學研究院智能電網所<br>
 * @author shangbingbing 2015-09-01編寫
 * @version 1.0
 */
@SuppressWarnings("deprecation")
public class BaseRemoteServiceImpl extends UnicastRemoteObject implements IBaseRemoteService {
    private static final long serialVersionUID = 7292764643219275924L;
    private String loginName = "";
    private String password = "";
    private String serviceName = "";
    private String clientAddress = "";
    private boolean loginStatus = false;
    private String loginStatusMessage = "";
    private String graphName;
    private String graphPath;
    
    /**
     * 獲取圖數據庫操作實例GraphTemplate
     * @return
     */
    public GraphTemplate getTemplate() {
        if(StringUtils.isNotBlank(this.getGraphName())) {
            return GraphTemplate.getInstanceByName(this.getGraphName());    
        } else {
            return GraphTemplate.getInstance(this.getGraphPath());
        }
    }
    
    public BaseRemoteServiceImpl() throws RemoteException {
        super();
    }
    
    /**
     * 獲取登錄用戶名稱
     * @return
     */
    protected String getLoginName() {
        return loginName;
    }
    /**
     * 獲取登錄用戶密碼
     * @return
     */
    protected String getPassword() {
        return password;
    }
    /**
     * 獲取遠程服務名稱(主要用於記錄日志)
     * @return
     */
    protected String getServiceName() {
        return serviceName;
    }
    /**
     * 設置遠程服務名稱(主要用於記錄日志)
     * @param serviceName
     */
    protected void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }
    protected String getClientAddress() {
        try {
            this.clientAddress = RemoteServer.getClientHost();
        } catch (ServerNotActiveException e) {
            e.printStackTrace();
        }
        return clientAddress;
    }
    /**
     * 獲取用戶登錄狀態。
     * @return
     */
    protected boolean getLoginStatus() {
        return loginStatus;
    }
    /**
     * 獲取用戶登錄狀態信息。
     * @return
     */
    protected String getLoginStatusMessage() {
        return loginStatusMessage;
    }
    /**
     * 獲取當前數據庫名稱。
     * @return
     */
    public String getGraphName() {
        return graphName;
    }
    /**
     * 獲取當前數據庫路徑。
     * @return
     */
    public String getGraphPath() {
        return graphPath;
    }
    
    /**
     * 檢查遠程調用方法的權限
     * @param methodName
     */
    protected void checkRemoteMethodPermission(String methodName) throws RemoteException {
        boolean hasPermission = RemoteUserPermissionUtil.isCanAccessServiceMethod(this.getLoginName(), this.getServiceName(), methodName);
        if(hasPermission) {
            String log = String.format("%s\t來自【%s】的用戶【%s】調用%s中的接口方法【%s】一次!", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName(), methodName);
            RemoteServerFactory.addLog(log);
        } else {
            String log = String.format("警告:來自【%s】的用戶【%s】無權調用%s中的接口方法【%s】!", this.getClientAddress(), this.getLoginName(), this.getServiceName(), methodName);
            RemoteServerFactory.addLog(log);
            throw new RemoteException(log);
        }
    }
    
    @Override
    public String remoteTest() throws RemoteException {
        return RemoteClientFactory.REMOTE_CALL_STATUS_SUCCESS;
    }

    @Override
    public int login(String loginName, String password) throws RemoteException {
        this.loginName = loginName;
        this.password = password;
        RemoteServerFactory.addLog(String.format("%s\t來自【%s】的用戶【%s】正在驗證訪問【%s】服務!", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName()));
        int loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_SUCCESS;
        if(RemoteHostUtil.isAllowHostAccess(this.getClientAddress())) {
            if(StringUtils.isBlank(loginName) || StringUtils.isBlank(password)) {
                loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_PASSWORD_EMPTY;
            } else {
                if(RemoteUserUtil.getRemoteUserList().containsKey(loginName)) {
                    if(RemoteUserUtil.getRemoteUserList().get(loginName).isEnabled()) {
                        RemoteUser user = RemoteUserUtil.getRemoteUserList().get(loginName);
                        if(user.getPassword().equals(password)) {
                            if(RemoteUserPermissionUtil.isCanAccessService(loginName, this.getServiceName())) {
                                loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_SUCCESS;
                                
                                if(RemoteUserUtil.getOnlineRemoteHostList().containsKey(this.getClientAddress())) {
                                    RemoteUserUtil.getOnlineRemoteHostList().remove(this.getClientAddress());
                                }
                                RemoteUserUtil.getOnlineRemoteHostList().put(this.getClientAddress(), DateTimeUtil.getFormatDateTime(new Date()));
                                
                                if(RemoteUserUtil.getOnlineRemoteUserList().containsKey(this.getLoginName())) {
                                    RemoteUserUtil.getOnlineRemoteUserList().remove(this.getLoginName());
                                }
                                RemoteUserUtil.getOnlineRemoteUserList().put(this.getLoginName(), DateTimeUtil.getFormatDateTime(new Date()));
                            } else {
                                loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_PERMISSION_ERROR;
                            }
                        } else {
                            loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_PASSWORD_ERROR;
                        }
                    } else {
                        loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_ERROR;
                    }
                } else {
                    loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_USER_ERROR;
                }
            }
        } else {
            loginStatusValue = RemoteLoginStatusUtil.LOGIN_STATUS_IP_PERMISSION_ERROR;
        }
        
        this.loginStatusMessage = RemoteClientFactory.parseLoginStatusValue(loginStatusValue);
        if(loginStatusValue == 0) {
            this.loginStatus = true;
            RemoteServerFactory.addLog(String.format("%s\t來自【%s】的用戶【%s】通過【%s】服務的訪問驗證!", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName()));
        } else {
            this.loginStatus = false;
            RemoteServerFactory.addLog(String.format("%s\t來自【%s】的用戶【%s】未通過【%s】服務的訪問驗證。\r\n%s", DateTimeUtil.getFormatDateTime(new Date()), this.getClientAddress(), this.getLoginName(), this.getServiceName(), this.getLoginStatusMessage()));
        }
        return loginStatusValue;
    }

    @Override
    public void initGraphName(String graphName) throws RemoteException {
        this.graphName = graphName;
    }
    
    @Override
    public void initGraphPath(String graphPath) throws RemoteException {
        this.graphPath = graphPath;
    }
}
3.2.3 Neo4j實體映射轉換

Neo4j實體映射轉換,即將Neo4j原生的接口類對象映射轉換為我們在RMI Client中自定義的可序列化的遠程服務實體類。主要轉換方法如下所示。

/**
     * 將Node轉換為GNode對象。<br>
     * <b>【備注:未進行事務處理】</b>
     * @param node
     * @return
     */
    protected GNode convertNodeToGNode(Node node) {
        if(node == null) {
            return null;
        }
        
        GNode gnode = new GNode();
        gnode.setId(node.getId());
        gnode.setDegree(node.getDegree());
        
        Iterator<Label> itLabel = node.getLabels().iterator();
        while(itLabel.hasNext()) {
            Label label = itLabel.next();
            gnode.getLabelNameList().add(label.name());
        }
        
        for(String name : node.getAllProperties().keySet()) {
            Object value = node.getProperty(name);
            gnode.getPropertyList().put(name, value);
        }
        
        Iterator<Relationship> itRelationship = node.getRelationships().iterator();
        while(itRelationship.hasNext()) {
            Relationship rel = itRelationship.next();
            String relType = rel.getType().name();
            gnode.getRelationshipList().add(rel.getId());
            if(gnode.getRelationshipTypeList().containsKey(relType)) {
                gnode.getRelationshipTypeList().get(relType).add(rel.getId());
            } else {
                ArrayList<Long> list = new ArrayList<Long>();
                list.add(rel.getId());
                gnode.getRelationshipTypeList().put(relType, list);
            }
        }
        
        Iterator<Relationship> itRelationshipIncoming = node.getRelationships(Direction.INCOMING).iterator();
        ArrayList<Long> incomingList = new ArrayList<Long>();
        while(itRelationshipIncoming.hasNext()) {
            Relationship rel = itRelationshipIncoming.next();
            incomingList.add(rel.getId());
        }
        gnode.getRelationshipDirectionList().put(Direction.INCOMING.name(), incomingList);
        
        Iterator<Relationship> itRelationshipOutgoing = node.getRelationships(Direction.OUTGOING).iterator();
        ArrayList<Long> outgoingList = new ArrayList<Long>();
        while(itRelationshipOutgoing.hasNext()) {
            Relationship rel = itRelationshipOutgoing.next();
            outgoingList.add(rel.getId());
        }
        gnode.getRelationshipDirectionList().put(Direction.OUTGOING.name(), outgoingList);
        return gnode;
    }
    
    /**
     * 將Node對象轉換為GNode對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param nodes
     * @return 
     */
    public List<GNode> parseNodes(List<Node> nodes) {
        List<GNode> gNodeList = new ArrayList<GNode>();
        if(nodes == null || nodes.size() == 0) return gNodeList;
        if(this.getTemplate() == null) return gNodeList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(Node node : nodes) {
                GNode gnode = this.convertNodeToGNode(node);
                if(gnode == null) continue;
                gNodeList.add(gnode);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gNodeList;
    }
    
    /**
     * 將Node對象轉換為GNode對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param node
     * @return 
     */
    public GNode parseNode(Node node) {
        List<Node> nodes = Arrays.asList(node);
        List<GNode> gNodeList = this.parseNodes(nodes);
        if(gNodeList == null || gNodeList.size() == 0) {
            return null;
        } else {
            return gNodeList.get(0);
        }
    }
    
    /**
     * 根據編碼解析節點對象,將其轉為GNode對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param nodeIDs
     * @return
     */
    public List<GNode> parseNodesByID(List<Long> nodeIDs) {
        List<GNode> gNodeList = new ArrayList<GNode>();
        if(nodeIDs == null || nodeIDs.size() == 0) return gNodeList;
        if(this.getTemplate() == null) return gNodeList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(long nodeID : nodeIDs) {
                Node node = this.getTemplate().getGraphDBService().getNodeById(nodeID);
                GNode gnode = this.convertNodeToGNode(node);
                if(gnode == null) continue;
                gNodeList.add(gnode);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gNodeList;
    }
    
    /**
     * 根據編碼解析節點對象,將其轉為GNode對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param nodeID
     * @return
     */
    public GNode parseNodeByID(long nodeID) {
        List<Long> nodeIDs = Arrays.asList(nodeID);
        List<GNode> gNodeList = this.parseNodesByID(nodeIDs);
        if(gNodeList == null || gNodeList.size() == 0) {
            return null;
        } else {
            return gNodeList.get(0);
        }
    }
    
    /**
     * 將Relationship轉換為GRelationship對象。<br>
     * <b>【備注:未進行事務處理】</b>
     * @param relationship
     * @return
     */
    protected GRelationship convertRelToGRel(Relationship relationship) {
        if(relationship == null) {
            return null;
        }
        
        GRelationship grelationship = new GRelationship();
        grelationship.setId(relationship.getId());
        grelationship.setStartNodeID(relationship.getStartNode().getId());
        grelationship.setEndNodeID(relationship.getEndNode().getId());
        grelationship.setRelationshipType(relationship.getType().name());
        for(String name : relationship.getAllProperties().keySet()) {
            Object value = relationship.getProperty(name);
            grelationship.getPropertyList().put(name, value);
        }
        return grelationship;
    }
    
    /**
     * 將Relationship對象轉換為GRelationship對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param relationships
     * @return 
     */
    public List<GRelationship> parseRelationships(List<Relationship> relationships) {
        List<GRelationship> gRelationshipList = new ArrayList<GRelationship>();
        if(relationships == null || relationships.size() == 0) return gRelationshipList;
        if(this.getTemplate() == null) return gRelationshipList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(Relationship relationship : relationships) {
                GRelationship grelationship = this.convertRelToGRel(relationship);
                if(grelationship == null) continue;
                gRelationshipList.add(grelationship);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gRelationshipList;
    }
    
    /**
     * 將Relationship對象轉換為GRelationship對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param relationship
     * @return 
     */
    public GRelationship parseRelationship(Relationship relationship) {
        List<Relationship> relationships = Arrays.asList(relationship);
        List<GRelationship> gRelationshipList = this.parseRelationships(relationships);
        if(gRelationshipList == null || gRelationshipList.size() == 0) {
            return null;
        } else {
            return gRelationshipList.get(0);
        }
    }
    
    /**
     * 根據編碼解析關系對象,將其轉為GRelationship對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param relationshipIDs
     * @return
     */
    public List<GRelationship> parseRelationshipsByID(List<Long> relationshipIDs) {
        List<GRelationship> gRelationshipList = new ArrayList<GRelationship>();
        if(relationshipIDs == null || relationshipIDs.size() == 0) return gRelationshipList;
        if(this.getTemplate() == null) return gRelationshipList;
        
        Transaction tx = this.getTemplate().createTransaction();
        try {
            for(long relationshipID : relationshipIDs) {
                Relationship relationship = this.getTemplate().getGraphDBService().getRelationshipById(relationshipID);
                GRelationship grelationship = this.convertRelToGRel(relationship);
                if(grelationship == null) continue;
                gRelationshipList.add(grelationship);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
            tx.failure();
        } finally {
            tx.finish();
        }
        
        return gRelationshipList;
    }
    
    /**
     * 根據編碼解析關系對象,將其轉為GRelationship對象。<br>
     * <b>【備注:已進行事務處理】</b>
     * @param relationshipID
     * @return
     */
    public GRelationship parseRelationshipByID(long relationshipID) {
        List<Long> relationshipIDs = Arrays.asList(relationshipID);
        List<GRelationship> gRelationshipList = this.parseRelationshipsByID(relationshipIDs);
        if(gRelationshipList == null || gRelationshipList.size() == 0) {
            return null;
        } else {
            return gRelationshipList.get(0);
        }
    }
    
    /**
     * 將Path對象轉換為GPath對象。<br>
     * <b>【備注:未進行事務處理】</b>
     * @param paths
     * @return
     */
    protected List<GPath> convertPathToGPath(List<Path> paths) {
        List<GPath> gPathList = new ArrayList<GPath>();
        if(paths == null || paths.size() == 0) return gPathList;
        
        for(Path path : paths) {
            GPath gpath = new GPath();
            
            Iterator<Node> itNode = path.nodes().iterator();
            while(itNode.hasNext()) {
                Node node = itNode.next();
                GNode gnode = this.convertNodeToGNode(node);
                if(gnode == null) continue;
                gpath.getNodes().add(gnode);
            }
            
            Iterator<Relationship> itRelationship = path.relationships().iterator();
            while(itRelationship.hasNext()) {
                Relationship relationship = itRelationship.next();
                GRelationship grelationship = this.convertRelToGRel(relationship);
                if(grelationship == null) continue;
                gpath.getRelationships().add(grelationship);
            }
            
            gPathList.add(gpath);
        }
        
        return gPathList;
    }
    
    /**
     * 將Path對象轉換為GPath對象。<br>
     * <b>【備注:未進行事務處理】</b>
     * @param path
     * @return
     */
    protected GPath convertPathToGPath(Path path) {
        List<Path> paths = Arrays.asList(path);
        List<GPath> gPathList = this.convertPathToGPath(paths);
        if(gPathList == null || gPathList.size() == 0) {
            return null;
        } else {
            return gPathList.get(0);
        }
    }
    
    /**
     * 將GPage對象轉換為GraphPageModel對象。
     * @param gpage
     * @return
     */
    public GraphPageModel parseGPageToGraphPageModel(GPage gpage) {
        GraphPageModel pageModel = null;
        if(gpage == null) {
            pageModel = new GraphPageModel(20);
        } else {
            pageModel = new GraphPageModel(gpage.getPageSize());
            pageModel.setPageIndex(gpage.getPageIndex());
            pageModel.setTotalCount(gpage.getTotalCount());
        }
        return pageModel;
    }
    
    /**
     * 將GraphPageModel對象轉換為GPage對象。
     * @param pageModel
     * @return
     */
    public GPage parseGraphPageModelToGPage(GraphPageModel pageModel) {
        GPage gpage = null;
        if(pageModel == null) {
            gpage = new GPage(20);
        } else {
            gpage = new GPage(pageModel.getPageSize());
            gpage.setPageIndex(pageModel.getPageIndex());
            gpage.setTotalCount(pageModel.getTotalCount());
            List<GNode> nodeList = this.parseNodes(pageModel.getNodeList());
            for(GNode node : nodeList) {
                gpage.getNodeList().add(node);
            }
            List<GRelationship> relationshipList = this.parseRelationships(pageModel.getRelationshipList());
            for(GRelationship relationship : relationshipList) {
                gpage.getRelationshipList().add(relationship);
            }
        }
        return gpage;
    }
    
    /**
     * 將GDirection對象轉換為Direction對象。
     * @param gdirection
     * @return
     */
    public Direction parseGDirection(GDirection gDirection) {
        if(gDirection == null) {
            return null;
        }
        if(StringUtils.isBlank(gDirection.getName())) {
            return null;
        }
        return Direction.valueOf(gDirection.getName());
    }
    
    /**
     * 將關系類型名稱解析為關系類型枚舉對象。
     * @param relationshipType
     * @return
     */
    public RelationshipType parseRelationshipType(String relationshipType) {
        if(StringUtils.isBlank(relationshipType)) {
            return null;
        }
        return this.getTemplate().getRelTypeUtil().get(relationshipType);
    }

 

3.3. RMI Server Form

RMI Server Form,即RMI服務窗口管理器,是通過窗口方式來管理RMI服務,包括啟動RMI服務,停止RMI服務,監控RMI日志信息,監控遠程登錄用戶信息,初始化相關配置等操作。此功能集成在“圖數據庫管理系統Server端”,功能界面如下圖所示:

remote service

 

【完】

作者:商兵兵

單位:河南省電力科學研究院智能電網所

QQ:52190634

主頁:http://www.cnblogs.com/shangbingbing

空間:http://shangbingbing.qzone.qq.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM