Json Web Token(JWT) 快速入门
推荐视频:【从零开始掌握JWT】
目录
第一章 会话跟踪
01 使用Cookie和Session,jsessionid
02 使用token
例子一:自定义token
例子二:使用redis存储token
第一章 会话跟踪
应用背景 :浏览器访问web应用,基于HTTP协议发送报文,HTTP是一种无状态协议,每当用户发出请求时,服务器就会做出响应,客户端与服务器之间的联系是离散的、非连续的。 服务器不知道是谁发的请求。当用户在同一网站的多个页面之间转换时,需要知道用户是谁时,会话跟踪技术就可以解决这个问题。
会话跟踪主要使用的技术:
URL重写,隐藏表单域,cookie,session
# 1、URL重写技术
就是在URL结尾添加一个附加数据以标识该会话,把会话ID通过URL的信息传递过去,以便在服务端进行识别不同的用户# 2、隐藏表单域:
将会话ID添加到HTML表单元素中提交到服务器,此表单不再客户端显示# 3、cookie
Cookie是web服务器发送给客户端的一小段信息,客户端请求时可以读取该信息发送到服务器端,进而进行用户的识别。对于客户端的每次请求,服务器都会将Cookie发送到客户端,在客户端可以进行保存,以便下次使用。# 4、session
在服务器端会创建一个session对象,产生一个sessionID来标识这个session对象然后将这个sessionID放入到Cookie中发送到客户端,下一次访问时,sessionID会发送到服务器,在服务器端进行识别不同的用户session是依赖Cookie的,如果cookie被禁用,那么session也将失效 ,session默认的会话时长为30分钟。
01 使用Cookie和Session,jsessionid
项目背景:进行一次请求转发,产生一次会话。
项目场景:
- @WebServlet 是Java Servlet规范中的注解,用于标识一个Servlet类,并指定该Servlet处理的URL模式等配置信息。
- @ServletComponentScan 是Spring Boot提供的注解,用于扫描并注册使用@WebServlet、@WebFilter和@WebListener注解标记的Servlet、Filter和Listener类。
package com.demo.servlet;
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.net.http.HttpRequest;
@WebServlet("myserlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//输出 sessionid
System.out.println("session="+req.getSession().getId());
// 转发到show.jsp
req.getRequestDispatcher("/show.jsp").forward(req, resp);
}
}
<html>
<body>
<h2>Hello World!</h2>
</body>
<a herf="myservlet">访问servlet</a>
</html>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h3>show.jsp</h3>
</body>
</html>
会话id:
02 使用token
token是什么?
token是什么:翻译是令牌,访问系统的凭证,令牌证明你是谁,你能做什么。
token是服务端生成的一串字符串,客户端首先请求一个令牌,当第一次登录后,服务器生成一个token并将此token返回给客户端,以后客户端只需带上这个token前来请求数据即可,无需再次带上用户名和密码。
例子一:自定义token
1.创建springboot项目
2.加入依赖,spring-web依赖,jquery (webjar)
3.在static目录下创建htm1目录,新建login.html
4.login.html登录页面,获取token
5.新建Logincontroller,模拟登录业务逻辑,使用UUID生成一个token,返回给请求
6.新建main.html作为业务逻辑处理页面,定义一个查询按钮,发起ajax请求,参数有token
7.创建一个处理query的方法,使用token验证登录用户信息。根据验证结果处理其他业务。
1)加入依赖,spring-web依赖,jquery (webjar)
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
2)在static目录下创建html目录,新建login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<!-- 从项目的 webjars 中加载了 jQuery 3.6.0 版本的库文件,通过该文件可以在页面中使用 jQuery 提供的功能和方法。-->
<script src="/webjars/jquery/3.6.0/jquery.js"></script>
<!-- 从 BootCDN 加载了 jQuery Cookie 插件的压缩版本。该插件使得在客户端可以方便地操作和管理 cookie,比如设置、获取和删除 cookie 等操作。-->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
</head>
<body>
<div style="margin-left: 300px" >
<h3>登录页面</h3>
<form id="loginFrm" action="" method="post">
用户名:<input type="text" name="name"> <br/>
密 码:<input type="text" name="pwd"> <br/>
<input type="button" id="loginBtn" value="登录">
</form>
</div>
<script type="text/javascript">
$(function () {
$("#loginBtn").on("click", function (){
$.ajax({
url:"app/login",
type:"post",
data:$("#loginFrm").serialize(),
dataType:"json",
success:function (resp){
// 存放到cookie
$.cookie("accessToken",resp.data);
alert("resp="+resp.msg+", token="+resp.data);
}
})
})
})
</script>
</body>
</html>
3)login.html登录页面,获取token
@RequestParam("name") String name
表示从请求中获取名为 "name" 的参数的值,并将其赋给名为name
的 String 类型变量。@RequestParam("pwd") String pwd
表示从请求中获取名为 "pwd" 的参数的值,并将其赋给名为pwd
的 String 类型变量。
UUID.randomUUID()
:UUID
类是 Java 中用于表示统一唯一标识符的类。randomUUID()
方法会生成一个新的随机 UUID 对象。
package com.example.root.controller;
import com.example.root.vo.ResultObject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@RestController
public class LoginController {
Map<String,String> tokenMap = new ConcurrentHashMap<>();
//登录方法
@PostMapping("/app/login")
public ResultObject login(@RequestParam("name") String name, @RequestParam("pwd") String pwd){
ResultObject ro = new ResultObject();
if ("lisi".equals(name) && "123".equals(pwd)){
// 登录成功,生成token
String token = UUID.randomUUID().toString().replaceAll("-","");
ro.setCode(0);
ro.setMsg("登录成功,返回token");
ro.setData(token);
//存储token
tokenMap.put(token, name);
}else{
ro.setCode(1000);
ro.setMsg("登录失败,没有token");
ro.setData("");
}
return ro;
}
}
分别对应Code;Msg;Token;
4)带上token访问前端API
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 从项目的 webjars 中加载了 jQuery 3.6.0 版本的库文件,通过该文件可以在页面中使用 jQuery 提供的功能和方法。-->
<script src="/webjars/jquery/3.6.0/jquery.js"></script>
<!-- 从 BootCDN 加载了 jQuery Cookie 插件的压缩版本。该插件使得在客户端可以方便地操作和管理 cookie,比如设置、获取和删除 cookie 等操作。-->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
</head>
<body>
<div>
<input type="text" name="studentId" id="studentId" value="1001"><br/>
<input type="button" name="queryBtn" id="queryBtn" value="访问api需要带上token"><br/>
</div>
<script type="text/javascript">
$(function(){
$("#queryBtn").on("click", function (){
$.ajax({
url:"app/query",
type:"post",
data:{
"studentId":$("#studentId").val(),
"accessToken":$.cookie("accessToken")
},
dataType:"json",
success:function (resp){
alert("resp="+resp.msg);
}
})
})
})
</script>
</body>
</html>
package com.example.root.controller;
import com.example.root.vo.ResultObject;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@RestController
public class LoginController {
Map<String,String> tokenMap = new ConcurrentHashMap<>();
//登录方法
@PostMapping("/app/login")
public ResultObject login(@RequestParam("name") String name, @RequestParam("pwd") String pwd){
ResultObject ro = new ResultObject();
if ("lisi".equals(name) && "123".equals(pwd)){
// 登录成功,生成token
String token = UUID.randomUUID().toString().replaceAll("-","");
ro.setCode(0);
ro.setMsg("登录成功,返回token");
ro.setData(token);
//存储token
tokenMap.put(token, name);
}else{
ro.setCode(1000);
ro.setMsg("登录失败,没有token");
ro.setData("");
}
return ro;
}
@PostMapping("/app/query")
public ResultObject query(Integer studentId, String accessToken){
ResultObject ro = new ResultObject();
// 判断token
if(tokenMap.containsKey(accessToken)){
//执行业务逻辑,查询学生
ro.setCode(0);
ro.setMsg("执行业务逻辑是成功的");
ro.setData("学生是id"+studentId);
}else {
ro.setCode(1);
ro.setMsg("不能访问系统");
ro.setData("");
}
return ro;
}
}
例子二:使用redis存储token
stringRedisTemplate
:这是 Spring Data Redis 提供的用于操作 Redis 数据库的模板类。opsForHash()
:这是StringRedisTemplate
提供的用于操作哈希表数据结构的方法,返回一个HashOperations
对象,通过这个对象可以进行哈希表的各种操作。putAll(key, map)
:这是HashOperations
接口中的一个方法,用于向指定哈希表key
中批量 添加 字段和对应的值。参数map
是一个 Java Map 对象,其中包含要添加到哈希表中的字段和值。entries(key)
:这是HashOperations
接口中的一个方法,用于 获取 指定哈希表key
中的所有字段和对应的值,并以 Map 的形式返回结果。
1)向redis存储token
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties:
# 设置redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
login2.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
<!-- 从项目的 webjars 中加载了 jQuery 3.6.0 版本的库文件,通过该文件可以在页面中使用 jQuery 提供的功能和方法。-->
<script src="/webjars/jquery/3.6.0/jquery.js"></script>
<!-- 从 BootCDN 加载了 jQuery Cookie 插件的压缩版本。该插件使得在客户端可以方便地操作和管理 cookie,比如设置、获取和删除 cookie 等操作。-->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
</head>
<body>
<div style="margin-left: 300px" >
<h3>登录页面redis</h3>
<form id="loginFrm" action="" method="post">
用户名:<input type="text" name="name"> <br/>
密 码:<input type="text" name="pwd"> <br/>
<input type="button" id="loginBtn" value="登录">
</form>
</div>
<script type="text/javascript">
$(function () {
$("#loginBtn").on("click", function (){
$.ajax({
url:"app/redis/login",
type:"post",
data:$("#loginFrm").serialize(),
dataType:"json",
success:function (resp){
// 消息框
alert("redis resp="+resp.msg+", token="+resp.data);
// 存放到cookie
$.cookie("accessRedisToken", resp.data);
}
})
})
})
</script>
</body>
</html>
LoginController2.java:
package com.example.root.controller;
import com.example.root.vo.ResultObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class LoginController2 {
private String TOKEN_KEY = "TOKEN:";
@Autowired
private StringRedisTemplate stringRedisTemplate;
//获取token,redis存储token
@PostMapping("/app/redis/login")
public ResultObject loginAccessToken(@RequestParam("name") String name,
@RequestParam("pwd") String pwd) {
ResultObject ro = new ResultObject();
// 登录处理,返回token
if ("lisi".equals(name) && "123".equals(pwd)){
// 登录成功,生成token
String token = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
// 准备数据
Map<String,String> map = new HashMap<>();
map.put("userId","1001");
map.put("name","lisi");
map.put("role","admin");
// 其他数据
// 存储数据到redis
String key = TOKEN_KEY + token;
stringRedisTemplate.opsForHash().putAll(key, map);
// 有效会话时间 20min
stringRedisTemplate.expire(key, 20, TimeUnit.MINUTES);
// 返回结果
ro.setCode(0);
ro.setMsg("登录成功");
ro.setData(token);
}else {
ro.setCode(1);
ro.setMsg("认证失败");
ro.setData("");
}
return ro;
}
}
redis安装和配置:Window下Redis的安装和部署详细图文教程(Redis的安装和可视化工具的使用)_redis安装-CSDN博客
注意访问:localhost:8080/login2.html
2) 从redis取数据访问前端API
main2.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- 从项目的 webjars 中加载了 jQuery 3.6.0 版本的库文件,通过该文件可以在页面中使用 jQuery 提供的功能和方法。-->
<script src="/webjars/jquery/3.6.0/jquery.js"></script>
<!-- 从 BootCDN 加载了 jQuery Cookie 插件的压缩版本。该插件使得在客户端可以方便地操作和管理 cookie,比如设置、获取和删除 cookie 等操作。-->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
</head>
<body>
<div>
<input type="text" name="studentId" id="studentId" value="1001"><br/>
<input type="button" name="queryBtn" id="queryBtn" value="访问api需要带上token(redis)"><br/>
</div>
<script type="text/javascript">
$(function(){
$("#queryBtn").on("click", function (){
$.ajax({
// 指定了AJAX请求的目标 URL到 controller类
url:"app/redis/query",
type:"post",
data:{
"studentId":$("#studentId").val(),
//获取 cookie
"accessToken":$.cookie("accessRedisToken")
},
dataType:"json",
// 设置请求成功后的处理函数
success:function (resp){
alert("resp="+resp.msg);
}
})
})
})
</script>
</body>
</html>
LoginController.java:
package com.example.root.controller;
import com.example.root.vo.ResultObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
public class LoginController2 {
private String TOKEN_KEY = "TOKEN:";
@Autowired
private StringRedisTemplate stringRedisTemplate;
//获取token,redis存储token
@PostMapping("/app/redis/login")
public ResultObject loginAccessToken(@RequestParam("name") String name,
@RequestParam("pwd") String pwd) {
ResultObject ro = new ResultObject();
// 登录处理,返回token
if ("lisi".equals(name) && "123".equals(pwd)){
// 登录成功,生成token
String token = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
// 准备数据
Map<String,String> map = new HashMap<>();
map.put("userId","1001");
map.put("name","lisi");
map.put("role","admin");
// 其他数据
// 存储数据到redis
String key = TOKEN_KEY + token;
stringRedisTemplate.opsForHash().putAll(key, map);
// 有效会话时间 20min
stringRedisTemplate.expire(key, 20, TimeUnit.MINUTES);
// 返回结果
ro.setCode(0);
ro.setMsg("登录成功");
ro.setData(token);
}else {
ro.setCode(1);
ro.setMsg("认证失败");
ro.setData("");
}
return ro;
}
//模拟业务处理方法
@PostMapping("app/redis/query")
public ResultObject query(Integer studentId, String accessToken){
ResultObject ro = new ResultObject();
// 检查token
String key = TOKEN_KEY + accessToken;
if(stringRedisTemplate.hasKey(key)){
// 获取redis存储数据,userId,role等
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
String userId = (String) map.get("userId");
String role = (String) map.get("role");
String name = (String) map.get("name");
// 处理业务逻辑
if("admin".equals(role)){
ro.setCode(0);
ro.setMsg("可以执行admin的操作");
ro.setData("执行查询学生是"+studentId+"姓名是:"+name);
}
}
return ro;
}
}
第二章 会用JWT
TOKEN的特点 :
1. 无状态,可扩展。无需session,可以把token数据从一个服务传递到其他服务,从网关发送到其他服务也很容易。
2. 安全性,token是可以进行加密处理,不存储敏感数据,而且token有实效的,有过期的,过期后需要重写验证。
3. 可扩展,token能实现不同应用之间的通信。 比如使用token访问第三方应用。
4. 跨平台,现代化的应用可能接入不同的设备和其他应用。使用一个有效 token可以在手机app,web应用等访问应用。
JWT规范,允许我们使用JWT在两个组织之间传递安全可靠的信息。
JWT是一个凭证。
JWT的工作原理:
用户先到服务器认证身份,认证后服务器返回一个json,就像这个样子。
以后用户再发起请求,就是带着这个ison数据,服务器拿这个ison对象确定用户身份,判断用户能执行操作,获取数据。为了防止用户篡改数据,服务器在生成这个json对象的时候,会加上签名。
服务器就不保存任何 session 数据了,也无需使用redis存储,服务器变成无状态了,从而比较容易实现扩展。
什么时候使用JWT:
1)授权Authorization:这是使用WT的最常见方案。用户登录后,每个后续请求都将包含JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用IWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
2)信息交换(Information Exchange):JSON Web令牌是在各方之间安全地传输信息的一种好方法。因为可以对]WT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否未被算改。
3)客户端会话(无Session)。
- HttpServlet 是Java Servlet API 的一个抽象类,用于处理来自客户端的HTTP请求并生成HTTP响应。开发人员可以通过继承HttpServlet类并重写其中的doGet()、doPost()等方法来处理特定的HTTP请求。
- doGet(HttpServletRequest request, HttpServletResponse response): 处理HTTP GET请求的方法。开发人员可以重写这个方法来处理GET请求,并生成相应的响应。
- doPost(HttpServletRequest request, HttpServletResponse response): 处理HTTP POST请求的方法。开发人员可以重写这个方法来处理POST请求,并生成相应的响应。
- doPut(HttpServletRequest request, HttpServletResponse response): 处理HTTP PUT请求的方法。
- doDelete(HttpServletRequest request, HttpServletResponse response): 处理HTTP DELETE请求的方法。
HttpServletRequest常用方法:
- getParameter(String name): 获取HTTP请求中的参数值。
- getMethod(): 获取HTTP请求的方法,如GET、POST等。
- getRequestURL(): 获取请求的URL。
- getSession(): 获取与该请求关联的会话,如果没有会话存在,则创建一个新的会话。
- getHeader(String name): 获取指定名称的请求头的值。
- getCookies(): 获取包含该请求的所有Cookie的数组。
HttpServletResponse常用方法:
- setContentType(String type): 设置响应的内容类型。
- setStatus(int sc): 设置响应的状态码(如200、404等)。
- getWriter(): 返回一个PrintWriter对象,用于向客户端发送文本数据。
- sendRedirect(String location): 发送一个重定向响应到指定的URL。
- addHeader(String name, String value): 添加一个响应头。
- setCookie(Cookie cookie): 将Cookie添加到响应中。