2.1 文件下载 文件上传:将本机文件上传到服务器 文件下载:将服务器上的文件下载到本地 如果是浏览器支持的文件格式,则下载后直接显示 例如:*.html,*.jpg 如果浏览器不支持显示该文件,则提示保存到本地 例如:*.exe,*.rar 注意:都是文件下载,区别是有的能直接显示 不支持下载的目录 Web Application 目录下的文件由tomcat自动支持下载的. 但是: 1.WEB-INF 和META-INF下的文件不支持下载 2.Web Application目录之外的文件不支持下载. 小结: 介绍了什么是文件下载,哪些位置的文件不支持自动下载. 文件下载的本质:tomcat把一个文件的内容传递给客户端. 2.2 自定义下载 自定义下载:使用Servlet自己控制下载 特点:-文件可以在任意位置(Web目录之外) -可以控制权限 -可以控制下载速度(下载限速) -可以控制是否允许多点下载(多线程下载) -其他方面的下载控制 自定义下载的示例: -所有用户的在头像存储在 c:\data\photo\下 -头像的命名规则:20180001.jpg -要求在web项目中显示加载显示头像. 一般可以认为是"/"的作用等同于"\\" 最好用“/”因为java是跨平台的。“\”(在java代码里应该是\\)是windows环境下的路径分隔符, Linux和Unix下都是用“/”。而在windows下也能识别“/”。所以最好用“/” 自定义下载的实现步骤: 1.添加一个Servlet:DownloadService 2.配置映射路径 /downlad/* 3.从访问URI里取得用户的请求参数,对应到实际的本地文件 4.读取本地文件的内容,发给客户端. 404错误,如果用户访问的目标文件不存在,应返回404错误. 前端无法区别这是一个Servlet还是一个静态文件 如<img src="download/photo/20180001.jpg" /> 2.3 内容类型Content-Type 在HTTP应答的头部,需要指定内容的类型 示例: HTTP/1.1 200 Content-Type:image/jpeg //内容的类型 Content-Length:51967 //内容的长度 观察2.1的抓包数据,找到Content-Type字段. 内容类型主要分为5大类: text/* //文本 image/* //图片 audio/* //音频 video/* //视频 application/* //应用文件 @WebServlet("/download/*") public class DownloadService extends HttpServlet { File dataDir = new File("c:/data/"); protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // getServletPath() 返回的是 /download ,不含全路径信息 // String servletPath = request.getServletPath(); // 请求路径 // requestUri: 例如 /mid0201/download/photo/20180001.jpg String requestUri = request.getRequestURI(); // contextPath: 例如 /mid0201 String contextPath = request.getContextPath(); // servletPath: 例如 /download/photo/20180001.jpg String servletPath = requestUri.substring( contextPath.length()); // 提取出目标信息 targetPath: "photo/20180001.jpg" String targetPath = servletPath .substring("/download/".length()); File targetFile = new File(dataDir, targetPath); // 检查目标文件是否存在 if(!targetFile.exists() || ! targetFile.isFile()) { System.out.println("目标文件不存在!" +targetFile); response.sendError(404); return; } // 应答:设置 Content-Type 和 Content-Length 读取目标文件,发送个客户端 String contentType = "image/jpeg";//"application/octet-stream"; long contentLength = targetFile.length(); response.setContentType(contentType); response.setHeader("Content-Length", String.valueOf(contentLength)); // 应答:读取目标文件的数据, 发送给客户端 InputStream inputStream = new FileInputStream(targetFile); OutputStream outputStream = response.getOutputStream(); try { streamCopy (inputStream, outputStream); }catch(Exception e) { try{ inputStream.close();} catch(Exception e2){} } outputStream.close(); } private long streamCopy(InputStream in, OutputStream out) throws Exception { long count = 0; byte[] buf = new byte[8192]; while (true) { int n = in.read(buf); if (n < 0) break; if (n == 0) continue; out.write(buf, 0, n); count += n; } return count; } } 测试1 映射:DownloadService->/download/* 比较: image/jpeg application/octet-steam 观察浏览器反应. HTTP应答时,应设置正确的Content-Type 浏览器会结合文件后缀和Content-Type来处理 注:不要求记住,直到它的作用和5种分类即可 2.4 HTTP 404错误 404 Not Found 是一个网站开发里常见的错误 404是啥意思?为什么不能是403,405? HTTP状态码(HTTP Status Code) 分为4类: 2XX:成功,正常,已受理 3XX:重定向 4XX:请求错误 5XX:服务器错误 常见状态码: 200:ok 206:Partial Content 302:Move temporaily 304:Not Modified 403:ForBidden 404:Not Found // 检查目标文件是否存在 Servlet返回应答错误 在Servlet里,可以调用sendError()返回应答错误: // 检查目标文件是否存在 if(!targetFile.exists() || ! targetFile.isFile()) { System.out.println("目标文件不存在!" +targetFile); response.sendError(404); return; } isFile() public boolean isFile()测试此抽象路径名表示的文件是否是一个标准文.如果该文件不是一个目录, 并且满足其他与系统有关的标准,那么该文件是标准文件.由Java应用程序创建的所有非目录文件一定是标准文件. 返回:当且仅当此抽象路径名表示的文件存在且是一个标准文件时,返回true;否则返回false; 抛出:SecurityException,如果存在安全管理器,且其SecurityManager.checkRead(java.lang.String)方法拒绝对文件进行读访问. exists() public boolean exists()测试此抽象路径名表示的文件或目录是否存在. 返回:当且仅当此抽象路径名表示的文件或目录存在时,返回true;否则返回false; 抛出:SecurityException如果存在安全管理器,且其SecurityManager.checkRead(java.lang.String)方法拒绝对文件或目录进行写访问. isFile():判断是否文件,也许可能是文件或者目录 exists():判断是否存在,可能不存在 两个不一样的概念 介绍了HTTP状态码的含义 介绍了用sendError()返回404. 3.1 会话Session 在javaweb开发中,Servlet,Session,Filter是三个最重要的机制. 什么叫会话 和日常生活中的"会话"意思差不多 比如:去拜访一家公司,从敲门到离开的这一过程就是一次会话. 敲门 xxx-------->Session <-------(公司) 离开 当用户访问网站时,会话指从打开到关闭这一过程 开始:用户打开浏览器,打开网站地址 结束:用户关闭浏览器 请求和会话 从用户开始访问该网站,每一个请求都会关联到后台的一个Session对象. 再Servlet里,可以取得当前的会话对象. HttpSession session =request.getSession(); 每一个会话都有一个ID String sessionId=session.getId(); 演示:在一次会话期间,所有的请求都关联到同一个Session对象.
@WebServlet("/Example1") public class Example1 extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //每一个request都关联到一个session对象上 HttpSession session=request.getSession(); //每一个会话都有一个ID String sessionId=session.getId(); System.out.println("访问example1-Session ID: " + sessionId); response.getWriter().append("Served at: ").append(request.getContextPath()); } } 访问example1-Session ID: 30A34D43449564344339374868C36FEC 访问example2-Session ID: 30A34D43449564344339374868C36FEC 浏览器未关闭的话是同一个会话. 浏览器关闭后再访问 访问example1-Session ID: 95CF8FA07AC01878D292836AD73FB9DE 思考: 1.如果关闭浏览器,再打开浏览器重新打开网站,后台会创建一个新的会话吗? 会 2.除了Servlet之外,访问静态文件(html,jpg)等请求不是也在会话里吗? 是的 由于静态文件是由tomcat处理,所有没法用代码直接进行演示. 如果有多个用户同时访问网站,那么tomcat会创建多个Session对象,分别表示每一个会话. 引入会话Session概论 1.会话表示从打开网站到离开的一次过程 2.重新打开浏览器,则重启一个新的会话 3.会话期间,该用户所有的请求操作都关联到一个 Session session = request.getSession() 3.2 会话与当前用户 会话的作用:一般用于保存当前用户的信息. 应用演示: -实现一个提供下载业务的网站 -要求用户必须在登录后才能下载. 演示:下载业务 -添加下载页project.html -添加DownladService.java 提供下载服务 现在,任何人都可以打开project.html进行下载... 怎样限制用户必须在登录后才能下载呢? 用户登录 -添加登录页面 login.html -添加登录接口 LoginApi.java 现在,用户输入用户名,密码即可实现登录... 下一步:在DownloadService里如何检查当前用户是否已经登录呢? 现在单独实现了登录页面和下载页面,但是用户可以直接访问登录页面进行下载. 需要在下载页面的session中对用户权限进行审查.在下载页面中检查用户是否登录. 3.3 会话与用户权限. 会话的作用:一般用于保存当前的用户信息. 当用户登录后,把用户的相关信息保存在当前会话里,例如,用户ID,用户权限... 用户登录 -LoginApi.java 当用户登录后,应把用户信息保存到当前会话里 //在当前会话里保存当前用户的信息 //在真实项目里,此信息应当从数据库里取得,这里仅供演示 User user=new User(username,true); HttpSession httpSession =this.httpReq.getSession(); httpSession.setAttribute("user", user); 用户权限检查 -DownloadService.java 在下载之前,检查当前用户的权限 // 检查当前用户是否已经登录 User user=(User)request.getSession().getAttribute("user"); if(user==null) { System.out.println("用户未登陆,请先登录"); response.sendRedirect("login.html"); //302重新定向 return; } download通过前端的超链接 <a href="down?id=20180002"> YY系统源码 </a> 通过识别down调用@WebServlet("/down")注解的方法. 关闭浏览器后再下载,需要重新登录,因为此时后台开启了一个新的会话,user变为null. 3.4 HTTP 302重新定向 重定向,Redirection,即指示客户端打开一个新的位置. response.sendRedirect("login.html"); //302重定向. 第一步:浏览器发送get请求 down?id=2018001 服务器返回:HTTP/1.1 302 Location:login.html 浏览器发送新的get请求 login.html 服务器返回:200. 4.1 实例-BBS论坛 演示一个BBS论坛系统,从中理解会话的作用 -用户登录 login.html -浏览 index.html -发帖 edit.html 代码框架 沿用网站入门篇的af-service框架 -数据库MySQL:af_example.sql -框架支持 -添加API -添加前端页面. 4.2 会话的作用. 会话的作用,主要用于保存当前用户的信息 1.在用户登录时,将用户信息保存到当前会话 2.在后续操作时,从会话中取出当前用户信息 1.UserLoginApi登录 在登录时,保存当前用户信息 httpReq.getSession().setAttribute("user",u); 2.ArticleSaveApi登录 在保存发帖时,显然,要知道当前用户是谁 user=(User)httpReq.getSession().getAttribute("user"); row.setCreator(user.id); 更多功能 1.用户注册 2.只看自己的帖子 3.删帖 -管理员删帖 -普通用户删自己的帖子 几乎所有的后台操作,都要检验当前用户的身份和权限. 5.1 权限检查 权限检查:即检查当前用户有没有执行操作的权限 例如,发帖时要检查用户是否登录,有没有发帖权限. 存在的问题,在当前演示系统中,已经有了后台检查,但是缺少前端检查. 比如,如果用户未登陆,那么应该让他根本点不了"发帖界面",而不是编辑好了点保存,才提示说尚未登陆. 添加前端检查 第一种方法:(不推荐) 在点击发帖按钮时,向后台发起请求,检查当前用户的信息. User user = (User) httpReq.getSession().getAttribute("user"); //(User)是什么意思 强制类型转换为User类 if(user == null) throw new Exception("请先登录!"); 检查权限的两种方式:前端检查和后台检查 后台检查:(必须有)保证系统的安全性,完整性 前端检查:(最好有)增强系统的易用性,避免误操作,提升用户体验 5.2 前端权限检查 存在的问题,在上一节课中,在每次按钮时,向后台发起一个请求SesionInfo.api来获取当前用户信息... 问题:当用户访问量大时,会影响网站效率... 前端权限检查:借助于浏览器缓存,在前端自行完成检查. 一、sessionStroage sessionStorage是浏览器对象,可以用JavaScript来访问它。前端页面可以把数据存储到这里。 例如, 存数据sessionStorage.setItem (“username”, “邵发”); 取数据 var name = sessionStorage.getItem(“username”); 注意: 1 当浏览器关闭时,sessionStorage的内容被清空。 2 只能存储字符串。如果想存储一个对象,需要转成字符串。 sessionStorage.setItem (“key”, JSON.stringify( obj ) ); var obj = JSON.parse( sessionStorage.getItem(“key”)); 二、添加前端检查 1 在登录返回时,保存当前用户信息到缓存 (1) 修改 UserLoginApi, 返回当前用户的信息 (2) 修改 login.html sessionStorage.setItem("user", JSON.stringify(data)); 2 在发贴时,检查缓存里的当前用户的信息 修改 index.html // 点击 ‘发贴‘ 按钮 M.doCreateNew = function() { var user =JSON.parse(sessionStorage.getItem("user")); if(user == null) { alert("请先登录鸭"); location.href = "login.html"; } else { location.href = "edit.html"; } } 1.在登录返回时,将当前用户的信息存在缓存里 sessionStorage.setItem("key",value) 2.在点发帖时,从缓存里检查当前用户的信息 value=sessionStorage.getItem("key") UserLoginApi.java 最后添加 JSONObject jdata =new JSONObject(u); jdata.remove("password"); //不把密码返回给客户端 return jdata; 小结 认识了浏览器缓存对象 sessionStorage 学会使用sessionStorage存储当前用户信息,实现前端检查,减少服务器的负担. 5.3 后端检查与网络安全 问题:只使用前端检查,不使用后端检查,可以吗 例如:在点"发帖"时,前端检查一次.. 在点"发表"时,前端检查一次.. 似乎不再需要后端检查了,是不是? 取消后台检查,所有检查都在前端进行.看起来网站功能和以前一样,没有影响.. 模拟攻击 不做后台检查的网站,很容易能攻击 演示: 新建一个Java Project,直接调用后台的接口.. 显然,攻击者不需要知道用户名和密码,直接可以随意修改网站的数据. 后端检查:必须有,保证安全性,数据完整性. 前端检查:最好有,提升易用性和用户体验. 5.4 显示细节优化. 添加方法 // 查询,并返回 JSONArray public static JSONArray executeQuery2JSON(String sql) throws Exception { JSONArray result = new JSONArray(); AfSqlConnection connection = getConnection(); try{ ResultSet rs = connection.executeQuery(sql); // 取得每一列的名称和类型 ResultSetMetaData rsmd = rs.getMetaData(); int numColumns = rsmd.getColumnCount(); // 一共几列 int[] columnTypes = new int[numColumns]; // 每列的类型 String[] columnLabels = new String[numColumns]; // 每列的标题 for(int i=0; i<numColumns; i++) { int columnIndex = i + 1; // 列序号 columnLabels[i] = rsmd.getColumnLabel(columnIndex); // 列标题 columnTypes[i] = rsmd.getColumnType(columnIndex); // 类型, 参考 java.sql.Types定义 } while(rs.next()) { // 每一行转成一个JSONObject JSONObject jrow = new JSONObject(); result.put(jrow); for(int i=0; i<numColumns; i++) { String columnValue = rs.getString( i + 1); // 每列的值 if(columnValue == null) continue; int type = columnTypes[i]; if(type == Types.TINYINT || type == Types.SMALLINT || type == Types.INTEGER || type == Types.BIGINT) { jrow.put(columnLabels[i], Long.valueOf(columnValue)); } else if(type == Types.DOUBLE || type == Types.FLOAT) { jrow.put(columnLabels[i], Double.valueOf(columnValue)); } else { jrow.put(columnLabels[i], columnValue); } } } return result; }finally{ connection.close(); } } 使输出为一个JSONArray对象 在前端页面index.html中修改方法M.shouResult // 格式化数据并显示 M.showResult = function(data) { // 创建一个 AfTemplate对象用于替换 var templ = new AfTemplate( $(‘.template‘).html()); var target = $(".main .content tbody"); target.html(""); // 清空 for(var row of data) { row.timeCreated =row.timeCreated.substr(0,16); target.append( templ.replace(row) ); } // 如果没有数据,则把下面的提示显示出来 if(data.length > 0) $(".main .content .empty").hide(); else $(".main .content .empty").show(); } 6.1 用户注册 实现用户以手机号进行注册 -数据库af_example.sql -注册页面 register.html -登录页面 login.html 手机号需要效验. 6.2 手机短信验证 在用户以手机号进行用户注册时,需要填写验证码 1.绑定手机号 2.点"发送验证码".后台系统将发送一个验证码到这个手机号,同时把验证码存在Session中. 3.用户在手机上查看短信验证码,填写到页面 4.用户点"注册"...后台对检查验证码是否正确.. 通过UserSendVerifyApi生成验证码 // 生成4位验证码 int randomCode = new Random().nextInt(5000) + 1000; String verifyCode = String.valueOf(randomCode); 通过MySmsService发送给手机 public class MySmsService { public static MySmsService i = new MySmsService(); //public static String serviceUrl = "http://127.0.0.1:8080/service/SendSms.api"; public static String serviceUrl = "http://service.afanihao.cn/SendSms.api"; // 从 http://service.afanihao.cn 上获取 API Key / API Secret private String API_KEY = "CAE742E06B27C66D7DD79B3CF3F043BB"; private String API_SECRET = "C0CDE27BC95F32E4C831A376C8E43F0254F6D55A"; private MySmsService() { } public void send(String cellphone, String code) throws Exception { for(int i=0; i<cellphone.length(); i++) { char ch = cellphone.charAt(i); if(ch<‘0‘ || ch >‘9‘) throw new Exception("手机号必须全是数字!"); } if(cellphone.length() != 11) throw new Exception("手机号必须是11位数字!"); if(code.length() > 6) throw new Exception("验证码最大6位!"); JSONObject jreq = new JSONObject(); jreq.put("apiKey", API_KEY.trim()); jreq.put("apiSecret", API_SECRET.trim()); jreq.put("target", cellphone); jreq.put("code", code); JSONObject jresp = REST.post(serviceUrl, jreq); int error = jresp.getInt("error"); if(error != 0) { String reason = jresp.getString("reason"); throw new Exception(reason); } } } 再通过UserRegisterApi验证 // 检查验证码 String verifyCode = jreq.getString("verifyCode").trim(); //从前台输入中获取 String code = (String)httpReq.getSession().getAttribute("verifyCode"); //从会话中的code获取 if(code == null || ! code.equals( verifyCode)) throw new Exception("验证码不匹配!"); 网站后台如何发送短信 注:参考<Java如何发送手机短信> 本章的例子是可以运行的,在发送短信的环节,需要大家按以下说明修改一下。 1 打开 http://service.afanihao.cn/ 2 注册一个账号 3 登录系统,可以看到你的API Key, API Secret 4 修改代码 MySmsService.java 内部原理: MySmsService里将手机号、验证码发给给 service.afanihao.cn , 然后由service.afanihao.cn这台服务器来发送一个短信到目标手机上。 注:如果你有一个已备案的带域名的个人网站,则可以各家云服务提供商自行申请短信功能, 然后用云平台的API来发送短信。(必须是已备案的网站才有可能申请成功)。 7.1 过滤器 Filter 网站开发的三大核心机制:Servlet,Session,Filter 过滤器的作用:对请求进行过滤处理 filter1 filter2 |--->Servlet 请求---------------------->| |--->静态文件 创建过滤器 演示:添加一个Filter 右键new Filter 最主要的是doFilter方法 过滤器的配置 Filter的配置也分注解方式和XML方式 URL规则和Servlet完全一样 //在xml中配置项 <url-pattern> 精准路径:如/home/ex.html 前缀匹配:如/download/* 后缀匹配:如*.api
public void init(FilterConfig fConfig) throws ServletException 初始化方法 public void destroy() 停止时调用. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 主要方法. @WebFilter("/login.html") //修改注释的拦截路径 注释,访问http://127.0.0.1:8080/demo06/login.html Filter方法被调用. 不想放行 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println("Filter 被调用"); HttpServletResponse httpResp =(HttpServletResponse)response; httpResp.sendError(403,"无权访问"); // chain.doFilter(request, response); } 7.2 访问过滤 假设有一些静态资源放在/download下面, 但是,其中的vip/目录下的资源要求登录后可见. // servletPath : 例如 /download/vip/yanran.jpg String servletPath = httpReq.getServletPath(); //.getServletPath() 获取路径 if(servletPath.startsWith("/download/vip/")) //.startsWith(String prefix) 检测字符串是否以指定的前缀开始。 if(servletPath.startsWith("/download/vip/") { { User user = (User) httpReq.getSession().getAttribute("user"); if(user == null) { // 第1种:可以返回重定向 302 // 控制浏览器重定向到 login.html // 注意:要计算出绝对路径 String contextPath = httpReq.getContextPath(); httpResp.sendRedirect(contextPath + "/login.html"); // 第2种:可以返回错误 4XX // httpResp.sendError(403, "请先登录"); // 第3种:可以返回200,并返回HTML内容 // httpResp.setCharacterEncoding("utf-8"); // httpResp.setContentType("text/html"); // httpResp.getWriter().write("<html><body>你还没登录呢!</body></html>"); return; } } 要点 1.使用前缀匹配/download/* 2.检查会话,确认用户身份 3.在Filter里返回应答 (通过HttpServletResponse对象来返回应答) 进一步理解过滤器写法 通过检查Session信息,让一部分请求通过,另一部分请求直接返回. 7.3 访问过滤(2) 示例: 假设有一些服务接口需要在登录后才能调用 例如:/GetSecret 是一个Servlet服务接口 要求当前用户有相应权限才能调用 MyFilter3 请求:/GetSecret <----------------->MyService3
原文:https://www.cnblogs.com/cqbstyx/p/10449286.html