Spring MVCとRestTemplateによるCorsProxyの実現


CORS PROXYとは
ドメイン間の問題は皆さんご存知のように、ajaxリクエストは別のドメイン名の下のインタフェースを直接呼び出すことはできません.jsonpは一定の問題を解決することができますが、Post、PUT、DELETEなどの高度な機能のサポートにはどうしようもありません.
 
この問題を解決するために、高級ブラウザではCORSがサポートされ始めました.CORSはheadersで関連パラメータを定義し、ブラウザの私のインタフェースが外部のサイトに要求されることを許可するかどうか、どのMethodを許可するかなどを教えてくれました.
具体的な使い方については、関連ドキュメントを参照してください.https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS
 
CORSがあって、次の問題がまた来て、CORSはサービス側に関連するheadersを加えなければならないので、サードパーティのインタフェースがCORSを有効にしていないのはどうしますか?
彼らが実現に間に合わないのではなく、安全性のために実現したくないのです.CORSには一定の危険性があるからです.http://www.freebuf.com/articles/web/18493.html
CORSには定義があるAccess-Control-Allow-Originドメイン間リクエストが許可されているソースを示すために使用され、上記のようなサードパーティ共通インタフェースがCORSを開放している場合、ソースは不確定であるため、ここですべてのソースを許可するように構成すると非常に危険である.
ハッカーはXSSを利用して関連攻撃を行う可能性が高い.
 
だからどうやってこのような第三者のインターフェースに対処しましたか?
やっとCORS Proxyを引き出しました!
CORS Proxyはクライアントとサードパーティサーバの間のエージェントサーバであり、このサーバは自分でしか使用できません(Access-Control-Allow-Originクライアントのアドレスに設定します).
CORS Proxy内部では、Http Clientでサードパーティサーバを要求します.
注意!コードで書かれたHttp Clientは、server側の制限ではなく、ブラウザが追加したセキュリティ保護措置であるため、ドメイン間の問題は存在しません.
 
CORS PROXY実現原理
CORS Proxyの原理は実は簡単で、主に3つのことをしています.
  • 認証
  • 転送要求
  • CORS関連headers付き
  • Javaでどうやって実現したの?
    Spring MVCとSpringのRestTemplateを用いてCORS Proxyを実現した.
    @Controller
    @RequestMapping(value = "/corsproxy")
    public class CorsProxyController {
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        private RestTemplate restTemplate;
        private HeaderFilter headerFilter;
        private TargetUrlFilter targetUrlFilter;
        private final String CORS_PREFIX = "corsproxy/";
        private final String HTTP_PREFIX = "http/";
        private final String HTTPS_PREFIX = "https/";
    
        @RequestMapping(value = "/**")
        public ResponseEntity<byte[]> proxy(HttpServletRequest request, @RequestBody byte[] body, @RequestHeader MultiValueMap headers) throws UnsupportedEncodingException {
    
            String url = request.getRequestURI();
            String queryString = request.getQueryString();
    
            if (queryString != null && queryString != "") {
                url = url + "?" + queryString;
            }
    
            String targetUrl = getTargetUrl(url);
    
            if (!targetUrlFilter.checkUrl(targetUrl)) {
                return new ResponseEntity<byte[]>(HttpStatus.FORBIDDEN);
            }
    
            ResponseEntity<byte[]> result = null;
            try {
                result = restTemplate.exchange(new URI(targetUrl), HttpMethod.valueOf(request.getMethod()), new HttpEntity<byte[]>(body, headers), byte[].class);
            } catch (HttpClientErrorException exp) {
                return new ResponseEntity<byte[]>(exp.getResponseBodyAsByteArray(), getResponseHeaders(exp.getResponseHeaders()), exp.getStatusCode());
            } catch (HttpServerErrorException exp) {
                return new ResponseEntity<byte[]>(exp.getResponseBodyAsByteArray(), getResponseHeaders(exp.getResponseHeaders()), exp.getStatusCode());
            } catch (Exception exp) {
                return new ResponseEntity<byte[]>(exp.getMessage().getBytes("utf-8"), getResponseHeaders(new HttpHeaders()), HttpStatus.INTERNAL_SERVER_ERROR);
            }
    
            return new ResponseEntity<byte[]>(result.getBody(), getResponseHeaders(result.getHeaders()), result.getStatusCode());
        }
    
        @Resource(name = "restTemplate")
        public void setRestTemplate(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
        }
    
        @Resource(name = "headerFilter")
        public void setHeaderFilter(HeaderFilter headerFilter) {
            this.headerFilter = headerFilter;
        }
    
        @Resource(name = "targetUrlFilter")
        public void setTargetUrlFilter(TargetUrlFilter targetUrlFilter) {
            this.targetUrlFilter = targetUrlFilter;
        }
    
        private String getTargetUrl(String url) {
            String targetUrl = url.substring(url.indexOf(CORS_PREFIX) + CORS_PREFIX.length());
            if (targetUrl.indexOf(HTTP_PREFIX) == 0) {
                targetUrl = "http://" + targetUrl.substring(HTTP_PREFIX.length());
            } else if (targetUrl.indexOf(HTTPS_PREFIX) == 0) {
                targetUrl = "https://" + targetUrl.substring(HTTPS_PREFIX.length());
            }
            return targetUrl;
        }
    
        private HttpHeaders getResponseHeaders(HttpHeaders originHeaders) {
            HttpHeaders header = new HttpHeaders();
            for (Entry> item : originHeaders.entrySet()) {
                if (headerFilter.needRemoveHeader(item.getKey(), item.getValue().toString())) {
                    continue;
                }
                header.put(item.getKey(), item.getValue());
            }
    
            return header;
        }
    
    }
    

    コードは複雑ではありません.ここではControllerです.もう一つのFilterがあります.
    public class CorsFilter extends OncePerRequestFilter  implements Filter{
        private HeaderHelper headerHelper;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            for(Map.Entry extends String, ? extends List<String>> header: headerHelper.getHeadersMap().entrySet()){
                Joiner joiner = Joiner.on("; ").skipNulls();
                String value = joiner.join(header.getValue());
                response.addHeader(header.getKey(),value);
            }
            filterChain.doFilter(request, response);
        }
    
        @Resource(name = "headerHelper")
        public void setHeaderHelper(HeaderHelper headerHelper) {
            this.headerHelper = headerHelper;
        }
    }
    

    コードの中のHeaderHelper HeaderFilterTargetUrlFilterあまり論理的ではなく、構成を読み取っただけです.
     
    いくつかの注意事項
    この実現は難しくないが、その中の1つの部分は大きな穴だ.
    コーディングの問題
    最初は私のCORS Proxyで受けていたRequestBodyはいStringで、RestTemplateリクエストの戻り値もStringでしたが、後になってその中に多くの問題があることに気づきました.Clientとserverのコード仕様は必ずしも標準ではありませんが、実はあなたはエージェントサーバとして、コードを行う必要はありません.clientがあなたに渡したものは何なのか、そのままserverに伝えればいいので、私たちが作成するときはすべて使いましたbyte[]、これからは問題ありません.
     
    もう一つはGETQueryString、ここは穴だらけ!中国語であればRequestから得られるものは符号化されたものであり、自動的に中国語に復号されることはない.そしてRestTemplate自動でエンコードされるので、クライアントが受け取ったのは2回のエンコードの内容で、1回だけ復号すると、中国語は得られません.
    どうやって解決しますか?呼び出しRestTemplateの場合はやめましょうStringタイプのurlを渡すのではなく、1つURIオブジェクトを渡すことでRestTemplate自動符号化されなくなります.
     
    Transfer-Encoding
    最初はserver側から戻ってきたbodyとheadersをそのままclientに渡しましたが、clientはずっと接続を中断していました!データが全く届かない.
    直感はheadersの中のいくつかに問題があると思います.そこで排除法で一つ一つ試してみると、問題はTransfer-Encodingにある.
    これはブロック転送符号化だったのか、なぜこのヘッダを加えると問題があったのか.
    私のエージェントはすでにserver側とすべてのデータを転送し終わったので、私はすべてのデータをclientに返しましたが、エージェントはまたclientに自分がブロック転送だと言いました...clientは理解できません...
    最後に私はまた1つHeaderFilterを書いて、それからいくつかの伝送する必要のないheaderを濾過しました.
     
    Content-Length Content-Length穴でもあるのですが、なぜでしょうか?サーバ側から伝わってきたのでContent-Lengthクライアントに直接伝えることはできません.
    クライアントとproxyの間にgzipがあり、serverとproxyの間にgzipがないシーンがあります.
    これはproxyがクライアントに伝えるContent-Length圧縮前の長さで、問題が発生します.
    どうやって解決しますか?同様に上のHeaderFilterを使うと、フィルタリングしてproxyがいるサービス側が自動的にResponse加えてContent-Lengthの、手動で指定する必要はありません.
     
    Access-Control-Allow-Origin
    CORS規格にはいくつかのヘッダがあり、そのうちの1つはAccess-Control-Allow-Originで、どのドメイン名でドメインをまたいでアドレスを要求できるかを表しています.
    私の上のHeaderHelper自動でこのヘッドを付けますが、後で奇抜なcaseに遭遇しました!
    元の住所にはAccess-Control-Allow-Originが付いていて、それから私のHeaderHelperもう一つ追加します.
    それからAccess-Control-Allow-Originこのヘッドはとても穴のお父さんで、それは*であってもよいし、http://www.dozer.ccであってもよいが、それはhttp://www.dozer.cc, http://www.baidu.comであってはならない.
    ワイルドカードかドメイン名が1つしか許可されていないということです!
    私は2回プラスして、解析されました\*,\*、それからブラウザは認識していません...
     
    複数のドメイン名があればどうしますか?それはこのヘッダを動的に生成するしかありません.
    しかし、セキュリティの観点から、混用せずに独自のCorsPorxyを独立して導入することを強くお勧めします.
     
    リダイレクトの問題
    この話題の設計のものは比較的に多くて、すでに自作の文章:RestTemplateの自動リダイレクト機能を無効にします
     
     
    後ろにはどんな穴がありますか?私たちはまだ出会っていませんが、多くのものを配置に書いて、将来問題があることに気づいたら配置を変更しました.実現論理はそれだけで、配置できるところもそれだけで、万変はその宗から離れない.
    本作品はDozerによって作成され、知識共有署名-非商業的使用4.0国際ライセンス契約を採用してライセンスされている.