spring mvc 复习整理三(仗剑走天涯) 跨域 当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同时,就会产生跨域
这里先写点spring mvc相关的跨域吧 剩下的知识等学完的在写点吧
同源策略 同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本的安全功能。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port)
跨域的案例
非同源限制 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。
无法接触非同源网页的 DOM
无法向非同源地址发送 AJAX 请求
两种请求 跨域资源共享”(Cross-origin resource sharing)
简单请求 请求方法是下面的其中一个:HEAD
GET
POST
对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段
HTTP的头信息不超出以下几种字段:Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
Content-Type
:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
对于简单请求,浏览器直接发出CORS请求。就是在头信息之中,增加一个Origin字段 Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求
1 2 3 4 5 6 GET /cors HTTP/1.1 Origin : http://127.0.0.1:8848Host : api.ydlclass.comAccept-Language : en-USConnection : keep-aliveUser-Agent : Mozilla/5.0...
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获
如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段
1 2 3 4 Access-Control-Allow-Origin : http://127.0.0.1:8848Access-Control-Allow-Credentials : trueAccess-Control-Expose-Headers : FooBarContent-Type : text/html; charset=utf-8
Access-Control-Allow-Origin
:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
Access-Control-Allow-Credentials
: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可
Access-Control-Expose-Headers
:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定
withCredentials 属性
:CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段 为true 另一方面,必须在AJAX请求中打开withCredentials属性
1 2 var xhr = new XMLHttpRequest();xhr.withCredentials = true ;
否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理 但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials
1 xhr.withCredentials = false ;
如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie
非简单请求
预检请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。OPTIONS
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错
浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求
1 2 3 4 5 6 7 8 OPTIONS /cors HTTP/1.1 Origin : http://127.0.0.1:8848Access-Control-Request-Method : PUTAccess-Control-Request-Headers : X-Custom-HeaderHost : api.ydlclass.comAccept-Language : en-USConnection : keep-aliveUser-Agent : Mozilla/5.0...
预检请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源
Access-Control-Request-Method
:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
Access-Control-Request-Headers
:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
预检请求的响应
服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应
如果服务器否定了预检请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。
服务器回应的其他CORS相关字段
1 2 3 4 Access-Control-Allow-Methods : GET, POST, PUTAccess-Control-Allow-Headers : X-Custom-HeaderAccess-Control-Allow-Credentials : trueAccess-Control-Max-Age : 1728000
Access-Control-Allow-Methods
:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
Access-Control-Allow-Headers
:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在预检中请求的字段。
Access-Control-Allow-Credentials
:该字段与简单请求时的含义相同。
Access-Control-Max-Age
:该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求
跨域案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 商品</title > </head > <body > <script src ="https://unpkg.com/axios/dist/axios.min.js" > </script > <script src ="https://code.jquery.com/jquery-3.1.1.min.js" > </script > <h1 > 商品页面</h1 > <script > $.ajax( { type : "GET" , url : "http://localhost:8080/goods/findAll" , dataType : "json" , success : function (data ) { console .log("get请求!---------------------" ) console .log(data) } }); $.ajax( { type : "GET" , url : "http://localhost:8080/goods/findAll/1" , dataType : "json" , success : function (data ) { console .log("get请求!---------------------" ) console .log(data) } }); $.ajax( { type : "DELETE" , url : "http://localhost:8080/goods/delete/1" , dataType : "json" , success : function (data ) { console .log("delete请求!---------------------" ) console .log(data) } }); $.ajax( { type : "put" , url : "http://localhost:8080/goods/update" , dataType : "json" , data : {name :"电脑" ,price :123 }, success : function (data ) { console .log("get请求!---------------------" ) console .log(data) } }); $.ajax( { type : "post" , url : "http://localhost:8080/goods/save" , dataType : "json" , data : {name :"电脑" ,price :123 }, success : function (data ) { console .log("get请求!---------------------" ) console .log(data) } }); </script > </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @RestController @RequestMapping(value = "/goods") @CrossOrigin(origins = "http://127.0.0.1:8848") public class RestfulController { @GetMapping(value = "/findAll") public R findAll () { List<Goods> goods = List.of( new Goods("电脑" , new BigDecimal("20000" )), new Goods("电话" ,new BigDecimal("4000" )) ); return R.success(goods); } @GetMapping(value = "/findAll/{id}") public R findAllById (@PathVariable Integer id) { Goods goods = new Goods("电话" , new BigDecimal("4000" )); return R.success(goods); } @PostMapping(value = "/save") public R insert (@RequestBody Goods goods) { Goods goodss = new Goods(goods.getName(), goods.getPrice()); return R.success(goodss); } @PutMapping(value = "/update") public R update (Goods goods) { Goods goods1 = new Goods(goods.getName(), goods.getPrice()); return R.success(goods1); } @DeleteMapping(value = "/delete/{id}") public R deleteById (@PathVariable Integer id) { return R.error(); } }
文件上传和下载 文件上传 MultipartResolver用于处理文件上传。当收到请求时,DispatcherServlet 的 checkMultipart() 方法会调用 MultipartResolver 的 isMultipart() 方法判断请求中 是否包含文件。如果请求数据中包含文件,则调用 MultipartResolver 的 resolveMultipart() 方法对请求的数据进行解析,然后将文件数据解析成 MultipartFile 并封装在 MultipartHttpServletRequest (继承了 HttpServletRequest) 对象中,最后传递给 Controller
MultipartResolver 默认不开启,需要手动开启
文件上传前端表单的要求 表单 必须 将 method设置为POST 并将enctype设置为 multipart/form-data 只有在这样的情况下,浏览器才会把用户选择的文件以二进制数据发送给服务器
application/x-www-form-urlencoded
:默认方式,只处理表单域中的 value 属性值,采用这种编码方式的表单会将表单域中的值处理成 URL 编码方式
multipart/form-data
:这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数中,不会对字符编码
文件上床操作 需要两个依赖 commons-fileupload
commons-io
1 2 3 4 5 6 <dependency > <groupId > commons-fileupload</groupId > <artifactId > commons-fileupload</artifactId > <version > 1.3.3</version > </dependency >
1 2 3 4 5 6 7 8 <bean id ="multipartResolver" class ="org.springframework.web.multipart.commons.CommonsMultipartResolver" > <property name ="defaultEncoding" value ="utf-8" /> <property name ="maxUploadSize" value ="10485760" /> <property name ="maxInMemorySize" value ="40960" /> </bean >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <title > 文件上传</title > </head > <body > <form action ="/file/upload" method ="post" enctype ="multipart/form-data" > <input type ="file" name ="file" /> <input type ="submit" value ="上传" > </form > </body > </html >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestController @RequestMapping(value = "/file") public class FileController { @PostMapping(value = "/upload") public R fileUpload (@RequestParam("file") CommonsMultipartFile multipartFile) { String originalFilename = multipartFile.getOriginalFilename(); String s = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.indexOf("." )); MyFile myFile = new MyFile(); myFile.setOriginalFileName(multipartFile.getOriginalFilename()); myFile.setNewFileName(s); myFile.setSize(multipartFile.getSize()); try { multipartFile.transferTo(new File("D://" + s)); } catch (IOException e) { e.printStackTrace(); } return R.success(); } }
CommonsMultipartFile常用方法
String getOriginalFilename()
:获取上传文件的原名
InputStream getInputStream()
:获取文件流
void transferTo(File dest)
:将上传文件保存到一个目录文件中
文件下载 两种方式
传统方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @GetMapping("/download1") @ResponseBody public R download1 (HttpServletResponse response) { FileInputStream fileInputStream = null ; ServletOutputStream outputStream = null ; try { String fileName = "小博.jpg" ; response.reset(); response.setCharacterEncoding("UTF-8" ); response.setContentType("multipart/form-data" ); response.setHeader("Content-Disposition" , "attachment;fileName=" + URLEncoder.encode(fileName, "UTF-8" )); File file = new File("D:/upload/" +fileName); fileInputStream = new FileInputStream(file); outputStream = response.getOutputStream(); byte [] buffer = new byte [1024 ]; int len; while ((len = fileInputStream.read(buffer)) != -1 ){ outputStream.write(buffer,0 ,len); outputStream.flush(); } return R.success(); } catch (IOException e) { e.printStackTrace(); return R.fail(); }finally { if ( fileInputStream != null ){ try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if ( outputStream != null ){ try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
直接向response的输出流中写入对应的文件流
使用ResponseEntity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @GetMapping("/download2") public ResponseEntity<byte []> download2(){ try { String fileName = "小博.jpg" ; byte [] bytes = FileUtils.readFileToByteArray(new File("D:/upload/" +fileName)); HttpHeaders headers=new HttpHeaders(); headers.set("Content-Disposition" ,"attachment;filename=" + URLEncoder.encode(fileName, "UTF-8" )); headers.set("charsetEncoding" ,"utf-8" ); headers.set("content-type" ,"multipart/form-data" ); ResponseEntity<byte []> entity=new ResponseEntity<>(bytes,headers, HttpStatus.OK); return entity; } catch (IOException e) { e.printStackTrace(); return null ; } }
使用 ResponseEntity<byte[]>来向前端返回文件
WebSocket ebSocket 协议提供了一种标准化方式,可通过单个 TCP 连接在客户端和服务器之间建立全双工、双向通信通道。它是与 HTTP 不同的 TCP 协议,但旨在通过 HTTP 工作,使用端口 80 和 443
Http与WebSocket区别 Http 在 HTTP 和 REST 中,一个应用程序被建模为多个 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和请求头将请求路由到适当的处理程序
WebSocket WebSocket中,通常只有一个 URL 用于初始连接。随后,所有应用程序消息都在同一个 TCP 连接上流动
短轮询 用JS写个死循环(setInterval),不停的去请求服务器中的库存量是多少,然后刷新到这个页面当中,这其实就是所谓的短轮询。
短轮询有明显的坏处,那就是你很浪费服务器和客户端的资源。如果有1000个人停留在某个商品详情页面,那就是说会有1000个客户端不停的去请求服务器获取库存量,这显然是不合理的
长轮询 长轮询中,服务器如果检测到库存量没有变化的话,将会把当前请求挂起一段时间(这个时间也叫作超时时间,一般是几十秒)。在这个时间里,服务器会去检测库存量有没有变化,检测到变化就立即返回,否则就一直等到超时为止
长轮询的坏处:因为把请求挂起同样会导致资源的浪费,假设还是1000个人停留在某个商品详情页面,那就很有可能服务器这边挂着1000个线程,在不停检测库存量,这依然是有问题的
应用 如果消息量相对较低(例如监控网络故障,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能也完全没问题),HTTP轮询可以提供有效的解决方案。低延迟、高频率和高容量的组合是使用 WebSocket 的最佳案例
代码案例
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-websocket</artifactId > <version > 5.2.18.RELEASE</version > </dependency > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-messaging</artifactId > <version > 5.2.18.RELEASE</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd" > <html > <head > <meta http-equiv ="Content-Type" content ="text/html; charset=UTF-8" > <title > websocket调试页面</title > </head > <body > <div style ="float: left; padding: 20px" > <strong > location:</strong > <br /> <input type ="text" id ="serverUrl" size ="35" value ="" /> <br /> <button onclick ="connect()" > connect</button > <button onclick ="wsclose()" > disConnect</button > <br /> <strong > message:</strong > <br /> <input id ="txtMsg" type ="text" size ="50" /> <br /> <button onclick ="sendEvent()" > 发送</button > </div > <div style ="float: left; margin-left: 20px; padding-left: 20px; width: 350px; border-left: solid 1px #cccccc;" > <strong > 消息记录</strong > <div style ="border: solid 1px #999999;border-top-color: #CCCCCC;border-left-color: #CCCCCC; padding: 5px;width: 100%;height: 172px;overflow-y: scroll;" id ="echo-log" > </div > <button onclick ="clearLog()" style ="position: relative; top: 3px;" > 清除消息</button > </div > </div > </body > <script type ="text/javascript" > let output ; let websocket; function connect ( ) { output = document .getElementById("echo-log" ) let inputNode = document .getElementById("serverUrl" ); let wsUri = inputNode.value; try { websocket = new WebSocket(wsUri); }catch (ex){ console .log(ex) alert("对不起websocket连接异常" ) } connecting(); window .addEventListener("load" , connecting, false ); } function connecting ( ) { websocket.onopen = function (evt ) { onOpen(evt) }; websocket.onclose = function (evt ) { onClose(evt) }; websocket.onmessage = function (evt ) { onMessage(evt) }; websocket.onerror = function (evt ) { onError(evt) }; } function sendEvent ( ) { let msg = document .getElementById("txtMsg" ).value doSend(msg); } function onOpen (evt ) { writeToScreen("CONNECTED" ); doSend("WebSocket 已经连接成功!" ); } function onClose (evt ) { writeToScreen("连接已经断开!" ); } function onMessage (evt ) { writeToScreen('<span style="color: blue;">服务器: ' + evt.data+'</span>' ); } function onError (evt ) { writeToScreen('<span style="color: red;">异常信息:</span> ' + evt.data); } function doSend (message ) { writeToScreen("客户端A: " + message); websocket.send(message); } function clearLog ( ) { output.innerHTML = "" ; } function wsclose ( ) { websocket.close(); } function writeToScreen (message ) { let pre = document .createElement("p" ); pre.innerHTML = message; output.appendChild(pre); } </script > </html >
1 2 3 4 5 6 7 8 9 10 11 @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers (WebSocketHandlerRegistry registry) { registry.addHandler(new MessageHandler(), "/message" ) .addInterceptors(new HttpSessionHandshakeInterceptor()) .setAllowedOrigins("*" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class MessageHandler extends TextWebSocketHandler { Logger log = LoggerFactory.getLogger(MessageHandler.class); private List<WebSocketSession> sessions = new CopyOnWriteArrayList<>(); @Override public void afterConnectionClosed (WebSocketSession session, CloseStatus status) throws Exception { sessions.remove(session); log.info("{} 连接已经关闭,现从list中删除 ,状态信息{}" , session, status); } @Override public void afterConnectionEstablished (WebSocketSession session) throws Exception { sessions.add(session); log.info("用户{}连接成功.... " ,session); } @Override public void handleMessage (WebSocketSession session, WebSocketMessage<?> message) throws Exception { log.info("收到来自客户端的信息: {}" ,message.getPayload()); session.sendMessage(new TextMessage("当前时间:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss" )) +",收到来自客户端的信息!" )); for (WebSocketSession wss : sessions) if (!wss.getId().equals(session.getId())){ wss.sendMessage(message); } } }
你知道的越多 你不知道的越多 嘿 我是小博 带你一起看我目之所及的世界……