目录
前言
1.javaEE环境安装
第一步:正常创建javase项目
第二步:右击项目,引入框架支持
第三步:将项目部署到Tomcat服务器上。Tomcat安装及配置
2.servlet搭建
请求格式为:ip:端口/项目名/servlet地址
3.servlet生命周期
4.http请求和响应
4.1http请求
4.2http响应:
5.过滤器(Filter)
6.异步请求(Ajax)+跨域问题
6.1异步请求
6.2跨域问题
6.2.1什么是跨域?
6.2.2为什么要跨域?
6.2.3后端怎么解决跨域问题?
7.前端过渡
8.json数据格式+前端数据请求格式问题
8.1后端数据响应json处理
8.2前端请求数据格式处理
9.后端验证用户名和密码(联系数据库查询)
9.1Reult类
9.2User类
9.3JDBC代码
9.4.LoginServlet类
10.前端收到响应判断是否登录+sessionstorage+localStorage
10.1前端接收到json字符串,做出判断
10.2sessionstorage+localStorage
10.2.1提出问题:
10.2.2问题分析:
10.2.3解决问题:
10.2.4路由导航守卫
11.会话跟踪
11.1token是怎么生成的?
11.2token优点
11.3base64转码
11.4后端生成token并响应
11.5前端接收到token后存储,并且请求时将token放在请求头
11.5.1存储:
11.5.2前端发送请求时(axios请求拦截):
11.6后端通过过滤器中统一验证
11.7前端接收到验证响应做出判断(axios响应拦截)
前言
现在前端和后端已经完全分离,本篇文章会从javaEE环境的安装一直到会话跟踪结束,搭建一个学生管理系统的登录需求,实现前后端的基本交互。至于前端登录页面的搭建,是来源于于之前的文章前端项目框架的搭建(Vue.js+ElementUI骨灰级保姆教程)。切记细细理解代码中的注释!!!
1.javaEE环境安装
项目是在IntelliJ IDEA 2020.2.1 版本下创建的。
第一步:正常创建javase项目
第二步:右击项目,引入框架支持
选择Web Application(4.0) ,点击ok
文件介绍
第三步:将项目部署到Tomcat服务器上。Tomcat安装及配置
Server设置成这样。
在Deployment 添加创建的项目。
完成此操作后,项目即已部署在服务器上。
2.servlet搭建
导入servlet-api.jar包
首先我们创建一个servlet包,里面创建LoginServlet类,此类继承HttpServlet类,在里面生成四个方法,分别是构造方法、init(ServletConfig config)、service()、destroy()。
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.IOException;
public class LoginServlet extends HttpServlet {
public LoginServlet() {
System.out.println("无参构造方法");
}
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("初始化Servlet");
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("service");
}
@Override
public void destroy() {
System.out.println("destroy");
}
}
在xml文件中配置LoginServlet。
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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_4_0.xsd"
version="4.0">
<!--配置项目中的LoginServlet-->
<servlet>
<servlet-name>loginServlet</servlet-name>
<servlet-class>servlet.LoginServlet</servlet-class><!--LoginServlet在项目所处的位置-->
</servlet>
<!--为LoginServlet配置访问地址-->
<servlet-mapping>
<servlet-name>loginServlet</servlet-name>
<url-pattern>/login</url-pattern><!--地址以/开头-->
</servlet-mapping>
</web-app>
然后启动服务器,在浏览器中访问:
请求格式为:ip:端口/项目名/servlet地址
回车后,我们可以发现在后端输出框中输出了:
无参构造方法
初始化Servlet
service
为啥会输出这些?这就牵扯到了servlet生命周期~~~~~
3.servlet生命周期
访问路线:
方法解析:
package servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServlet;
import java.io.IOException;
/*!!!无论什么时侯创建,创建的servlet对象指向的是servlet地址,
servlet什么时候被创建: 小于0:第一次访问服务器时,大于等于0:在服务器启动时
<load-on-startup>-1</load-on-startup> 在xml中配置
创建对象后就直接会执行构造方法和init(),之后访问到地址时(即通过浏览器-服务器-项目-地址),才会
执行service()等一系列方法。
*/
/*
* 一次http请求发送到后端找到Servlet程序,是按照一个特定的顺序调用方法
* 在最上层LoginServlet实现了Servlet接口,所有的javaEE的类都实现了Servlet接口,一切按照Servlet接口中定义方法的顺序来走。
*/
public class LoginServlet extends HttpServlet {
// 构造方法() 最先执行 一次 初始化对象的,主要是针对成员变量
public LoginServlet() {
System.out.println("无参构造方法");
}
//init(ServletConfig config) 在构造方法之后就会执行 一次 初始化servlet,主要是针对配置文件,可获取到配置文件中定义的参数parameter,然后可通过config调用
/*<init-param>
<param-name>zhangsan</param-name>
<param-value>123</param-value>
</init-param>
*/
@Override
public void init(ServletConfig config) throws ServletException {
System.out.println("初始化Servlet");
}
//service() 每次请求都会执行,提供服务的。
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
System.out.println("service");
}
//destroy() 在服务器关闭时执行,只执行一次。比如将一些数据存储到数据库,打印日志信息。
@Override
public void destroy() {
System.out.println("destroy");
}
}
4.http请求和响应
由于前端发送的http请求有get和post两种,所以后端提供doGet()和doPost()进行相应的处理和响应。
4.1http请求
请求中包含:
请求行 | 请求的地址,请求的方式,请求的状态。 |
请求头 | 包含服务器信息,客户端信息,以键值对的形式向后端发送,键都是固定的。 |
请求体 | 存放post请求方式向服务器端发送的数据。 |
http请求的两种方式:
get:
1.get方式主要用于从服务器获取数据,也是可以向服务器发送数据的后端地址?键=值&键=值。
2.不能传递过多的数据,因为浏览器会限制 一般情况 1-2kb。
3.请求的数据在地址后面显示的,不能传递敏感数据,安全性低。4.tomcat8之后,get请求中有中文,不会乱码。
post:
1.post主要用于向后端发送请求,请求的数据存放在请求体中,不会显示在地址栏中。
2.相对安全,传输的数据量大,没有限制(可以通过post提交文件)。3.后端接受前、响应前需要设置编码,要不然接受时会乱码。setCharacterEncoding(编码格式)。
4.2http响应:
1.Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个代表响应的HttpServletResponse对象。
2.用getWriter()获得一个PrintWriter字符输出流输出数据 3.response.setContetnType("text/html;charset=utf-8");方法可以同时设定response所使用的字符集编码和浏览器打开所用的字符集编码。
前端发送(举例):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<!--get方式-->
<a href="http://127.0.0.1:8080/webBack_Test/login?name=ami&age=20">同步访问后端</a>
<!--post方式-->
<form action="http://127.0.0.1:8080/webBack_Test/login" method="post">
用户名<input type="text" name="username" value="" />
密码<input type="password" name="password" value="" />
<input type="submit" value="登录"/>
</form>
</body>
</html>
后端接收:
public class LoginServlet extends HttpServlet {
/*
虽然调用doGet和doPost方法,但是构造,init,service,destroy方法,仍然会正常执行,这是Servlet接口的规范,是不能变的!!
其实父类的service方法,里面是会进行一个判断的,判断请求方式是get还是post,然后调用doGet()或者doPost()方法,
只不过之前我们在本类中又重写了service方法,使他的实际功能没有体现出来。
当一次http请求发送到服务器时,tomcat会将请求的所有数据封装到一个类当中,而这个类实现了HttpServletRequest接口。
* */
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
/*
get与post的接收顺序,步骤一样,就是doGet里面不用设置编码
* */
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//接收请求数据(请求行,请求头,请求体)
req.getRequestURL();//http://127.0.0.1:8080/webBack_Test/login
req.getRemoteAddr();//获取远端的ip地址 127.0.0.1
req.getRemotePort();//获取远端的端口
//接收用户自己发来的数据.post数据在接受之前,需要设置一个解码规则
req.setCharacterEncoding("utf-8");
req.getParameter("username");
req.getParameter("password");
//处理
//响应
//resp.setContentType("text/html;charset=utf-8");设置编码
resp.setHeader("Content-Type","text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();//获取打印流
writer.print("提交成功"); //对象,集合,map-->以后用json
//print与write没区别,就是print的方法重载比较多
}
}
5.过滤器(Filter)
我们知道在前端发送post请求时,后端接收时要先设置编码,以防止乱码,如果项目中servlet包有多个类时,每一个在接受前都要手动设置一下编码,岂不繁琐。所以我们提出过滤器。
Servlet API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。
Filter也称之为过滤器,它是Servlet技术中最实用的技术,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Servlet, 从而实现一些特殊的功能。例如实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。
● doFilter(ServletRequest request, ServletResponseresponse,FilterChain chain):该方法是filter进行过滤操作的方法,是最重要的方法。
● filterChain过滤链,处理完后,让请求继续向下执行,下一个可能时目标Servlet,也可能是下一个过滤器。
filterChain.doFilter(servletRequest,servletResponse);
作用:对服务器web资源进行拦截(权限控制,通过拦截资源进行权限控制,是否可以访问)
即浏览器访问服务器时,可以进入过滤器1、也可访问servlet地址。如果进入到过滤器1后,可以继续进到过滤器2、也可直接截止做出响应、也可继续访问servlet地址,都是我们可以设置的!
所以我们可以创建一个filter包里面创建一个设置编码的过滤器(类),让每次请求先通过编码过滤器,设置编码。
/*编码过滤器*/
public class EncodeFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
servletRequest.setCharacterEncoding("utf-8");
//filterChain过滤链,处理完后,让请求继续向下执行,下一个可能时目标Servlet,也可能是下一个过滤器
filterChain.doFilter(servletRequest,servletResponse);
}
}
在xml文件中配置过滤器:配置/*表示所有请求都得过此过滤器
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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_4_0.xsd"
version="4.0">
<!--配置项目中的LoginServlet-->
<servlet>
<servlet-name>loginServlet</servlet-name>
<servlet-class>servlet.LoginServlet</servlet-class><!--LoginServlet在项目所处的位置-->
</servlet>
<!--为LoginServlet配置访问地址-->
<servlet-mapping>
<servlet-name>loginServlet</servlet-name>
<url-pattern>/login</url-pattern><!--地址以/开头-->
</servlet-mapping>
<!--配置编码过滤器-->
<filter>
<filter-name>encode</filter-name>
<filter-class>filter.EncodeFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>encode</filter-name>
<url-pattern>/*</url-pattern> <!--配置哪些请求地址进入到编码过滤器 /*表示所有-->
</filter-mapping>
</web-app>
6.异步请求(Ajax)+跨域问题
6.1异步请求
说起异步,涉及的领域很广,这里我们探讨异步请求。下面是同步请求和异步请求之间的区别。
以前javaScript里的异步请求(用户名失焦验证举例):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script type="text/javascript">
function checkusername(username){
//最底层代码,是用XMLHtttpRequest对象来发送请求
var httpObj = new XMLHttpRequest();
httpObj.open("GET","http://127.0.0.1:8080/webBack/login?username="+username,true);
httpObj.send(null);//发送请求
//接受请求
httpObj.onreadystatechange=function(){
//就相当于一个状态信息码 从2开始--到4响应结束
if(httpObj.readyState==4){
document.getElementById("msgId").innerHTML = httpObj.responseText;
}
}
</script>
</head>
<body>
<form action="" method="post">
账号<input type="text" name="username" value="" onblur="checkusername(this.value)" /><span id="msgId"></span> <br />
密码<input type="password" name="password" value="" /><br />
<input type="submit" value="登录"/>
</form>
</body>
</html>
现在用axios,是一个Ajax框架来进行改进,但是底层还是通过XMLHtttpRequest对象来操作的。此时我们还未把我们所讲的东西整合在Vue-cli(脚手架)项目中,所以我们需要导入一个axios.min.js的js文件。axios.min.js下载
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="js/axios.min.js" type="text/javascript" charset="UTF-8"></script>
<script type="text/javascript">
function checkusername(username){
//将创建XMLHtttpRequest对象的过程封装在了get()里面
axios.get("http://127.0.0.1:8080/webBack_Test/login?username="+username).then(function(resp){
//resp就是拿到的一系列响应过来的数据
document.getElementById("msgId").innerHTML = resp.data;
});
}
</script>
</head>
<body>
<form action="" method="post">
账号<input type="text" name="username" value="" onblur="checkusername(this.value)" /><span id="msgId"></span> <br />
密码<input type="password" name="password" value="" /><br />
<input type="submit" value="登录"/>
</form>
</body>
</html>
运行之后会发现,浏览器发出请求给服务器,可是服务器响应,浏览器接收不到,这就引出了跨域问题。 之前我们同步请求时,服务器的响应是直接响应在浏览器重新生成的窗口上,而异步请求时服务器的响应是要被浏览器给接收到的,然后通过标签响应在此页面上。
6.2跨域问题
6.2.1什么是跨域?
跨域是指从一个域名的网页去请求另一个域名的资源,严格一点的定义是:只要 协议,域名,端口有任何一个的不同,就被当作是跨域.
跨域问题是一个前端问题,是在前后分离的架构中出现的。
6.2.2为什么要跨域?
有时公司内部有多个不同的子域,比如一个是location.company.com ,而应用是放在app.company.com , 这时想从 app.company.com去访问 location.company.com 的资源就属于跨域。
6.2.3后端怎么解决跨域问题?
解决办法是创建一个过滤器(CorsFilter)。
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
//后端解决跨域问题
public class CorsFilter implements Filter {
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
//允许携带Cookie时不能设置为* 否则前端报错
//1.本质就是你浏览器在访问服务器时,我后端获取到你浏览器的地址,然后设置前端让前端知道这是一个安全的响应,即后端响应可以到浏览器上
//就比如说,你浏览器是8848的端口,你访问我8080的服务器,我给你做出响应,你不接收,我后端就就要设置8848要可跨域。
httpResponse.setHeader("Access-Control-Allow-Origin", httpRequest.getHeader("origin"));//允许所有请求跨域
httpResponse.setHeader("Access-Control-Allow-Methods", "*");//允许跨域的请求方法GET, POST, HEAD 等
httpResponse.setHeader("Access-Control-Allow-Headers", "*");//允许跨域的请求头
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");//是否携带cookie
filterChain.doFilter(servletRequest, servletResponse);
}
}
在xml中配置cors过滤器。
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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_4_0.xsd"
version="4.0">
<!--配置项目中的LoginServlet-->
<servlet>
<servlet-name>loginServlet</servlet-name>
<servlet-class>servlet.LoginServlet</servlet-class><!--LoginServlet在项目所处的位置-->
</servlet>
<!--为LoginServlet配置访问地址-->
<servlet-mapping>
<servlet-name>loginServlet</servlet-name>
<url-pattern>/login</url-pattern><!--地址以/开头-->
</servlet-mapping>
<!--配置编码过滤器-->
<filter>
<filter-name>encode</filter-name>
<filter-class>filter.EncodeFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>encode</filter-name>
<url-pattern>/*</url-pattern> <!--配置哪些请求地址进入到编码过滤器-->
</filter-mapping>
<!--配置解决跨越过滤器-->
<filter>
<filter-name>cors</filter-name>
<filter-class>filter.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
7.前端过渡
现在我们把Ajax加到我们之前用vue写的前端项目(vue-cli)里面,项目是之前用脚手架方式搭建的,感兴趣的友友们可以看一下:前端项目框架的搭建(Vue.js+ElementUI骨灰级保姆教程)。
在终端中输入命令:npm install axios
之前我们在main.js里面导入了路由和ElementUI框架,现在把axios也加进去。
发送post请求方式为:
this.$http.post("请求地址",需要传输的数据).then((resp)=>{ });
8.json数据格式+前端数据请求格式问题
8.1后端数据响应json处理
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,是为了在不同语言间传输数据方便,我们知道java和javaScript的语法是不一样的,那么我们在前后端之间的数据传输就要用到json。
转换代码:
ObjectMapper objectMapper = new ObjectMapper(); //创建对象
String json = objectMapper.writeValueAsString(响应的数据);//将响应的数据转化为字符串
利用writeValueAsString()方法可以将要响应的数据,例如对象,转化为一个json字符串,前端接收到json字符串后,自动会转换为js对象。
例如:
{"code":200,"data":{"id":1,"username":"zhangsan","password":null},"msg":"登录成功"}
要使用json数据交换格式,我们需要在lib目录里面导入6个json的jar包。
8.2前端请求数据格式处理
我们在用vue框架前,前端请求数据都是以键值对的,例如 username = " ",这样后端就用getparameter("键名")方法,能够获取到数据。而当我们使用了vue框架后,里面的数据格式会不同,例如 usernam : " ",这样就会导致数据传输到后端,但是后端getparameter("键名")方法读取不到。所以我们要在前端对数据进行处理。我们自定义一个转换函数~~~
//序列化为键=值&键=值
function jsonToString(jsonobj){
console.log(jsonobj)
var str = "";
for(var s in jsonobj){
str+=s+"="+jsonobj[s]+"&";
}
return str.substring(0,str.length-1);
}
即发送请求方式为:
this.$http.post("login",jsonToString(this.ruleForm)).then((resp)=>{ });
ruleForm为我们提交的表单中的数据,里面包含username,password。
9.后端验证用户名和密码(联系数据库查询)
结合上面我们所讲的知识,联系起来,搭建基本的后端代码。
9.1Reult类
为了响应数据格式统一,我们创建一个Reuslt类,用来封装每次后端响应的数据。
public class Result {
int code;//200成功 201失败
Object data;//数据 user对象
String msg; //信息提示
public Result(int code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public static Result success(Object data){ //操作成功
return new Result(200,data,"登录成功");
}
public static Result warn(){ //登录失败
return new Result(201,null,"账号或密码错误");
}
public static Result error(String msg){ //出现异常
return new Result(500,null,msg);
}
}
9.2User类
查询前实例化一个user对象,将查询的结果储存在user里面,没查询到user即为null
public class User {
private int id;
private String username;
private String password;
private String token; //会话跟踪处要用
public User() {
}
public User(int id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
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;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
9.3JDBC代码
联系数据库、通过JDBC查询返回User。代码相信大家都能理解,不做过多解释。
public class LoginDao {
static Connection con = null;
static PreparedStatement pst = null;
static ResultSet rs = null;
public static User find(String username,String password) throws SQLException {
User user = null;
try {
Class.forName("com.mysql.cj.jdbc.Driver");
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/demo?serverTimezone=Asia/Shanghai", "root", "root");
String sql = "select * from user where username = ? and password =?";
pst = con.prepareStatement(sql);
pst.setObject(1,username);
pst.setObject(2,password);
rs = pst.executeQuery();
while (rs.next()) {
user = new User();
user.setId(rs.getInt("id"));
user.setUsername(rs.getString("username"));
user.setPassword(null);
}
} catch (SQLException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
if(con!=null){
con.close();
}
if(pst!=null){
pst.close();
}
if(rs!=null){
rs.close();
}
}
return user;
}
}
9.4.LoginServlet类
通过ObjectMapper对象,将result转化为json字符串,响应到前端。
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Result result = null;
//要通过编码过滤器,所以不用设置编码。
//接收数据
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println(username); //打印一下,以便于看一下后端接收到前端发送的数据了没
System.out.println(password);
//设置响应编码
resp.setHeader("Content-Type","text/html;charset=utf-8");
//获取打印流
PrintWriter writer = resp.getWriter();
//处理 访问dao 与数据库交互,根据返回的结果向客户端响应内容
try {
User user = LoginDao.find(username,password);
if(user!=null){
result = Result.success(user);
}else {
result = Result.warn();
}
} catch (SQLException e) {
e.printStackTrace();
result = Result.error("系统忙"+e.getMessage());
}
//两种语言间对象结构不同,需要一种标准格式进行数据传输,json是一个轻量级的格式。
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
System.out.println(json);
writer.print(json);
}
}
10.前端收到响应判断是否登录+sessionstorage+localStorage
10.1前端接收到json字符串,做出判断
//如果发送成功,在此向用户提示,并跳转到登陆成功的组件
this.$http.post("login",jsonToString(this.ruleForm)).then((resp)=>{
//resp.data = Result 前端接收到json字符串后,自动会转换为js对象。 resp.data其实就是拿到了json字符串,只不过它可以看作是user对象
if(resp.data.code==200){
//直接跳转
this.$message({message: '登录成功',type: 'success'});//提示信息都是ElementUI里面的
this.$router.push("/main"); //路由切换,切换进主页面
}else if(resp.data.code==201){
this.$message({message:resp.data.msg,type: 'warning'});//提示信息
}else{
this.$message.error(resp.data.msg);//提示信息
}
});
10.2sessionstorage+localStorage
我们现在实现了到登录界面输入用户名和密码,然后提交表单到后端,后端查询后做出响应给前端,前端做出判断是否登录成功。
10.2.1提出问题:
但是现在有个问题,就是如果我们不通过登录页面,而是直接访问主界面,那么也会访问的到,并且如果我们关闭主界面,或者退出主界面,然后再访问主界面,它依旧是可以直接跳转到主界面,没有通过登录就可以访问主界面,这些显然是不切合实际的。
10.2.2问题分析:
那我们试想有没有这样一种东西,我们每次登录成功后把用户名存在浏览器里面,然后每次跳转到其他页面(除过跳转登录页面)时,我都要做一次判断,看一下浏览器里面有没有用户名信息,如果没有的话,则跳转至登录页面,表示要重新登陆!
10.2.3解决问题:
sessionstorage | 浏览器提供的一个会话级别的存储空间,浏览器关闭后立刻消失. | setItem("键",值); |
localStorage | 长久保存到浏览器。只能手动清除 | setItem("键",值) |
这里用sessionstorage就可以解决我们的问题:sessionStorage.setItem("username",用户名);
登录成功后将用户名信息存储即可,不仅在关闭页面后,需要清空存储。我们还得在主界面退出登
录的时候也得将存储清空。
主界面退出函数:
exit(){
this.$confirm('你确定要退出此窗口?', '提示', {
confirmButtonText: '确定',
type: 'warning'
}).then(() => {
sessionStorage.clear();//清除sessionStorage缓存
this.$router.push("/login");
});
}
改进过后的登录判断(将用户名信息存储):
//如果发送成功,在此向用户提示,并跳转到登陆成功的组件
this.$http.post("login",jsonToString(this.ruleForm)).then((resp)=>{
//resp.data = Result 前端接收到json字符串后,自动会转换为js对象。 resp.data其实就是拿到了json字符串,只不过它可以看作是user对象
if(resp.data.code==200){
//直接跳转
sessionStorage.setItem("username",resp.data.data.username);
this.$message({message: '登录成功',type: 'success'});//提示信息都是ElementUI里面的
this.$router.push("/main"); //路由切换,切换进主页面
}else if(resp.data.code==201){
this.$message({message:resp.data.msg,type: 'warning'});//提示信息
}else{
this.$message.error(resp.data.msg);//提示信息
}
});
10.2.4路由导航守卫
在我们路由(页面)每次想跳转时,我们都要自己在要切换的路由里mounted(当vue对象于标签绑定完毕后执行,类似于onload事件)添加判断信息:
mounted(){
this.username = sessionStorage.getItem("username");
if(this.username==null){
this.$router.push("/login");
}
}
这样特别麻烦,因为在我们的主界面得有很多操作,都得添加判断,在这我们引出路由导航守卫来改进。在router目录里index.js文件里添加。文件在前端项目框架的搭建(Vue.js+ElementUI)
里面都有讲过。感兴趣的友友可以了解一下~~~
import Vue from 'vue';
import router from 'vue-router'; /* 导入路由*/
/* 导入组件 */
import Login from "../Login.vue"; //../返回上一级目录
import Main from "../Main.vue";
Vue.use(router);
/* 定义组件路由 */
var rout = new router({
routes: [
{
path: '/login', //为组件定义地址
name: 'Login', //自己起一个名字 也可以不用起名
component: Login //与上面导入的名字必须对应一致
},
{
path: '/main',
component: Main
}
]
});
//在router里面的index.js文件里加入路由导航守卫,每次发生路由时触发
//to:要访问的路由 from:请求访问的路由 next:跳转
rout.beforeEach((to,from,next)=>{
if(to.path=='/login'){ //如果用户访问的登录页,直接放行
return next();
}else{
var username = sessionStorage.getItem("username");
if(username==null){
return next("/login");//跳转到登录页面
}else{
return next();//继续
}
}
})
//导出路由对象
export default rout;
11.会话跟踪
因为http的请求都是无状态的(请求--响应的模式),请求中并没有能识别对方身份的标识,那么我们在实际开发中就要解决这个问题,假如:我们在网上购物,虽然我们登录过了,然后点击付款,这个付款请求中没有携带用户信息,所以后端不知道是哪个用户请求的。
这个实现的功能称为web会话跟踪技术。
11.1token是怎么生成的?
token是由三部分组成,第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 用户的信息),第三部分是签证(signature),将这三段信息文本用.
链接一起就构成了token字符串。
例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
头信息(header):
完整的头部就像下面这样的JSON:
{
'typ': 'JWT', 声明类型,这里是jwt
'alg': 'HS256' 声明加密的算法 通常直接使用 HMAC HS256
}
然后将头部进行base64转码,构成了第一部分
载荷(payload):
可以添加任何的信息,一般添加用户的相关信息、需要的必要信息.但不建议添加敏感信息(例如密码),因为该部分在客户端可解密。
然后将其进行base64转码,得到第二部分
签证(signature):
签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64转码后的header和base64转码后的payload使用
.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
11.2token优点
1.简洁(Compact): 可以通过
URL
,POST
参数或者在HTTP header
发送,因为数据量小,传输速度也很快
2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
3.因为Token
是以JSON
加密的形式保存在客户端的,所以JWT
是跨语言的,原则上任何web形式都支持。
4.不需要在服务端保存会话信息,特别适用于分布式微服务。
11.3base64转码
说起base64转码,我们必须得知道base64它是一种编码方式,而不是用来加密的!!!
base64里面包含64个可打印的字符,在数据传输中,网络传送渠道并不支持所有的字节,base64可以在不改变传统传输协议上将不可打印的字符转码成它里面可打印的字符。
索引表:
转码方式:
1.将字符串中字节每三个一组,一个字节8bit,所有共有24位二进制位。
2.每6位为一组,总共分为4组,(因为2^6=64,刚好在可打印字符序列内)。
3.每一组转为十进制,对应索引表进行转换即可。
举例:
11.4后端生成token并响应
首先我们应导入java-jwt-3.8.2.jar下载及commons-codec-1.15.jar下载包。
然后再domain包里创建JWTUtil类:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtil {
/**
* jwt 生成 token
* @param id
* @param username * @return
*/
public static String token (Integer id, String username){
String token = "";
try {
//过期时间 为 1970.1.1 0:0:0 至 过期时间 当前的毫秒值 + 有效时间
Date expireDate = new Date(new Date().getTime() +60*60*24*1000);
//秘钥及加密算法 加盐
Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
//设置头部信息
Map<String,Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256");
//携带 id,账号信息,生成签名
token = JWT.create()
.withHeader(header)//头部
.withClaim("id",id)//用户id
.withClaim("username",username)//用户账号
.withExpiresAt(expireDate)//过期时间
.sign(algorithm);
}catch (Exception e){
e.printStackTrace(); return null;
}
return token;
}
public static boolean verify(String token){
try {
//验签
Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {//当传过来的 token 如果有问题,抛出异常
return false;
}
}
/**
* 获得 token 中 playload 部分数据,按需使用
* @param token
* @return
*/
public static DecodedJWT getTokenInfo(String token){
return JWT.require(Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE")).build().verify(token);
}
}
最后我们要将生成的token放在user里,随着Result响应到浏览器,对LoginServlet类:
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Result result = null;
//要通过编码过滤器,所以不用设置编码。
//接收数据
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println(username);
System.out.println(password);
resp.setHeader("Content-Type","text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
//处理 访问dao 与数据库交互,根据返回的结果向客户端响应内容
try {
User user = LoginDao.find(username,password);
if(user!=null){
String token = JWTUtil.token(user.getId(),user.getUsername());
user.setToken(token); //将token放在user类里,随着对象一起响应到后端
result = Result.success(user);
}else {
result = Result.warn();
}
} catch (SQLException e) {
e.printStackTrace();
result = Result.error("系统忙"+e.getMessage());
}
//两种语言间对象结构不同,需要一种标准格式进行数据传输,json是一个轻量级的格式。
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
System.out.println(json);
writer.print(json);
}
}
11.5前端接收到token后存储,并且请求时将token放在请求头
11.5.1存储:
//如果发送成功,在此向用户提示,并跳转到登陆成功的组件
this.$http.post("login",jsonToString(this.ruleForm)).then((resp)=>{
//resp.data = Result 前端接收到json字符串后,自动会转换为js对象。 resp.data其实就是拿到了json字符串,只不过它可以看作是user对象
if(resp.data.code==200){
//直接跳转
sessionStorage.setItem("username",resp.data.data.username);
sessionStorage.setItem("token",resp.data.data.token);//存储token
this.$message({message: '登录成功',type: 'success'});//提示信息都是ElementUI里面的
this.$router.push("/main"); //路由切换,切换进主页面
}else if(resp.data.code==201){
this.$message({message:resp.data.msg,type: 'warning'});//提示信息
}else{
this.$message.error(resp.data.msg);//提示信息
}
});
11.5.2前端发送请求时(axios请求拦截):
在main.js里面加入请求拦截:
//axios 请求拦截
axios.interceptors.request.use(config =>{
//将token字符串添加到头信息中,头信息中自定义起名为token,只定义一次即可
config.headers.token =sessionStorage.getItem('token');
return config;
})
主界面里随便定义一个onclick()事件,调用test()函数,来演示发送token
test(){
//看似发送时没有携带任何数据,其实已经把token信息放在头信息中了。
this.$http.get("/admin/test").then((resp)=>{ //我们等会在后端servlet包里面创建一个TestServlet类
});
}
11.6后端通过过滤器中统一验证
token过滤器:
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//向下转型
HttpServletRequest req = (HttpServletRequest)servletRequest;
String token = req.getHeader("token");
//验证token是否有效
boolean res = JWTUtil.verify(token);//如果为false则证明失效了,伪造的
if(res){//正确,继续向下进行
filterChain.doFilter(servletRequest,servletResponse);
}else {//错误,直接响应返回
HttpServletResponse resp = (HttpServletResponse)servletResponse;
Result result = new Result(202,false,"token有误,请重新登录!");
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
resp.setHeader("Content-Type","text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.print(json);
}
}
}
如果token无误,通过过滤器后,就要找到servlet地址,所以为了测试,我们再定义了一个TestServlet类来处理get请求:
public class TestServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Result result = null;
String token = req.getHeader("token");//获取token
DecodedJWT tokenInfo = JWTUtil.getTokenInfo(token);//解析token
Integer id = tokenInfo.getClaim("id").asInt();
String username = tokenInfo.getClaim("username").asString();
System.out.println("测试请求"+id+":"+username);
try {
result = new Result(200, true, "token正确");
}catch (Exception e){
result = Result.error("系统忙"+e.getMessage());
e.printStackTrace();
}
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
resp.setHeader("Content-Type","text/html;charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.print(json);
}
}
在xml中配置TestServlet和token过滤器:
这里注意:因为在开始登录时提交表单此时scessionstorage里面还没有信息,而且我们在登录时也不需要验证token,所以当前端请求得sevlet地址为login时,不需要通过token过滤器。
所以我们在配置其他访问地址时(除过登录)前面可以加上/admin/servlet地址,表示登录之后的访问。
<?xml version="1.0" encoding="UTF-8"?>
<web-app 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_4_0.xsd"
version="4.0">
<!--配置项目中的LoginServlet-->
<servlet>
<servlet-name>loginServlet</servlet-name>
<servlet-class>servlet.LoginServlet</servlet-class><!--LoginServlet在项目所处的位置-->
</servlet>
<!--为LoginServlet配置访问地址-->
<servlet-mapping>
<servlet-name>loginServlet</servlet-name>
<url-pattern>/login</url-pattern><!--地址以/开头-->
</servlet-mapping>
<!--配置项目中的TestServlet 用于测试token-->
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>servlet.TestServlet</servlet-class>
</servlet>
<!--为TestServlet配置访问地址-->
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/admin/test</url-pattern><!--地址以/开头-->
</servlet-mapping>
<!--配置编码过滤器-->
<filter>
<filter-name>encode</filter-name>
<filter-class>filter.EncodeFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>encode</filter-name>
<url-pattern>/*</url-pattern> <!--配置哪些请求地址进入到编码过滤器-->
</filter-mapping>
<!--配置解决跨越过滤器-->
<filter>
<filter-name>cors</filter-name>
<filter-class>filter.CorsFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>cors</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--配置token过滤器-->
<filter>
<filter-name>token</filter-name>
<filter-class>filter.TokenFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>token</filter-name>
<url-pattern>/admin/*</url-pattern> <!--在登录之后的所有请求,都要经过token验证过滤器-->
</filter-mapping>
</web-app>
11.7前端接收到验证响应做出判断
test(){ //看似发送时没有携带任何数据,其实已经把token信息放在头信息中了。
this.$http.get("/admin/test").then((resp)=>{
if(resp.data.code==202){
this.$message({message:resp.data.msg,type: 'warning'});
sessionStorage.clear();//清除sessionStorage缓存
this.$router.push("/login");
}
});
}
但是前端我们会有很多个这样的请求,我们需要每次写重复的token判断,所以我们既然有axios请求拦截,那我们在这用axios响应拦截来改进。
在main.js里面加入响应拦截,每次接收到响应都会先在响应拦截器里做处理。
//axios 添加响应拦截器
axios.interceptors.response.use((resp) =>{//正常响应拦截
if(resp.data.code==500){
ElementUI.Message({message:resp.data.msg,type:"error"})
}
if(resp.data.code==202){
ElementUI.Message({message:resp.data.msg,type: 'warning'});
sessionStorage.clear();//清除sessionStorage缓存
router.replace("/login");
}
return resp;
});
test(){ //看似发送时没有携带任何数据,其实已经把token信息放在头信息中了。
this.$http.get("/admin/test").then((resp)=>{
if(resp.data.code==200){
alert("之后操作代码");
}
});
}
这样我们就可以在test()里面设计我们之后的代码了。
至此,我们登录前后端分离式的交互基本搭建完成。