Token 是服务端生成的一串字符串,以作为客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token 便将此 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带上用户名和密码。
无状态、可扩展
Tokens
是无状态的,并且能够被扩展。基于这种无状态和不存储Session
信息,负载负载均衡器能够将用户信息从一个服务传到其他服务器上。tokens
自己hold
住了用户的验证信息。Tokens
能够创建与其它程序共享权限的程序。
跨域
安全
token
而不再是发送cookie
能够防止CSRF
(跨站请求伪造)。即使在客户端使用cookie
存储token
,cookie
也仅仅是一个存储机制而不是用于认证。不将信息存储在Session
中,让我们少了对session
操作。Token 设置过期时间
官网:https://jwt.io/
GitHub:https://github.com/auth0/java-jwt
API:https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html
JSON Web令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地将信息作为 JSON 对象传输。 由于此信息是经过数字签名的,因此可以被验证和信任。 可以使用秘钥(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公用/专用密钥对对 JWT 进行签名。
在前后端或者服务器进行交互的过程中,通过 JSON 形式作为 Web 应用中的令牌,以完成数据传输、加密、签名等相关处理操作。
JWT 有3个组成部分,每个部分之间用 . 进行分隔
格式为
xxxxx.yyyyy.zzzzz
1、头部(header) 声明类型以及加密算法,例如 HMAC、SHA256或 RSA。
{"alg":"HS256","typ":"JWT"}
2、载荷(payload) 携带一些用户身份信息,用户id,颁发机构,颁发时间,过期时间等。用Base64进行了处理。这一段其实是明文,所以一定不要放敏感信息。例如
{"id": 200, "username": "易烊千玺"}
3、签证(signature) 签名信息,使用了自定义的盐然后加密后的结果,目的就是为了保证签名的信息没有被别人改过,这个一般是让服务器验证的。
public class JwtTest {
// 通过JWT获取token
@Test
public void createToken() {
HashMap<String, Object> header = new HashMap<>();
header.put("alg", "HS256");
header.put("typ", "JWT");
Calendar expireTime = Calendar.getInstance();
expireTime.add(Calendar.MINUTE, 30);
// 创建一个Token
String token = JWT.create().
// 头部
withHeader(header).
// 载荷
withClaim("username", "易烊千玺").
withClaim("id", 1).
withExpiresAt(expireTime.getTime()).
// 签名(盐)
sign(Algorithm.HMAC256("ABC8D!EFG"));
System.out.println(token);
}
// 验证JWT的token
@Test
public void verifyToken() {
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("ABC8D!EFG")).build();
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjE4MTM4NzY0LCJ1c2VybmFtZSI6IuaYk-eDiuWNg-eOuiJ9.IhMvu1CPUVd94rPTu6HNQTGP_fBTOaJaAofD7WTDFwo");
System.out.println("token头:" + verify.getHeader());
System.out.println("token载荷:" + verify.getPayload());
System.out.println("token签名:" + verify.getSignature());
System.out.println("id:" + verify.getClaim("id").asInt());
System.out.println("username:" + verify.getClaim("username").asString());
System.out.println("过期时间:" + verify.getExpiresAt());
}
}
/**
* JWT工具类
*/
public class JwtUtils {
// token头算法
private static final String ALG = "HS256";
// token头类型
private static final String TYP = "JWT";
// token签名算法
private static final Algorithm ALGORITHM;
// token头
private static final HashMap<String, Object> HEADER = new HashMap<>();
// token签名
private static final String SIGN = "QwErTyUi";
// 静态代码块加载token头以及token签名算法
static {
HEADER.put("alg", ALG);
HEADER.put("typ", TYP);
ALGORITHM = Algorithm.HMAC256(SIGN);
}
/**
* 获取Token
*
* @param claim 载荷
* @param expireTime 过期时间,单位:分钟
* @return token
*/
public static String getToken(Map<String, Object> claim, Integer expireTime) {
// 返回创建的token
return JWT.create().
// 设置头
withHeader(HEADER).
// 设置过期时间,单位为分钟
withExpiresAt(new Date(System.currentTimeMillis() + expireTime * 1000 * 60)).
// 设置载荷
withClaim("claim", claim).
// 设置签名
sign(ALGORITHM);
}
/**
* 解析token
*
* @param token token
* @return 验证的信息
*/
public static DecodedJWT parseToken(String token) {
// 获取token
return JWT.require(ALGORITHM).build().verify(token);
}
/**
* 根据token获取载荷信息
*
* @param token token
* @return 验证的载荷
*/
public static Map<String, Object> getClaim(String token) {
// 获取token中的载荷信息
return parseToken(token).getClaim("claim").asMap();
}
/**
* 判断是否过期
*
* @param token token
* @return 是否已过期
*/
public static boolean isExpiration(String token) {
return parseToken(token).getExpiresAt().before(new Date());
}
}
测试
public class JwtUtilsTest {
@Test
public void test() {
// 准备载荷
HashMap<String, Object> map = new HashMap<>();
map.put("username", "易烊千玺");
// 获取token
String token = JwtUtils.getToken(map, 30);
System.out.println(token);
// 获取token中的载荷
Map<String, Object> claim = JwtUtils.getClaim(token);
String username = (String) claim.get("username");
System.out.println(username);
}
}
pom.xml
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webmvcartifactId>
<version>5.3.5version>
dependency>
<dependency>
<groupId>javax.servletgroupId>
<artifactId>javax.servlet-apiartifactId>
<version>3.1.0version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.14.0version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.12.2version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.16version>
dependency>
dependencies>
实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String username;
private String password;
}
业务层及实现类
public interface UserService {
User login(String username, String password);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public User login(String username, String password) {
if (username.equals("易烊千玺") && password.equals("123456")) {
return new User(username, password);
}
return null;
}
}
web.xml
<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">
<servlet>
<servlet-name>dispatcherServletservlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
<init-param>
<param-name>contextConfigLocationparam-name>
<param-value>classpath:springMvc.xmlparam-value>
init-param>
<load-on-startup>1load-on-startup>
servlet>
<servlet-mapping>
<servlet-name>dispatcherServletservlet-name>
<url-pattern>/url-pattern>
servlet-mapping>
web-app>
login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
登录页
index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
主页
欢迎来到主页
controller
@Controller
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("login")
@ResponseBody
public Map<String, Object> login(User user) {
// 准备一个Map用来返回Json类型数据
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
map.put("message", "登录失败");
// 登录并获取User对象
User result = userService.login(user.getUsername(), user.getPassword());
// 如果User对象不为空说明登录成功
if (result != null) {
// 准备载荷信息
Map<String, Object> claim = new HashMap<>();
claim.put("username", result.getUsername());
// 获取token
String token = JwtUtils.getToken(claim, 30);
map.put("code", 200);
map.put("message", "登录成功");
map.put("token", token);
}
return map;
}
@PostMapping("get")
@ResponseBody
public Map<String, Object> get(String token) {
// 准备一个Map用来返回Json类型数据
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
// 如果token不存在,访问被拒绝
if (token == null) {
map.put("message", "token不存在,访问被拒绝");
}
// 通过工具类获取载荷信息
try {
Map<String, Object> claim = JwtUtils.getClaim(token);
map.put("message", "允许访问");
map.put("code", 200);
map.put("data", claim);
// 抛出对应的异常
} catch (AlgorithmMismatchException e) {
map.put("message", "算法不匹配");
} catch (InvalidClaimException e) {
map.put("message", "非法载荷");
} catch (SignatureVerificationException e) {
map.put("message", "签名不匹配");
} catch (TokenExpiredException e) {
map.put("message", "token已过期");
} catch (Exception e) {
map.put("message", "token异常,访问被终止");
}
return map;
}
}
springMvc.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.fc"/>
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
beans>
index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
主页
欢迎来到主页
controller
@Controller
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("login")
@ResponseBody
public Map<String, Object> login(User user) {
// 准备一个Map用来返回Json类型数据
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
map.put("message", "登录失败");
// 登录并获取User对象
User result = userService.login(user.getUsername(), user.getPassword());
// 如果User对象不为空说明登录成功
if (result != null) {
// 准备载荷信息
Map<String, Object> claim = new HashMap<>();
claim.put("username", result.getUsername());
// 获取token
String token = JwtUtils.getToken(claim, 30);
map.put("code", 200);
map.put("message", "登录成功");
map.put("token", token);
}
return map;
}
@GetMapping("get")
@ResponseBody
public Map<String, Object> get(HttpServletRequest request) {
// 准备一个Map用来返回Json类型数据
Map<String, Object> map = new HashMap<>();
map.put("code", 200);
map.put("message", "请求成功");
map.put("data", request.getAttribute("claim"));
return map;
}
}
自定义 Jwt 拦截器
/**
* 自定义Jwt拦截器
*/
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从请求头中获取token
String token = request.getHeader("token");
Map<String, Object> map = new HashMap<>();
map.put("code", -1);
try {
// 解析token并获取载荷
Map<String, Object> claim = JwtUtils.getClaim(token);
// 设置到域对象中
request.setAttribute("claim", claim);
return true;
// 抛出对应的异常
} catch (AlgorithmMismatchException e) {
map.put("message", "算法不匹配");
} catch (InvalidClaimException e) {
map.put("message", "非法载荷");
} catch (SignatureVerificationException e) {
map.put("message", "签名不匹配");
} catch (TokenExpiredException e) {
map.put("message", "token已过期");
} catch (Exception e) {
map.put("message", "token异常,访问被终止");
}
// 获取Jackson核心处理对象
ObjectMapper objectMapper = new ObjectMapper();
// 对象转Json字符串
String json = objectMapper.writeValueAsString(map);
// 设置响应内容类型
response.setContentType("application/json; charset=UTF-8");
// 发送响应
response.getWriter().println(json);
return false;
}
}
springMvc.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.fc"/>
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"/>
<mvc:annotation-driven/>
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/user/login"/>
<bean class="com.fc.interceptor.JwtInterceptor"/>
mvc:interceptor>
mvc:interceptors>
<mvc:default-servlet-handler/>
beans>
1、pom.xml引入依赖
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>2.12.2version>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
<version>1.15version>
dependency>
2、jwt工具类
/**
* 自定义jwt工具类
*/
public class JwtUtils {
/**
* 获取JWT
*
* @param claim 载荷
* @param salt 盐
* @return JWT
*/
public static String getJWT(Map<String, Object> claim, String salt) {
// 添加一个时间戳
claim.put("time", System.currentTimeMillis() + 1000 * 60 * 30);
// 未编码的头部
String decodedHeader = "{\"alg\":\"HS256\"}";
// 使用base64算法对二进制数据进行编码,获取编码后的头部
String header = Base64.getEncoder().encodeToString(decodedHeader.getBytes(StandardCharsets.UTF_8));
// 准备json解析的对象
ObjectMapper objectMapper = new ObjectMapper();
String jsonDecoded = null;
try {
// 将载荷转为json字符串
jsonDecoded = objectMapper.writeValueAsString(claim);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 使用base64算法对二进制数据进行编码,获取编码后的载荷
String payload = Base64.getEncoder().encodeToString(jsonDecoded.getBytes(StandardCharsets.UTF_8));
// 获取16进制的md5加密后的签名
String signature = DigestUtils.md5Hex(jsonDecoded + salt);
// 准备一个StringBuilder
StringBuilder stringBuilder = new StringBuilder();
// 将头部,载荷,签名进行拼接
StringBuilder append = stringBuilder.append(header).append(".").append(payload).append(".").append(signature);
// 类型转换获取JWT
return new String(append);
}
/**
* 解析JWT
*
* @param jwt jwt
* @param salt 盐
* @return 响应结果
*/
public static Map<String, Object> decodeJwt(String jwt, String salt) {
// 声明响应的结果,默认为false
Map<String, Object> result = new HashMap<>();
result.put("success", false);
// 拆分jwt为字符串数组
String[] jwtArr = jwt.split("\\.");
// 获取未解码的载荷
String payloadBase64 = jwtArr[1];
// 获取解码后的载荷字节数组
byte[] decode = Base64.getDecoder().decode(payloadBase64.getBytes(StandardCharsets.UTF_8));
// 获取解码后的载荷
String payload = new String(decode);
// 准备json解析对象
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> claim = null;
try {
// 将json类型的载荷解析为Map对象
claim = objectMapper.readValue(payload, new TypeReference<Map<String, Object>>() {});
// 获取载荷中的时间
Long time = (Long) claim.get("time");
// 如果大于当前时间说明jwt已失效,携带错误信息并返回
if (time < System.currentTimeMillis()) {
result.put("message", "当前jwt已失效");
return result;
}
} catch (JsonProcessingException e) {
e.printStackTrace();
}
// 载荷加上盐值
payload += salt;
// 使用md5生成签名
String sign = DigestUtils.md5Hex(payload);
// 和jwt中的签名对比
if (sign.equals(jwtArr[2])) {
// 将载荷放入响应内容中并返回
result.put("success", true);
result.put("message", "访问成功");
result.putAll(claim);
return result;
}
// 签名不相同说明jwt被篡改了,返回错误信息
result.put("message", "非法的jwt");
return result;
}
}
3、测试类
public class JwtTest {
@Test
public void test() {
// 准备一个盐值
String salt = "123456";
// 准备载荷
Map<String, Object> map = new HashMap<>();
map.put("username", "送你一朵小红花");
map.put("id", 1);
map.put("age", 21);
// 使用工具类生成jwt
String jwt = JwtUtils.getJWT(map, salt);
System.out.println(jwt);
// 使用盐值解析jwt
Map<String, Object> payload = JwtUtils.decodeJwt(jwt, salt);
// 判断是否解析成功
boolean success = (boolean) payload.get("success");
if (success) {
System.out.println(payload.get("username"));
} else {
System.out.println(payload.get("message"));
}
}
}
4、前端页面
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面title>
head>
<script src="js/jquery-1.8.3.min.js">script>
<body>
<form action="user/login" method="post">
<table align="center">
<caption><h1>测试jwth1>caption>
<tr>
<td>账号td>
<td><input type="text" name="username">td>
tr>
<tr>
<td>密码td>
<td><input type="password" name="password">td>
tr>
<tr>
<td align="center" colspan="2">
<input type="reset" value="重置">
<button type="button" onclick="login()">提交button>
td>
tr>
table>
form>
body>
<script type="text/javascript">
function login() {
var data = $("form").serialize();
$.ajax({
url: "user/login",
data: data,
success: function (res) {
localStorage.setItem("jwt", res.jwt);
window.location.href = "/success.html";
}
})
}
script>
html>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录成功页title>
head>
<script src="js/jquery-1.8.3.min.js">script>
<body>
<button type="button" onclick="add()">添加button>
<button type="button" onclick="show()">查看button>
body>
<script type="text/javascript">
var jwt = localStorage.getItem("jwt");
function add() {
$.ajax({
url: "user/add",
data: {"jwt": jwt},
success: function (res) {
alert(res.message);
}
})
}
function show() {
$.ajax({
url: "user/select",
data: {"jwt": jwt},
success: function (res) {
alert(res.message);
}
})
}
script>
html>
5、控制层
@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("login")
public Map<String, Object> login(String username, String password, HttpServletResponse response) {
Map<String, Object> map = new HashMap<>();
// 判断登录用户是否为管理员用户,如果是,添加管理员权限
if (username.equals("易烊千玺") && password.equals("123456")) {
map.put("auth", "admin");
} else {
// 不是管理员,添加普通权限
map.put("auth", "user");
}
// 将生成的jwt存入响应内容中
map.put("jwt", JwtUtils.getJWT(map, "salt"));
return map;
}
@RequestMapping("add")
public Map<String, Object> add(@RequestParam String jwt) {
// 解码jwt获取载荷
Map<String, Object> map = JwtUtils.decodeJwt(jwt, "salt");
// 获取载荷中的参数
boolean success = (boolean) map.get("success");
// 判断jwt是否解码成功
if (success) {
// 获取jwt中存储的用户权限
String auth = String.valueOf(map.get("auth"));
// 判断是否为管理员权限
if (auth.equals("admin")) {
map.put("message", "欢迎管理员进行访问");
} else {
map.put("message", "权限不够");
}
}
return map;
}
@RequestMapping("select")
public Map<String, Object> select(@RequestParam String jwt) {
// 解码jwt获取载荷
Map<String, Object> map = JwtUtils.decodeJwt(jwt, "salt");
// 获取载荷中的参数
boolean success = (boolean) map.get("success");
// 判断jwt是否解码成功
if (success) {
String auth = String.valueOf(map.get("auth"));
// 判断是否为管理员权限
if (auth.equals("admin")) {
map.put("message", "欢迎管理员访问");
} else {
map.put("message", "欢迎访问");
}
}
return map;
}
}
boolean success = (boolean) map.get("success");
// 判断jwt是否解码成功
if (success) {
// 获取jwt中存储的用户权限
String auth = String.valueOf(map.get("auth"));
// 判断是否为管理员权限
if (auth.equals("admin")) {
map.put("message", "欢迎管理员进行访问");
} else {
map.put("message", "权限不够");
}
}
return map;
}
@RequestMapping("select")
public Map select(@RequestParam String jwt) {
// 解码jwt获取载荷
Map map = JwtUtils.decodeJwt(jwt, "salt");
// 获取载荷中的参数
boolean success = (boolean) map.get("success");
// 判断jwt是否解码成功
if (success) {
String auth = String.valueOf(map.get("auth"));
// 判断是否为管理员权限
if (auth.equals("admin")) {
map.put("message", "欢迎管理员访问");
} else {
map.put("message", "欢迎访问");
}
}
return map;
}
}