写在前面的话:
蒽,Token是一个什么东西?那我们为什么要用它?
Token:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端的请求只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。
使用Token的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
同时在分布式中我们会选用Token来身份验证,而不是session。 原因就是:
session是有状态的,一般存于服务器内存中,当服务器采用分布式时,session就会面对负载均衡问题。如果遇到负载均衡多服务器的情况,那我们就不好确认当前用户是否登录,因为多服务器不共享session。但是token是无状态的,token字符串里就保存了所有的用户关键信息
看一个简单的Token结构:
简单说一下,Token的原理:
1、上面图片中的Token,前面一串
"id:1001,status:1,endtime:2022-01-08 19:18:43"
字符串就是服务端在用户第一次登录的时候前端传入的用户名及密码验证成功,后端就查询该用户的id以及状态,即该账号是否正常使用中,或是已经停用的账号,endtime是后端产生Token的当前时间加一个有效时间,作为验证Token的有效时间。后面的fb675266364f697519dac6d1e6ec1da3
是根据前面的字符串生成的MD5加密码。
2、ImlkOjEwMDIzLHN0YXR1czowLGVuZHRpbWU6MjAyMi0wMS0wOSAyMDozNTo0OCI7MjEwOTJkNDRkNzVjOTM2NzY0YThiODA3ZmEzYWU1NzE=
这一串是后端返回给前端看到的数据,当然就是把"id:1001,status:1,endtime:2022-01-08 19:18:43";fb675266364f697519dac6d1e6ec1da3
进行base64编码,对数据进行一定保护。
3、前端收到后端的登录成功消息后,同时保存Token,以后的每次请求都要带上Token(放在请求头)。
4、后端接收到前端的业务请求后,首先验证Token是否有效,即Token中的数据有没有更改(切确的说是看有没有人伪造用户来后端请求,冒充用户)。
5、这里各位看友可能会有点迷糊,请看下面简单的交互图
现在开始代码阶段:
1、先数据库连接成功
2、接受前端请求,后端查询数据库判断是否创建Token,即该用户是否合法
3、合法验证成功,即开始创建Token并传给前端
4、前端拿到Token并保存,当前登录成功
5、前端再次发送请求给后端,带上Token(放在请求头内)
6、后端接受请求,首先验证Token是否有效,若有效则返回前端的业务请求,并将当前新的Token传给前端
7、前端保存新的Token,为后面的请求,即完成一个Token的流程
1、数据库的连接如下:
private static final java.lang.String url = "jdbc:mysql://localhost:3306/xiaofang?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; private static final java.lang.String userName = "root"; private static final java.lang.String passwd = "root"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } **数据库文件:** ```sql /* Navicat Premium Data Transfer Source Server : mysql Source Server Type : MySQL Source Server Version : 80025 Source Host : localhost:3306 Source Schema : xiaofang Target Server Type : MySQL Target Server Version : 80025 File Encoding : 65001 Date: 09/01/2022 22:23:32 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `phone` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `head` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `addr` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `status` int(0) NULL DEFAULT NULL, `created` datetime(6) NULL DEFAULT NULL, `deleted` datetime(6) NULL DEFAULT NULL, `type` int(0) NOT NULL, PRIMARY KEY (`username`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES ('1001', 'cc', 'cc', 'cc123', NULL, '/static/img/user.jpg', NULL, 1, '2022-01-08 19:00:00.000000', NULL, 0); INSERT INTO `user` VALUES ('10023', 'tt', 'tt', 'tt456', NULL, NULL, NULL, NULL, NULL, NULL, 0); SET FOREIGN_KEY_CHECKS = 1;
2、同样的前端我们不写页面,后端直接用Servlet的doget拿参数,来验证:
不清楚Servlet前后端交互的看友,可以看看我前面的这篇文章Servlet前后端简单交互
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setHeader("Content-Type", "application/json;charset=UTF-8"); String name = req.getParameter("name"); String password = req.getParameter("password"); try (Connection conn = DriverManager.getConnection(url, userName, passwd); PreparedStatement ps = conn.prepareStatement("select * from user where name =? and password =?;") ) { ps.setString(1, name); ps.setString(2, password); ResultSet rs = ps.executeQuery(); //字符串等于比较,要记得不要混了用equals while (rs.next()) { if ((rs.getString("name").equals(name)) && (rs.getString("password").equals(password))) { resp.getWriter().println("登录成功!欢迎您" + rs.getString("name")); int id = rs.getInt("id"); int status = rs.getInt("status"); resp.getWriter().println("Token的值为:" + createToken(id, status)); } else { resp.getWriter().println("-----登录失败!-----"); resp.getWriter().println("-----用户名或密码错误!-----"); } } rs.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } }
3、合法验证成功,即开始创建Token并传给前端
/** * 创建Token对象 * * @param id * @param status */ public static String createToken(int id, int status) throws UnsupportedEncodingException { SimpleDateFormat sif = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = new Date(); //当前时间加五分钟,作为Token的有效时间 Date dateafter = new Date(date.getTime() + 300000); Object endtime = sif.format(dateafter); String json = "id:" + id + ",status:" + status + ",endtime:" + endtime; JSONObject jsonObject = new JSONObject(); String ss = jsonObject.toJSONString(json); String begin = ss + ";" + getMd5(ss); //调用并转换成base64 //这里的Base64码转换idea提供了相关的方法,我是调用了自己写的Base64码,想看的可以关注我,看我的文章哦 return Base644.Encoding(begin); }
将Token传给前端的代码,写在了登录验证成功就创建并传递给前端
resp.getWriter().println("Token的值为:" + createToken(id, status));
4、前端再次发送请求给后端,带上Token(放在请求头内)
这里有多种请求方法,比如Postman,或者Idea的.http请求文件
Token.http内容:
要知道这个是前端的请求,要带上我们前面后端给的Token
POST http://localhost:8080/war/helloworld Content-Type: application/json;charset=utf-8 Token: ImlkOjEwMDEsc3RhdHVzOjEsZW5kdGltZToyMDIyLTAxLTA5IDIwOjE4OjAwIjszZjRmYzMyMWE3Y2FlNThmMjhmNzBkYmYxNTg2YmNkMg== { "id": 1001, "name": "云先生", "date": [ { "create_time": "2022-01-06" } ] }
5、后端接受请求,首先验证Token是否有效,若有效则返回前端的业务请求,并将当前新的Token传给前端。
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setHeader("Content-Type", "application/json;charset=UTF-8"); try { timethan(req, resp); //如果用户身份的token验证通过,进行前端请求的业务操作, // 并返回前端一个新的token } catch (ParseException e) { e.printStackTrace(); } } /** * 后端拿到前端请求头的token对象,验证身份,比较有效时间 * * @param req * @param resp * @return * @throws ParseException */ public static void timethan(HttpServletRequest req, HttpServletResponse resp) throws ParseException, IOException { //拿到token对象 String str = Base644.Decoding(req.getHeader("Token")); //将token切割成两部分 //str:"id:1001,status:1,endtime:2022-01-08 19:18:43";fb675266364f697519dac6d1e6ec1da3 String ahead = str.substring(0, str.indexOf(";")); String behind = str.substring(str.indexOf(";") + 1, str.length()); //时间从字符串中截取出来 String ss = ahead.substring(ahead.indexOf("e"), ahead.length()); String cc = ss.substring(ss.indexOf(":") + 1, ss.length()); //将字符串的时间转为long类型的数据进行比较 SimpleDateFormat sif = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date data = sif.parse(cc); long stime = data.getTime(); Date date1 = new Date(); Date dateafter = new Date(date1.getTime()); long nowtime = dateafter.getTime(); long differen = stime - nowtime; //解析当前token对象的id,状态 int id = Integer.parseInt(ahead.substring(ahead.indexOf("i") + 3, ahead.indexOf(","))); int status = Integer.parseInt(ahead.substring(ahead.indexOf("s") + 7, ahead.lastIndexOf(","))); if (getMd5(ahead).equals(behind)) { try { if (differen < 0) { resp.getWriter().println("Token有效时间已过,请重新登录!"); } else { resp.getWriter().println("前端请求的业务处理结果,后端已送到,请接受!"); resp.getWriter().println("Token:"+createToken(id,status)); } } catch (IOException e) { e.printStackTrace(); } } else { resp.getWriter().println("Token信息有误!"); } }
最后,前端继续把Token保存,留作下一次的使用。 到了这里Token一个的前后端交互就完成了。
全部源码:
package com.example.demo.newyears; import com.alibaba.fastjson.JSONObject; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.*; // 先数据库连接成功 // 接受前端请求,后端查询数据库判断是否创建Token,即该用户是否合法 // 合法验证成功,即开始创建Token并传给前端 // 前端拿到Token并保存,当前登录成功 // 前端再次发送请求给后端,带上Token(放在请求头内) // 后端接受请求,首先验证Token是否有效,若有效则返回前端的业务请求,并将当前新的Token传给前端 // 前端保存新的Token,为后面的请求,即完成一个Token的流程 /** * @Author:Yun * @Date:2022/01/08/11:50 * @Description: **/ @WebServlet(name = "test", urlPatterns = "/helloworld") public class Token extends HttpServlet { private static final java.lang.String url = "jdbc:mysql://localhost:3306/xiaofang?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC"; private static final java.lang.String userName = "root"; private static final java.lang.String passwd = "root"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setHeader("Content-Type", "application/json;charset=UTF-8"); String name = req.getParameter("name"); String password = req.getParameter("password"); try (Connection conn = DriverManager.getConnection(url, userName, passwd); PreparedStatement ps = conn.prepareStatement("select * from user where name =? and password =?;") ) { ps.setString(1, name); ps.setString(2, password); ResultSet rs = ps.executeQuery(); //字符串等于比较,要记得不要混了用equals while (rs.next()) { if ((rs.getString("name").equals(name)) && (rs.getString("password").equals(password))) { resp.getWriter().println("登录成功!欢迎您" + rs.getString("name")); int id = rs.getInt("id"); int status = rs.getInt("status"); resp.getWriter().println("Token的值为:" + createToken(id, status)); } else { resp.getWriter().println("-----登录失败!-----"); resp.getWriter().println("-----用户名或密码错误!-----"); } } rs.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setHeader("Content-Type", "application/json;charset=UTF-8"); try { timethan(req, resp); //如果用户身份的token验证通过,进行前端请求的业务操作, // 并返回前端一个新的token } catch (ParseException e) { e.printStackTrace(); } } /** * 后端拿到前端请求头的token对象,验证身份,比较有效时间 * * @param req * @param resp * @return * @throws ParseException */ public static void timethan(HttpServletRequest req, HttpServletResponse resp) throws ParseException, IOException { //拿到token对象 String str = Base644.Decoding(req.getHeader("Token")); //将token切割成两部分 //str:"id:1001,status:1,endtime:2022-01-08 19:18:43";fb675266364f697519dac6d1e6ec1da3 String ahead = str.substring(0, str.indexOf(";")); String behind = str.substring(str.indexOf(";") + 1, str.length()); //时间从字符串中截取出来 String ss = ahead.substring(ahead.indexOf("e"), ahead.length()); String cc = ss.substring(ss.indexOf(":") + 1, ss.length()); //将字符串的时间转为long类型的数据进行比较 SimpleDateFormat sif = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date data = sif.parse(cc); long stime = data.getTime(); Date date1 = new Date(); Date dateafter = new Date(date1.getTime()); long nowtime = dateafter.getTime(); long differen = stime - nowtime; //解析当前token对象的id,状态 int id = Integer.parseInt(ahead.substring(ahead.indexOf("i") + 3, ahead.indexOf(","))); int status = Integer.parseInt(ahead.substring(ahead.indexOf("s") + 7, ahead.lastIndexOf(","))); if (getMd5(ahead).equals(behind)) { try { if (differen < 0) { resp.getWriter().println("Token有效时间已过,请重新登录!"); } else { resp.getWriter().println("前端请求的业务处理结果,后端已送到,请接受!"); //创建一个新的Token resp.getWriter().println("Token:"+createToken(id,status)); } } catch (IOException e) { e.printStackTrace(); } } else { resp.getWriter().println("Token信息有误!"); } } /** * 创建Token对象 * * @param id * @param status */ public static String createToken(int id, int status) throws UnsupportedEncodingException { SimpleDateFormat sif = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); Date date = new Date(); Date dateafter = new Date(date.getTime() + 300000); Object endtime = sif.format(dateafter); String json = "id:" + id + ",status:" + status + ",endtime:" + endtime; JSONObject jsonObject = new JSONObject(); String ss = jsonObject.toJSONString(json); System.out.println(ss); String begin = ss + ";" + getMd5(ss); //调用并转换成base64 return Base644.Encoding(begin); } /** * 将字符串转为MD5加密的方法 * * @param org * @return */ public static String getMd5(String org) { MessageDigest md = null; try { md = MessageDigest.getInstance("MD5"); md.reset(); md.update(org.getBytes(StandardCharsets.UTF_8)); byte[] byteArray = md.digest(); StringBuilder md5StrBuff = new StringBuilder(); for (int i = 0; i < byteArray.length; i++) { if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) md5StrBuff.append("0").append(Integer.toHexString(0xFF & byteArray[i])); else md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i])); } return md5StrBuff.toString(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } return ""; } }
最后有问题的看友,可以留言或者私聊我欧。