博客主页:作者主页
简介:JAVA领域优质创作者、一名在校大三学生、在校期间参加各种省赛、国赛,斩获一系列荣誉
关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿
传统的Web应用中,使用session来存在用户的信息,每次用户认证通过以后,服务器需要创建一条记录 保存用户信息,通常是在内存中。
JWT是是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。并且是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT
就是token的一种具体实现方式,其全称是JSON Web Token
官网地址:https://jwt.io/
先来说一下基本的流程:
与传统的认证方式相比有哪些好处
原理图
1、前端也就是客户端,通过表单提交用户名和密码信息发送到后端(服务的)
2、后端(服务端)验证该用户的用户名和密码是否正确,验证通过通过代码规定生成相对应的token令牌,token令牌将包含用户的数据i西信息作为Payload,与JWT Header分别进行Base64编码拼接后签名,生产类似于zzz.sss.rrr的字符串
3、后端(服务端)将生成的token作为判断用户登录成功的依据
4、前端拿到后端发来的token令牌后存储起来,等下一次用户需要再次请求服务器时,该用户将携带token(未过期的)请求服务器端以获取数据
5、后端拦截该用户的请求,判断token是否过期,未过期则执行业务逻辑,返回用户需要的数据
6、后端拦截该用户的请求,判断token是否过期,token令牌过期则返回错误的登录信息,这是需要后端再次生成token令牌,此时又会回到步骤1。
注意,session和JWT的主要区别就是保存的位置,session是保存在服务端的,而JWT是保存在客户 端的,JWT就是一个固定格式的字符串
JWT固定各种的字符串,由三部分组成:
注意,把这三部分使用点(.)连接起来,就是一个JWT字符串
header一般的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。 JWT里验证和签名使用的算法列表如下:
JWS | 算法名称 |
---|---|
HS256 | HMAC256 |
HS384 | HMAC384 |
HS512 | HMAC512 |
RS256 | RSA256 |
RS384 | RSA384 |
RS512 | RSA512 |
ES256 | ECDSA256 |
ES384 | ECDSA384 |
ES512 | ECDSA512 |
例如,
{
"typ": "JWT",
"alg": "HS256"
}
payload主要用来包含声明(claims ),这个声明一般是关于实体(通常是用户)和其他数据的声明。 声明有三种类型:
具体如下:
Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
Public claims : 可以随意定义
Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明
例如:
{
"iss": "sxau",
"iat": 1446593502,
"exp": 1446594722,
"aud": "sxau.edu.com",
"sub": "[email protected]",
"username": "admin"
}
注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的
把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号 . 连接在一起(头部在前),形成新的字符串:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y.WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9
最后,将上面拼接完的字符串用HS256算法进行加密,在加密的时候,还需要提供一个密钥(secret)。 加密后的内容也是一个字符串,这个字符串就是签名。
把这个签名拼接在刚才的字符串后面就能得到完整的JWT字符串。 header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分, 服务端也就无法通过。 在JWT中,消息体是透明的,使用签名可以保证消息不被篡改。 例如,使用HMACSHA256加密算法,配合秘钥,将前俩部进行加密,生成签名
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
例如,将Header、Payload、Signature三部分使用点(.)连接起来
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiI2MmI2OWNlZC02YWNlLTRmYzAtOTk5MS00Y WUwMjIxODQ0OTciLCJleHAiOjE2MDYwNTQzNjl9.DNVhr36j66JpQBfcYoo64IRp84dKiQeaq7axHTBcP9 E
例如,使用官网提供的工具,可以对该JWT进行验证和解析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p1WcHh3A-1642675440896)(https://gitee.com/z6135/cloudimage/raw/master/img/image-20220118124647526.png)]
注意,在代码中,我们使用JWT封装的工具类,也可以完成此操作
在springboot中可以很容易的使用JWT,只需要引入相关依赖,封装一个JWT的工具类,并且编写 Controller的拦截器,对指定路径进行拦截验证token即可。
1)新建项目springboot-jwt
2)pom文件中,引入操作jwt相关依赖
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.zmzgroupId>
<artifactId>springboot-jjwtartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>jarpackaging>
<name>springboot-jjwtname>
<description>Demo project for Spring Bootdescription>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.0.3.RELEASEversion>
<relativePath/>
parent>
<properties>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.47version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
3)Controller层
package com.zmz.springbootjjwt.api;
import com.alibaba.fastjson.JSONObject;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.TokenService;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
/**
* @author zhangshengrui
* @date 2022-01-06 20:45
*/
@Controller
@RequestMapping("api")
public class UserApi {
@Autowired
UserService userService;
@Autowired
TokenService tokenService;
@GetMapping("/loginto")
public String loginto(String username,String password ,ModelMap map){
map.addAttribute("name","ceshi");
System.out.println(username+" "+password);
return "login";
}
//登录
@ResponseBody
@PostMapping("/login")
public Object login(@RequestBody User user){
System.out.println(user);
JSONObject jsonObject=new JSONObject();
User userForBase=userService.findByUsername(user);
if(userForBase==null){
jsonObject.put("message","登录失败,用户不存在");
return jsonObject;
}else {
if (!userForBase.getPassword().equals(user.getPassword())){
jsonObject.put("message","登录失败,密码错误");
return jsonObject;
}else {
String token = tokenService.getToken(userForBase);
jsonObject.put("token", token);
jsonObject.put("user", userForBase);
return jsonObject;
}
}
}
@ResponseBody
@UserLoginToken
@GetMapping("/getMessage")
public String getMessage(){
return "你已通过验证";
}
}
package com.zmz.springbootjjwt.interceptor;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.zmz.springbootjjwt.annotation.PassToken;
import com.zmz.springbootjjwt.annotation.UserLoginToken;
import com.zmz.springbootjjwt.entity.User;
import com.zmz.springbootjjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @author zhangshengrui
* @date 2022-01-06 20:41
*/
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserService userService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod)object;
Method method=handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
User user = userService.findUserById(userId);
if (user == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPassword())).build();
try {
jwtVerifier.verify(token);
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
5)全局异常处理GloablExceptionHandler.java
package com.zmz.springbootjjwt.interceptor;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author zhangshengrui
* @date 2022-01-06 22:37
*/
@ControllerAdvice
public class GloablExceptionHandler {
@ResponseBody
@ExceptionHandler(Exception.class)
public Object handleException(Exception e) {
String msg = e.getMessage();
if (msg == null || msg.equals("")) {
msg = "服务器出错";
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("message", msg);
return jsonObject;
}
}
6)javaconfig配置类
package com.zmz.springbootjjwt.config;
import com.zmz.springbootjjwt.interceptor.AuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author zhangshengrui
* @date 2022-01-06 22:33
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**")// 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录
.excludePathPatterns("/loginto","/static/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
7)启动项目,直接访问http://localhost:8888/api/login
8)登录成功拿到Token之后访问http://localhost:8888/api/getMessage
不携带token
注意:这里的key
一定不能错,因为在拦截器中是取关键字token
的值String token = httpServletRequest.getHeader("token");
加上token
之后就可以顺利通过验证和进行接口访问了