SpringBoot整合FastDFS+Nginx整合基於Token的防盜鏈

java架構codi發表於2019-04-25

為什麼要用SpringBoot?

SpringBoot是由Pivotal團隊提供的全新框架,其設計目的是用來簡化新Spring應用的初始搭建以及開發過程。該框架使用了特定的方式來進行配置,從而使開發人員不再需要定義樣板化的配置。通過這種方式,Spring Boot致力於在蓬勃發展的快速應用開發領域(rapid application development)成為領導者。

  • 建立獨立的Spring應用程式

  • 嵌入的Tomcat,無需部署WAR檔案

  • 簡化Maven配置

  • 自動配置Spring

  • 提供生產就緒型功能,如指標,健康檢查和外部配置

  • 絕對沒有程式碼生成並且對XML也沒有配置要求

為什麼要用Nginx?

  • 概述

    Nginx(engine x)是一個開源的,支援高併發的www服務和代理服務軟體。Nginx是俄羅斯人Igor Sysoev開發的,最初被應用到俄羅斯的大型網站(www.rambler.ru)上。後來作者將原始碼以類BSD許可證的形式開源出來供全球使用。在功能應用方面,Nginx不僅是一個優秀的Web服務軟體,還具有反向代理負載均衡和快取的功能。在反向代理負載均衡方面類似於LVS負載均衡及HAProxy等你專業代理軟體。Nginx部署起來更加方便簡單,在快取服務功能方面,有類似於Squid等專業的快取服務軟體。Nginx可以執行在UNIX、Linux、MS Windows Server、Mac OS X Server、Solaris等作業系統中。

  • Nginx的重要特性

    1. 可以針對靜態資源高速節點併發訪問及快取。
    2. 可以使用反向代理加速,並且可以進行資料快取。
    3. 具有簡單負載均衡,節點健康檢查和容錯功能。
    4. 支援遠端Fast CGI服務的快取加速。
    5. 支援Fast CGI、Uwsgi、SCGI、Memcached Server的加速和快取。
    6. 支援SSL、TLS、SNI。
    7. 具有模組化的架構。
    8. 過濾器包括gzip壓縮、ranges支援、chunked響應、XSLT、SSL和影像縮放等功能。
    9. 在SSL過濾器中,包含多個SSL頁面,如果經由Fast CGI或反向代理處理,可以並行處理。
  • Nginx所具備的WWW服務特性

    1. 支援基於域名、埠和IP的虛擬主機配置。
    2. 支援KeepAlived和piplined連線。
    3. 可進行簡單、方便、靈活的配置和管理。
    4. 支援修改Nginx配置,並且在程式碼上線時,可平滑重啟,不中斷業務訪問。
    5. 可自定義訪問日誌格式,臨時緩衝寫日誌操作,快速日誌輪詢及通過rsyslog處理日誌。
    6. 可利用訊號控制Nginx程式。
    7. 支援3xx-5xxHTTP狀態碼重定向。
    8. 支援rewrite模組,支援URI重寫及正規表示式匹配。
    9. 支援基於客戶端IP地址和HTTP基本認證的訪問控制。
    10. 支援PUT、DELETE、MKCOL、COPY、MOVE等特殊的HTTP請求方法。
    11. 支援FLV流和MP4流技術產品應用。
    12. 支援HTTP響應速率限制。
    13. 支援同一IP地址的併發連線或請求限制。
    14. 支援郵件服務代理。
    15. 支援高併發,可以支援幾百萬併發連線。
    16. 資源消耗少,在3萬併發連線下,可以開啟10個nginx的執行緒消耗的記憶體不到200MB。
    17. 可以做HTTP反向代理及加速快取,及負載均衡功能,內建對RS節點伺服器健康檢查功能,折現但能夠與專業的HAProxy或LVS的功能。
    18. 具備Squid等專業快取軟體等的快取功能。
    19. 支援非同步網路I/O事件模型epoll(Linux2.6+)。
  • Nginx軟體主要企業應用

    1. 作為Web服務軟體。
    2. 使用Nginx執行HTML、JS、CSS、小圖片等靜態資料(類似於Lighttpd)。
    3. 結合Fast CGI執行PHP等動態程式(例如使用fastcgi_pass方式)。
    4. Nginx結合Tomcat/Resin等支援Java動態程式(常用proxy_pass)。
    5. 反向代理或負載均衡服務(Nginx從1.9.0開始就開始支援TCP的代理了)。
    6. 前端業務資料快取服務。
  • Web服務應用產品效能對比

    1. 靜態資料的訪問上:處理小檔案(小於1MB)時,Nginx和Lighttpd比Apache更有優勢,Nginx處理小檔案的優勢明顯,Lighttpd綜合最強。
    2. 動態資料的訪問上:三者差距不大,Apache更有優勢,因為處理動態資料的能力在於PHP(Java)和後端資料庫的服務能力,也就是說瓶頸不在Web伺服器上。
    3. 一般情況下普通PHP引擎支援的併發連線參考值300~1000。Java引擎和資料庫的併發連線參考值300~1500。
  • 為什麼Nginx比Apache的效能高?

    1. Nginx使用最新版的eepoll(Linux 2.6核心)和kqueue(FreeBSD)非同步網路I/O模型,而Apache使用的是傳統的select模型。
    2. 目前Linux下能夠承受高併發訪問的Squid、Memcached軟體採用都是epoll模型。
    3. 處理大量的連線的讀寫時,Apache所採用的select網路I/O模型比較低。
  • 如何正確採用Web伺服器?

    1. 靜態業務:如果是高併發場景,儘量採用Nginx或Lighttpd,二者首選Nginx。
    2. 動態業務:理論上採用Nginx和Apache均可,建議使用Nginx,為了避免相同業務服務的軟體多樣化,增加維護成本,動態業務可以使用Nginx兼做前端代理,再根據頁面的元素或目錄轉發到其他的伺服器進行處理。
    3. 既有動態業務又有靜態業務,就用Nginx。


使用IDEA場景啟動器建立工程

  • 建立Maven工程,修改POM.xml檔案新增如下依賴:
<dependencies>
    <!-- SpringBoot的自動配置相關依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
        <version>1.5.20.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <version>1.5.20.RELEASE</version>
    </dependency>
    <!-- 日誌相關的依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
        <version>1.5.20.RELEASE</version>
    </dependency>
    <!-- 物件池相關的依賴 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.6.0</version>
    </dependency>

</dependencies>複製程式碼

建立必要的包

  • annotation:存放相關的註解

  • autoconfiguation: 儲存自動配置類

  • factory: 存放工廠類

  • properties: 存放配置引數類

  • service: 存放服務類

一般情況下,SpringBoot都會提供相應的@EnableXxx註解標註在應用的主啟動類上開啟某個功能:

// EnableFastdfsClient.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(FastdfsAutoConfiguration.class)
@Documented
public @interface EnableFastdfsClient {
}複製程式碼

下面是相關的自動配置類:

// FastdfsAutoConfiguration.java
@Configuration
@EnableConfigurationProperties(FastdfsProperties.class)
public class FastdfsAutoConfiguration {
    @Autowired
    private FastdfsProperties fastdfsProperties;

    @Bean
    @ConditionalOnMissingBean(FastdfsClientService.class)
    public FastdfsClientService fastdfsClientService() throws Exception {
        return new FastdfsClientService(fastdfsProperties);
    }
}複製程式碼

建立相關的工廠類:

// StorageClientFactory.java
// 用於建立連線物件的工廠類
public class StorageClientFactory implements PooledObjectFactory<StorageClient> {

    @Override
    public PooledObject<StorageClient> makeObject() throws Exception {
        TrackerClient client = new TrackerClient();
        TrackerServer server = client.getConnection();
        return new DefaultPooledObject<>(new StorageClient(server, null));
    }

    @Override
    public void destroyObject(PooledObject<StorageClient> p) throws Exception {
        p.getObject().getTrackerServer().close();
    }

    @Override
    public boolean validateObject(PooledObject<StorageClient> p) {
        return false;
    }

    @Override
    public void activateObject(PooledObject<StorageClient> p) throws Exception {

    }

    @Override
    public void passivateObject(PooledObject<StorageClient> p) throws Exception {

    }
}複製程式碼

Properties類用來對映application.properties或者application.yml配置檔案:

// FastdfsProperties.java
@ConfigurationProperties(prefix = "fastdfs")
public class FastdfsProperties {
    // 連線超時時間
    // 網路超時時間
    // 字符集編碼
    // 是否使用Token
    // Token加密金鑰
    // 跟蹤器IP地址,多個使用分號隔開
    // 連線池的連線物件最大個數
    // 連線池的最大空閒物件個數
    // 連線池的最小空閒物件個數
    // Nginx伺服器IP,多個使用分號分割
    // 獲取連線物件時可忍受的等待時長(毫秒)
    private String connectTimeout = "5";
    private String networkTimeout = "30";
    private String charset = "UTF-8";
    private String httpAntiStealToken = "false";
    private String httpSecretKey = "";
    private String httpTrackerHttpPort = "";
    private String trackerServers = "";
    private String connectionPoolMaxTotal = "18";
    private String connectionPoolMaxIdle = "18";
    private String connectionPoolMinIdle = "2";
    private String nginxServers = "";

    // 需要建立相關的Setter和Getter方法
}複製程式碼

在Service類中封裝方法, 下面僅展示3個常用的方法:

// FastdfsClientSerivce.java
public class FastdfsClientService {
    // SpringBoot載入的配置檔案
    // 連線池配置項
    // 轉換後的配置條目
    // 連線池
    // Nginx伺服器地址
    private FastdfsProperties fdfsProp;
    private GenericObjectPoolConfig config;
    private Properties prop;
    private GenericObjectPool<StorageClient> pool;
    private String[] nginxServers;
    private Logger logger;

    public FastdfsClientService(FastdfsProperties fdfsProp) throws Exception {
        this.fdfsProp = fdfsProp;
        this.logger = LoggerFactory.getLogger(getClass());
        init();
        create();
        info();
    }

    /**
     * 初始化全域性客戶端
     */
    private void init() throws Exception {
        this.prop = new Properties();
        this.logger.info("FastDFS: reading config file...");
        this.logger.info("FastDFS: fastdfs.connect_timeout_in_seconds=" + this.fdfsProp.getConnectTimeout());
        this.logger.info("FastDFS: fastdfs.network_timeout_in_seconds=" + this.fdfsProp.getNetworkTimeout());
        this.logger.info("FastDFS: fastdfs.charset=" + this.fdfsProp.getCharset());
        this.logger.info("FastDFS: fastdfs.http_anti_steal_token=" + this.fdfsProp.getHttpAntiStealToken());
        this.logger.info("FastDFS: fastdfs.http_secret_key=" + this.fdfsProp.getHttpSecretKey());
        this.logger.info("FastDFS: fastdfs.http_tracker_http_port=" + this.fdfsProp.getHttpTrackerHttpPort());
        this.logger.info("FastDFS: fastdfs.tracker_servers=" + this.fdfsProp.getTrackerServers());
        this.logger.info("FastDFS: fastdfs.connection_pool_max_total=" + this.fdfsProp.getConnectionPoolMaxTotal());
        this.logger.info("FastDFS: fastdfs.connection_pool_max_idle=" + this.fdfsProp.getConnectionPoolMaxIdle());
        this.logger.info("FastDFS: fastdfs.connection_pool_min_idle=" + this.fdfsProp.getConnectionPoolMinIdle());
        this.logger.info("FastDFS: fastdfs.nginx_servers=" + this.fdfsProp.getNginxServers());

        this.prop.put("fastdfs.connect_timeout_in_seconds", this.fdfsProp.getConnectTimeout());
        this.prop.put("fastdfs.network_timeout_in_seconds", this.fdfsProp.getNetworkTimeout());
        this.prop.put("fastdfs.charset", this.fdfsProp.getCharset());
        this.prop.put("fastdfs.http_anti_steal_token", this.fdfsProp.getHttpAntiStealToken());
        this.prop.put("fastdfs.http_secret_key", this.fdfsProp.getHttpSecretKey());
        this.prop.put("fastdfs.http_tracker_http_port", this.fdfsProp.getHttpTrackerHttpPort());
        this.prop.put("fastdfs.tracker_servers", this.fdfsProp.getTrackerServers());
        ClientGlobal.initByProperties(this.prop);
    }
    /**
     * 顯示初始化資訊
     */
    private void info() {
        this.logger.info("FastDFS parameter: ConnectionPoolMaxTotal ==> " + this.pool.getMaxTotal());
        this.logger.info("FastDFS parameter: ConnectionPoolMaxIdle ==> " + this.pool.getMaxIdle());
        this.logger.info("FastDFS parameter: ConnectionPoolMinIdle ==> " + this.pool.getMinIdle());
        this.logger.info("FastDFS parameter: NginxServer ==> " + Arrays.toString(this.nginxServers));
        this.logger.info(ClientGlobal.configInfo());
    }

    /**
     * 建立連線池
     */
    private void create() {
        this.config = new GenericObjectPoolConfig();
        this.logger.info("FastDFS Client: Creating connection pool...");
        this.config.setMaxTotal(Integer.parseInt(this.fdfsProp.getConnectionPoolMaxTotal()));
        this.config.setMaxIdle(Integer.parseInt(this.fdfsProp.getConnectionPoolMaxIdle()));
        this.config.setMinIdle(Integer.parseInt(this.fdfsProp.getConnectionPoolMinIdle()));
        StorageClientFactory factory = new StorageClientFactory();
        this.pool = new GenericObjectPool<StorageClient>(factory, this.config);
        this.nginxServers = this.fdfsProp.getNginxServers().split(",");
    }

    /**
     * Nginx伺服器負載均衡演算法
     *
     * @param servers 伺服器地址
     * @param address 客戶端IP地址
     * @return 可用的伺服器地址
     */
    private String getNginxServer(String[] servers, String address) {
        int size = servers.length;
        int i = address.hashCode();
        int index = abs(i % size);
        return servers[index];
    }

    /**
     * 帶有防盜鏈的下載
     *
     * @param fileGroup       檔案組名
     * @param remoteFileName  遠端檔名稱
     * @param clientIpAddress 客戶端IP地址
     * @return 完整的URL地址
     */
    public String autoDownloadWithToken(String fileGroup, String remoteFileName, String clientIpAddress) throws Exception {
        int ts = (int) (System.currentTimeMillis() / 1000);
        String token = ProtoCommon.getToken(remoteFileName, ts, ClientGlobal.getG_secret_key());
        String nginx = this.getNginxServer(this.nginxServers, clientIpAddress);
        return "http://" + nginx + "/" + fileGroup + "/" + remoteFileName + "?token=" + token + "&ts=" + ts;
    }

    /**
     * 上傳檔案,適合上傳圖片
     *
     * @param buffer 位元組陣列
     * @param ext    副檔名
     * @return 檔案組名和ID
     */
    public String[] autoUpload(byte[] buffer, String ext) throws Exception {
        String[] upload = this.upload(buffer, ext, null);
        return upload;
    }

    /**
     * 不帶防盜鏈的下載,如果開啟防盜鏈會導致該方法丟擲異常
     *
     * @param fileGroup       檔案組名
     * @param remoteFileName  遠端檔案ID
     * @param clientIpAddress 客戶端IP地址,根據客戶端IP來分配Nginx伺服器
     * @return 完整的URL地址
     */
    public String autoDownloadWithoutToken(String fileGroup, String remoteFileName, String clientIpAddress) throws Exception {
        if (ClientGlobal.getG_anti_steal_token()) {
            this.logger.error("FastDFS Client: You've turned on Token authentication.");
            throw new Exception("You've turned on Token authentication.");
        }
        String nginx = this.getNginxServer(this.nginxServers, clientIpAddress);
        return "http://" + nginx + fileGroup + "/" + remoteFileName;
    }

    // 後面還有好多方法,就不一一展示了
}複製程式碼

為了在IDEA中使用便捷的配置提示功能,我們需要建立後設資料檔案(resources/spring-configuration-metadata.json):

{
  "groups": [
    {
      "name": "fastdfs",
      "type": "com.bluemiaomiao.properties.FastdfsProperties",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    }
  ],
  "properties": [
    {
      "name": "connectTimeout",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "5"
    },
    {
      "name": "networkTimeout",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "30"
    },
    {
      "name": "charset",
      "type": "java.lang.String",
      "defaultValue": "UTF-8"
    },
    {
      "name": "httpAntiStealToken",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "false"
    },
    {
      "name": "httpSecretKey",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    },
    {
      "name": "httpTrackerHttpPort",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    },
    {
      "name": "trackerServers",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    },
    {
      "name": "connectionPoolMaxTotal",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "18"
    },
    {
      "name": "connectionPoolMaxIdle",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "18"
    },
    {
      "name": "connectionPoolMinIdle",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "2"
    },
    {
      "name": "nginxServers",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    }
  ],
  "hints": [
    {
      "name": "http_anti_steal_token",
      "values": [
        {
          "value": "false"
        },
        {
          "value": "true"
        }
      ]
    }
  ]
}複製程式碼

將自定義starter新增到專案

  • 建立SpringBoot專案,勾選Web選項,版本選擇1.5.20

  • 進入場景啟動器的專案目錄執行mvn clean install 將其安裝到本地

  • 在POM.xml檔案中新增依賴:
<dependency>
    <groupId>com.bluemiaomiao</groupId>
    <artifactId>fastdfs-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>複製程式碼

記得開啟IDEA的自動匯入功能

  • 建立配置檔案application.properties
fastdfs.nginx-servers=192.168.80.2:8000,192.168.80.3:8000,192.168.80.4:8000
fastdfs.tracker-servers=192.168.80.2:22122,192.168.80.3:22122,192.168.80.4:22122
fastdfs.http-secret-key=2scPwMPctXhbLVOYB0jyuyQzytOofmFCBIYe65n56PPYVWrntxzLIDbPdvDDLJM8QHhKxSGWTcr+9VdG3yptkw
fastdfs.http-anti-steal-token=true
fastdfs.http-tracker-http-port=8080
fastdfs.network-timeout=30
fastdfs.connect-timeout=5
fastdfs.connection-pool-max-idle=18
fastdfs.connection-pool-min-idle=2
fastdfs.connection-pool-max-total=18
fastdfs.charset=UTF-8複製程式碼

或者使用application.yml

fastdfs:
  charset: UTF-8
  connect-timeout: 5
  http-secret-key: 2scPwMPctXhbLVOYB0jyuyQzytOofmFCBIYe65n56PPYVWrntxzLIDbPdvDDLJM8QHhKxSGWTcr+9VdG3yptkw
  network-timeout: 30
  http-anti-steal-token: true
  http-tracker-http-port: 8080
  connection-pool-max-idle: 20
  connection-pool-max-total: 20
  connection-pool-min-idle: 2
  nginx-servers: 192.168.80.2:8000,192.168.80.3:8000,192.168.80.4:8000
  tracker-servers: 192.168.80.2:22122,192.168.80.3:22122,192.168.80.4:22122複製程式碼
  • 建立控制器類測試方法
// controllers.DownloadController.java
@Controller
@RequestMapping(value = "/download")
public class DownloadController {

    @Autowired
    private FastdfsClientService service;

    @ResponseBody
    @RequestMapping(value = "/image")
    public String image() throws Exception {
        // 之前上傳過的資料,實際應用場景應該使用SQL資料庫來儲存
        return service.autoDownloadWithToken("group1", "M00/00/00/wKhQA1ysjSGAPjXbAAVFOL7FJU4.tar.gz", "192.168.80.1");
    }
}複製程式碼

歡迎Java工程師朋友們加入Java進階高階架構:809389099 群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼, MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料) 合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

相關文章