像圖片或者HTML文件這樣的靜態資源,在瀏覽器中打開正確的URL即可下載,只要該資源是放在應用程序的目錄下,或者放在應用程序目錄的子目錄下,而不是放在WEB-INF下,tomcat服務器就會將該資源發送到瀏覽器。然而,有時靜態資源是保存在應用程序目錄之外,或者是保存在某一個數據庫中,或者有時需要控制它的訪問權限,防止其他網站交叉引用它。如果出現以上任意一種情況,都必要通過編程來發送資源。
簡言之,通過編程進行的文件下載,使你可以有選擇地將文件發送到瀏覽器。本篇博客將介紹如果通過編程把資源發送到瀏覽器,並通過兩個案例進行示范。
一 文件下載概覽
為了將像文件這樣的資源發送到瀏覽器,需要在控制器中完成以下工作:
- 對請求處理方法添加HttpServletResponse、HttpServletRequest參數;
- 將相應的內容類型設為文件的內容類型。content-Type標題在某個實體的body中定義數據的類型,並包含媒體類型和子類型標識符。如果不清楚內容類型,並且希望瀏覽器式中顯示Save As(另存為)對話框,則將它設為application/octet-stream。這個值是不區分大小寫的(HTTP Content-type 對照表)。
- 添加一個屬性為content-Disposition的HTTP的響應標題,並賦予attachement;filename=fileName,這里的fileName是默認文件名,應該出現在File Download(文件下載)對話框中,它通常與文件同名,但是也並非一定如此。
文件下載的流程具體如下:
- 通過瀏覽器,輸入URL請求控制器的請求處理函數;
- 請求處理方法根據文件路徑,將文件轉換為輸入流;
- 通過輸出流將剛才已經轉為輸入流的文件,輸出給用戶(瀏覽器);
例如,以下代碼將一個文件發送到瀏覽器:
//下載文件:需要設置消息頭 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中,直至文件讀取完畢。
注意:這里將文件轉換為輸入流使用的是:
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());
代碼更短,運行速度更快。
二 范例1:隱藏資源
我們創建一個download應用程序,用於展示如何向瀏覽器發送文件。
1、目錄結構
2、 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; } }
3、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; } }
4、視圖
控制器的第一個請求處理方法是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; }
5、配置文件
下面給出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>
6、測試
將應用程序部署到tomcat服務器,並在網頁輸入以下URL:
http://localhost:8008/download/login
將會看到一個表單:
輸入用戶名"paul",密碼"secret",將會跳轉到文件下載頁面:
點擊超鏈接,下載secret.pdf文件。
三 范例2:防止交叉引用
心懷叵測的競爭對手有可能通過交叉引用“竊取”你的網站資產,比如,將你的資料公然放在他的資源上,好像那些東西原本就屬於他的一樣。如果通過編程控制,使其只有當referer中包含你的域名時才發出資源,就可以防止那種情況發生。當然,那些心意堅決的竊賊仍有辦法下載到你的東西,只不過不像之前那么簡單罷了。
1、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。
2、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>
3、測試
將應用程序部署到tomcat服務器,並在網頁輸入以下URL:
http://localhost:8008/download/images.html
將會看到如下效果:
相反,如果直接在瀏覽器輸入如下URL將會獲取圖片失敗:
http://localhost:8008/get-image/1
參考文章
[2]SpringMVC(六):@RequestMapping下使用@RequestHeader綁定請求報頭的屬性值、@CookieValue綁定請求中的Cookie值
[3]Spring MVC學習指南