利用nginx進(jìn)行url隱藏

????最近接觸到一個(gè)比較常見(jiàn)的需求,用戶在獲取線上資源時(shí),需要先對(duì)用戶的身份進(jìn)行認(rèn)證,認(rèn)證通過(guò)之后用戶可以正常的下載相關(guān)的資源。這個(gè)過(guò)程中,其實(shí)主要是以下功能點(diǎn)

  • 驗(yàn)證用戶的身份
  • 將用戶請(qǐng)求的url轉(zhuǎn)換到實(shí)際的資源上

????其中,驗(yàn)證身份的功能與具體業(yè)務(wù)有關(guān),所以在這里主要是介紹一下如何將url轉(zhuǎn)換到實(shí)際的資源上。對(duì)于這個(gè)做法,最直接的想法就是服務(wù)端在驗(yàn)證用戶身份之后,直接讀取資源然后寫入輸出流,用戶即可獲取對(duì)應(yīng)的資源。這種方案的問(wèn)題是,資源在業(yè)務(wù)服務(wù)上面做轉(zhuǎn)了一手。如果你的業(yè)務(wù)系統(tǒng)中有采用nginx進(jìn)行分發(fā),也可以試試本文這邊比較有意思的方案,借助nginx的error_page功能進(jìn)行內(nèi)部跳轉(zhuǎn),從而達(dá)到url隱藏的功能。

????首先,使用nginx-url-hide用來(lái)模擬url到資源的映射,該項(xiàng)目使用的spring boot 2.4.2構(gòu)建的項(xiàng)目。具體的controller如下。最關(guān)鍵的代碼是將具體的資源地址放到了response的header中,并且返回了一個(gè)自定義的http status code (599)。這個(gè)狀態(tài)碼將在拋出之后,被nginx所捕獲,并根據(jù)header中的target字段下載指定的資源。

package cn.yxsk.application;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class CheckUrlController {
    private static final Logger LOGGER = LoggerFactory.getLogger(CheckUrlController.class);
    
    private static final int REDIRECT_CODE = 599;
    private final Map<String, String> targetUri;
    
    public CheckUrlController() {
        Map<String, String> targetUri = new HashMap<>();
        targetUri.put("/res1", "/hello.jpg");
        this.targetUri = Collections.unmodifiableMap(targetUri);
    }
    
    @RequestMapping("**")
    public void check(HttpServletRequest request, HttpServletResponse response) {
        String query = request.getQueryString();
        LOGGER.debug("請(qǐng)求的url地址為{}", request.getRequestURL().append("?").append(query));
        
        String uri = request.getRequestURI();
        String target = this.targetUri.get(uri);
        
        if (ObjectUtils.isEmpty(target)) {
            response.setStatus(HttpStatus.NOT_FOUND.value());
        } else {
            StringBuilder targetUri = new StringBuilder(32).append(target);
            if (StringUtils.hasText(query)) {
                targetUri.append("?").append(request.getQueryString());
            }
            response.setHeader("target", targetUri.toString());
            response.setStatus(REDIRECT_CODE);
        }
    }
}

????其中nginx的配置如下。nginx在捕獲到自定義的狀態(tài)碼之后,進(jìn)行了一個(gè)內(nèi)部重定向,因此這個(gè)資源是無(wú)法在客戶端直接被訪問(wèn)到的,只有通過(guò)業(yè)務(wù)服務(wù)器隱藏的地址才能夠進(jìn)行訪問(wèn)。通過(guò)對(duì)請(qǐng)求的預(yù)處理,可以對(duì)用戶的請(qǐng)求進(jìn)行限制。

server {
    listen          80;
    server_name     redirect.yxsk.cn;

    fastcgi_intercept_errors on;
    proxy_intercept_errors on;

    location / {
        proxy_http_version 1.1;
        proxy_redirect  off;
        proxy_set_header Connection      "";
        proxy_set_header Host            $http_host;
        proxy_set_header X-Real-IP       $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Cookie          $http_cookie;
        proxy_pass http://127.0.0.1:7070;

        error_page 599 = @inner_redirect;
    }

    location @inner_redirect {
        set $target $upstream_http_target;
        root /data/wwwroot/res-static;
        try_files $target $document_root;
    }
}

????在整個(gè)nginx配置中,最關(guān)鍵的是如下的幾個(gè)配置,其中

  • error_page :用于捕獲錯(cuò)誤碼信息,然后進(jìn)行一次nginx的內(nèi)部跳轉(zhuǎn)。配置說(shuō)明
  • proxy_intercept_errors:該配置用于確定大于300的狀態(tài)碼,是否直接返回給客戶端還是允許nginx進(jìn)行一次內(nèi)部處理,此配置主要是用于proxy_pass 的配置。配置說(shuō)明
  • fastcgi_intercept_errors:與proxy_intercept_errors的作用類似,只是用于FastCGI 服務(wù)返回的狀態(tài)碼。配置說(shuō)明

采用proxy_intercept_errors還是fastcgi_intercept_errors取決于請(qǐng)求轉(zhuǎn)發(fā)給后端業(yè)務(wù)服務(wù)器的方式。對(duì)于業(yè)務(wù)服務(wù)器寫入的header字段target,在nginx的配置文件獲取時(shí)為$upstream_http_target

當(dāng)然,該方法目前目前一定的限制,這個(gè)限制主要是error_page 的限制。無(wú)論客戶端采用的是post還是get,經(jīng)過(guò)error_page的內(nèi)部跳轉(zhuǎn)之后,都會(huì)被轉(zhuǎn)為get

有沒(méi)有什么有意思的小需求,可以提供一下一起探討的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容