Spring MVC -- 下載文件

像圖片或者HTML文件這樣的靜態資源,在瀏覽器中打開正確的URL便可下載,只要該資源是放在應用程序的目錄下,或者放在應用程序目錄的子目錄下,而不是放在WEB-INF下,tomcat服務器就會將該資源發送到瀏覽器。然而,有時靜態資源是保存在應用程序目錄以外,或者是保存在某一個數據庫中,或者有時須要控制它的訪問權限,防止其餘網站交叉引用它。若是出現以上任意一種狀況,都必要經過編程來發送資源。css

簡言之,經過編程進行的文件下載,使你能夠有選擇地將文件發送到瀏覽器。本篇博客將介紹若是經過編程把資源發送到瀏覽器,並經過兩個案例進行示範。html

一 文件下載概覽

爲了將像文件這樣的資源發送到瀏覽器,須要在控制器中完成如下工做:java

  • 對請求處理方法添加HttpServletResponse、HttpServletRequest參數;
  • 將相應的內容類型設爲文件的內容類型。content-Type標題在某個實體的body中定義數據的類型,幷包含媒體類型和子類型標識符。若是不清楚內容類型,而且但願瀏覽器式中顯示Save As(另存爲)對話框,則將它設爲application/octet-stream。這個值是不區分大小寫的(HTTP Content-type 對照表)。
  • 添加一個屬性爲content-Disposition的HTTP的響應標題,並賦予attachement;filename=fileName,這裏的fileName是默認文件名,應該出如今File Download(文件下載)對話框中,它一般與文件同名,可是也並不是必定如此。

文件下載的流程具體以下:web

  • 經過瀏覽器,輸入URL請求控制器的請求處理函數;
  • 請求處理方法根據文件路徑,將文件轉換爲輸入流;
  • 經過輸出流將剛纔已經轉爲輸入流的文件,輸出給用戶(瀏覽器);

例如,如下代碼將一個文件發送到瀏覽器:spring

        //下載文件:須要設置消息頭
        response.setCharacterEncoding("UTF-8");
        response.addHeader("content-Type", "application/octet-stream");   //指定文件類型 MIME類型:二進制文件(任意文件)        

        String encodeFileName = null;
        
        if(userAgent.contains("MSIE") || userAgent.contains("Trident") || (userAgent.contains("GECKO") && userAgent.contains("RV:11"))) {
            //處理IE瀏覽器下載中文文件名亂碼問題            
            encodeFileName = URLEncoder.encode( filename,"UTF-8");            
        }else {
            encodeFileName = "=?UTF-8?B?" + new String(Base64.encodeBase64(filename.getBytes("UTF-8"))) + "?=";
            //encodeFileName = new String(filename.getBytes("UTF-8"),"ISO-8859-1");    
        }
        
        System.out.println(filename + ":" + encodeFileName);
        //若是有換行,對於文本文件沒有什麼問題,可是對於其餘格式:好比AutoCAD,Word,Excel等文件下載下來的文件中就會多出來一些換行符//0x0d和0x0a,這樣可能致使某些格式的文件沒法打開
        response.reset();
        response.addHeader("content-Disposition", "attachement;filename="+encodeFileName);  //告訴瀏覽器該文件以附件方式處理,而不是去解析
        
        //經過文件地址,將文件轉換爲輸入流
        InputStream in = request.getServletContext().getResourceAsStream(filename);
        
        //經過輸出流將剛纔已經轉爲輸入流的文件,輸出給用戶
        ServletOutputStream out= response.getOutputStream();
        
        byte[] bs = new byte[1000];
        int len = -1;
        while((len=in.read(bs)) != -1) {
            out.write(bs,0,len);
        }
        out.close();
        in.close();

爲了編程將一個文件發送到瀏覽器,首先要讀取該文件做爲InputStream ,隨後,獲取HttpServletResponse的OutputStream;循環從in中讀取1000個字節,寫入out中,直至文件讀取完畢。數據庫

注意:這裏將文件轉換爲輸入流使用的是:apache

request.getServletContext().getResourceAsStream(filename);

filename指的是相對當前應用根路徑下的文件。若是給定的路徑是絕對路徑,能夠採用以下函數將文件轉換爲輸入流:編程

FileInputStream in = new FileInputStream(filename) ;

將文件發送到HTTP客戶端的更好方法是使用Java NIO的Files.copy()方法:瀏覽器

        //將文件的虛擬路徑轉爲在文件系統中的真實路徑
        String realpath = request.getServletContext().getRealPath(filename);
        System.out.print(realpath);
        Path file = Paths.get(realpath);
        Files.copy(file,response.getOutputStream());

代碼更短,運行速度更快。spring-mvc

二 範例1:隱藏資源

咱們建立一個download應用程序,用於展現如何向瀏覽器發送文件。

一、目錄結構

二、 Login類

Login類有兩個屬性,登陸名和登陸密碼:

package domain;

import java.io.Serializable;

//登陸實體類
public class Login  implements Serializable {
    private static final long serialVersionUID = 1L;

    //用戶名
    private String userName;
    //用戶密碼
    private String password;
    
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

三、ResourceController類

在這個應用程序中,由ResourceController類處理用戶登陸,並將一個secret.pdf文件發送給瀏覽器。secret.pdf文件放在/WEB-INF/data目錄下,所以不能直接方法。只能獲得受權的用戶,才能看到它,若是用戶沒有登陸,應用程序就會跳轉到登陸頁面。

package controller;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import domain.Login;

@Controller
public class ResourceController {
    
    private static final Log logger = LogFactory.getLog(ResourceController.class);
    
    //請求URL:/login 
    @RequestMapping(value="/login")
    public String login(@ModelAttribute Login login, HttpSession session, Model model) {
        model.addAttribute("login", new Login());
        //校驗用戶名和密碼
        if ("paul".equals(login.getUserName()) &&
                "secret".equals(login.getPassword())) {
            //設置sessopm屬性"loggedIn"
            session.setAttribute("loggedIn", Boolean.TRUE);
            //校驗經過 請求轉發到Main.jsp頁面
            return "Main";
        } else {
            //校驗失敗 請求轉發到LoginForm.jsp頁面
            return "LoginForm";
        }
    }

    //請求URL:/download-resource 
    @RequestMapping(value="/download-resource")
    public String downloadResource(HttpSession session, HttpServletRequest request,
            HttpServletResponse response, Model model) {
        //若是用戶沒有登陸
        if (session == null || 
                session.getAttribute("loggedIn") == null) {
            model.addAttribute("login", new Login());
            //請求轉發到LoginForm.jsp頁面 等待用戶登陸
            return "LoginForm";
        }
        //用戶已經登陸  獲取待下載文件夾/WEB-INF/data在文件系統的真實路徑
        String dataDirectory = request.
                getServletContext().getRealPath("/WEB-INF/data");
        //建立Path對象  文件爲/WEB-INF/data/secret.pdf
        Path file = Paths.get(dataDirectory, "secret.pdf");
        //若是文件存在 下載文件
        if (Files.exists(file)) {
            //指定文件類型 pdf類型
            response.setContentType("application/pdf");
          //告訴瀏覽器該文件以附件方式處理,而不是去解析
            response.addHeader("Content-Disposition", "attachment; filename=secret.pdf");
            try {
                Files.copy(file, response.getOutputStream());
            } catch (IOException ex) {
            }
        }
        return null;
    }
}

四、視圖

控制器的第一個請求處理方法是login(),將用戶請求轉發到登陸表單LoginForm.jsp:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>
<div id="global">
<form:form modelAttribute="login" action="login" method="post">
    <fieldset>
        <legend>Login</legend>
        <p>
            <label for="userName">User Name: </label>
            <form:input id="userName" path="userName" cssErrorClass="error"/>
        </p>
        <p>
            <label for="password">Password: </label>
            <form:password id="password" path="password" cssErrorClass="error"/>
        </p>
        <p id="buttons">
            <input id="reset" type="reset" tabindex="4">
            <input id="submit" type="submit" tabindex="5" 
                value="Login">
        </p>
    </fieldset>
</form:form>
</div>
</body>
</html>

當咱們輸入用戶名"paul",密碼"secret",將會成功登陸,而後請求轉發到Main.jsp頁面,該頁面包含一個連接,點擊它能夠將secret.pdf文件下載下來:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<title>Download Page</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>
<div id="global">
    <h4>Please click the link below.</h4>
    <p>
        <a href="download-resource">Download</a>
    </p>
</div>
</body>
</html>

控制器的第二個方法downloadResource(),它經過驗證session屬性loggedIn是否存在,來覈實用戶是否已經成功登陸。若是找到該屬性,就會將文件發送到瀏覽器。不然,用戶就會跳轉到登陸頁面。

main.css:

#global {
    text-align: left;
    border: 1px solid #dedede;
    background: #efefef;
    width: 560px;
    padding: 20px;
    margin: 100px auto;
}

form {
  font:100% verdana;
  min-width: 500px;
  max-width: 600px;
  width: 560px;
}

form fieldset {
  border-color: #bdbebf;
  border-width: 3px;
  margin: 0;
}

legend {
    font-size: 1.3em;
}

form label { 
    width: 250px;
    display: block;
    float: left;
    text-align: right;
    padding: 2px;
}

#buttons {
    text-align: right;
}
#errors, li {
    color: red;
}
.error {
    color: red;
    font-size: 9pt;    
}
View Code

五、配置文件

下面給出springmvc-config.xml文件的全部內容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd     
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="controller" />
    <mvc:annotation-driven/>
    <mvc:resources mapping="/css/**" location="/css/" />
    <mvc:resources mapping="/*.html" location="/" />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
</beans>

部署描述符(web.xml文件):

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" 
        xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
            http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> 
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/config/springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>    
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>
View Code

六、測試

將應用程序部署到tomcat服務器,並在網頁輸入如下URL:

http://localhost:8008/download/login

將會看到一個表單:

輸入用戶名"paul",密碼"secret",將會跳轉到文件下載頁面:

點擊超連接,下載secret.pdf文件。

三 範例2:防止交叉引用

心懷叵測的競爭對手有可能經過交叉引用「竊取」你的網站資產,好比,將你的資料公然放在他的資源上,好像那些東西本來就屬於他的同樣。若是經過編程控制,使其只有當referer中包含你的域名時才發出資源,就能夠防止那種狀況發生。固然,那些心意堅定的竊賊仍有辦法下載到你的東西,只不過不像以前那麼簡單罷了。

一、ImageController類

download應用提供了ResourceController類,使其僅當referer不爲null且包含你的域名時時,纔將圖片發送給瀏覽器,這樣能夠防止僅在瀏覽器中輸入網址就能下載的狀況發生:

package controller;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class ImageController {
    
    private static final Log logger = LogFactory.getLog(ImageController.class);
    
    //請求URL:/get-image  使用了路徑變量id 
    @RequestMapping(value="/get-image/{id}", method = RequestMethod.GET)
    public void getImage(@PathVariable String id, 
            HttpServletRequest request, 
            HttpServletResponse response, 
            @RequestHeader(value = "referer") String referer,
            @RequestHeader(value = "User-Agent", required = true, defaultValue = "-999") String userAgent) {
        System.out.println(referer);
        System.out.println(userAgent);
        //判斷referer標題是否爲null 而且是從本身的域名發出的資源請求?
        if (referer != null && referer.contains("http://localhost:8008")) {
            //獲取待下載文件夾/WEB-INF/image在文件系統的真實路徑
            String imageDirectory = request.getServletContext().getRealPath("/WEB-INF/image");
          //建立Path對象  文件爲/WEB-INF/image/id.jpg
            Path file = Paths.get(imageDirectory, id + ".jpg");
            //文件存在 則下載文件
            if (Files.exists(file)) {
                //指定文件類型 img類型
                response.setContentType("image/jpg");
                //告訴瀏覽器該文件以附件方式處理,而不是去解析
                //response.addHeader("Content-Disposition", "attachment; filename="+id + ".jpg");
                try {
                    Files.copy(file, response.getOutputStream());
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

咱們在請求處理方法getImage()的簽名中使用了@RequestHeader(value = "referer") String referer參數,referer用來獲取獲取來訪者地址。只有經過連接訪問當前頁的時候,才能獲取上一頁的地址;不然referer的值爲null,經過window.open打開當前頁或者直接輸入地址,也爲null。

二、images.html

利用images.html,能夠對這個應用進行測試:

<!DOCTYPE html>
<html>
<head>
    <title>Photo Gallery</title>
</head>
<body>
  <img src="get-image/1"/>
  <img src="get-image/2"/>
  <img src="get-image/3"/>
  <img src="get-image/4"/>
  <img src="get-image/5"/>
  <img src="get-image/6"/>
  <img src="get-image/7"/>
  <img src="get-image/8"/>
  <img src="get-image/9"/>
  <img src="get-image/10"/>
</body>
</html>

三、測試

將應用程序部署到tomcat服務器,並在網頁輸入如下URL:

http://localhost:8008/download/images.html

將會看到以下效果:

相反,若是直接在瀏覽器輸入以下URL將會獲取圖片失敗:

http://localhost:8008/get-image/1

參考文章

[1]spring項目獲取ServletContext

[2]SpringMVC(六):@RequestMapping下使用@RequestHeader綁定請求報頭的屬性值、@CookieValue綁定請求中的Cookie值 

[3]Spring MVC學習指南

[4]request.getHeader("referer")的做用

[5]http請求頭中Referer的含義和做用

相關文章
相關標籤/搜索