????最近接觸到一個(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)有什么有意思的小需求,可以提供一下一起探討的