一、什么是JWT ?
现在很多企业基本都是基于前后端分离进行开发,因此在这种情况下,后端只需提供API接口,那么就需要一种机制来做校验,于是就有了JWT。JWT全称 “Json web token”,特别适用于分布式站点的单点登录。
1、JWT的组成
JWT由三个部分组成,分别是:
1.标头(Header)
2.有效荷载(Payload)
3.签名(Signature)
因此, JWT通常如下所示:xxxxx.yyyyy.zzzzz 三部分(Header.Payload.Signature)
{ }.{ }.{ } 三串JSON
2、Header 标头
标头通常由两部分组成: 令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用Base64 编码组成 JWT的第一部分。
注意:Base64是一种编码 ,也就是说。它是可以被翻译回原来的样子。它并不是一种加密过程。
例如:
{
"alg" : "HS256", //算法可以有多种选择
"typ" : "JWT" //这个是唯一的
}
3、.Payload (主要包含一些自己想包含的信息)
令牌的第二部分是有效负载,其中包含声明.声明是有关实体 (通常是用户信息,但是前端能解码,所以最好不要包含敏感信息:用户的密码等)和其他数据的声明,同样的,它会使用Base64 编码组成JWT结构的第二部分。
例如:
{
"sub":"123456",
"name": "John Doe",
"admin" :true
}
4、Signature 签名
前面两部分都是使用Base64进行编码的,即前端可以解码知道里面的信息。Signature需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用header中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。
如:
如:
HMACSHA256(base64UrlEncode(header) +"."+ base64UrlEncode(payload),secret);
签名的目的:
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被篡改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载行程的签名和JWT附带上的签名是不一样的,如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的.(所以密钥secret千万不能告诉别人)
JWT的三个部分总结如下:
放在一起
所组成的字符串如下所示 . 可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比。它更紧凑。
-简洁
可以通过URL,POST参数或者在HTTP header发送.因为数据量小,传输速度快
-自包含(Self-contained)
负载中包含了所有用户所需要的信息,避免了多次查询数据库
二、在JAVA中使用JWT
在springboot项目中导入以下坐标:
<!-- java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
1、生成JWT
class SpringbootJwtApplicationTests {
@Test
void contextLoads() {
Map<String, Object> map = new HashMap<>();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,20);
//头部可要可不要,因为会有默认值,JWT的三部分如下
String token = JWT.create().withHeader(map)//hearder设置头部 签名算法和type
.withClaim("userId", 23) //payload
.withClaim("username", "yangdan")
.withClaim("sex", "女")
.withExpiresAt(instance.getTime()) //指定;令牌的过期时间 20秒之后过期
.sign(Algorithm.HMAC256("!@dsa&&"));//签名 算法和密钥
System.out.println(token);
}
}
2、校验JWT
我们可以拿到jwt负载中声明的数据
/*令牌的验证*/
@Test
void authToken(){
/*创建验证对象 注意:算法和密钥要去上面生产Token时保持一致*/
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!@dsa&&")).build();
/*验证刚刚的token 然后拿到一个解密后的decodedJWT*/
/*注意:如果JWT不正确,那么这下面这一步校验的过程将会报错*/
DecodedJWT decodedJWT = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzZXgiOiLlpbMiLCJleHAiOjE2MjI0NjY5ODgsInVzZXJJZCI6MjMsInVzZXJuYW1lIjoieWFuZ2RhbiJ9.a_IYS7HK0LLDRddq8jsbNrqC2eHYAd8LFCFdGKp-6Sc");
Integer userId = decodedJWT.getClaim("userId").asInt(); //拿到负载中声明数据值
String username = decodedJWT.getClaim("username").asString();
System.out.println(userId); //23
System.out.println(username); //yangdan
}
3、JWT工具类的封装
为了在项目中更快捷的生成和校验JWT,我们需要封装一个JWT的工具类
public class JWTUtils {
private static final String secret="token!@18056112120";
/*生成token*/
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7); //设置7天过期
//创建JWT builder
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k,v)->{
builder.withClaim(k,v);
});
//sign
builder.withExpiresAt(instance.getTime());
String token = builder.sign(Algorithm.HMAC256(secret));
return token;
}
/*验证token*/
public static DecodedJWT verify(String token){
//创建JWT验证对象 验证token
return JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
}
}
三、基于JWT的认证
四、SpringBoot整合JWT
创建springboot项目导入以下坐标:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
项目具体结构如下:
编写用来统一返回的javabean
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JSONResult {
private Integer status;
private String msg;
private Object data;
public static JSONResult ok(){
JSONResult result = new JSONResult();
result.setStatus(200);
result.setMsg("ok");
return result;
}
public static JSONResult ok(Object object){
JSONResult result = new JSONResult();
result.setStatus(200);
result.setMsg("ok");
result.setData(object);
return result;
}
public static JSONResult error(String msg){
JSONResult result = new JSONResult();
result.setStatus(500);
result.setMsg(msg);
return result;
}
public static JSONResult build(Integer status,String msg,Object data){
return new JSONResult(status,msg,data);
}
}
Controller中编写登录的接口和用来测试的接口:
如果登录用户名和密码都正确,那么像前端返回JWT
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
@GetMapping("/login")
public JSONResult login(User user, HttpServletResponse response){
log.info("接收到的信息:"+user);
if (StringUtils.isBlank(user.getUsername())||StringUtils.isBlank(user.getPassword())){
return JSONResult.error("参数传递错误");
}
if ("yangzihao".equals(user.getUsername())&&"18056112120".equals(user.getPassword())){
/*生成JWT*/
Map<String, String> map = new HashMap<>();
map.put("username",user.getUsername());
map.put("password",user.getPassword());
String token = JWTUtils.getToken(map);
/*认证成功返回token*/
return JSONResult.ok(token);
}else {
return JSONResult.error("认证失败");
}
}
/*后序如果想调用其他接口*/
@GetMapping("/getOk")
public JSONResult test(){
return JSONResult.ok();
}
}
之后再访问其它端口我们需要使用拦截器进行请求的拦截,校验请求头中的JWT(前端一般将登录成功后拿到的JWT存入本地,然后每次发送请求的时候在请求头中带上JWT)
拦截器:
逻辑:拿到请求头中的JWT进行校验,前面说过,如果JWT错误会报错,那么报错向前端返回对应的错误信息。如果正确,即return true;放行。jwt一共有一下五种异常类型:
/*拦截器 用来判断用户是否授权*/
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
JSONResult jsonResult=null;
try {
//拿到请求头中的jwt
String token = request.getHeader("auth");
JWTUtils.verify(token);//验证令牌;
return true;
} catch (SignatureVerificationException e) {
e.printStackTrace();
jsonResult=JSONResult.error("无效签名");
} catch (TokenExpiredException e){
e.printStackTrace();
jsonResult=JSONResult.error("token过期");
} catch (AlgorithmMismatchException e){
e.printStackTrace();
jsonResult=JSONResult.error("token算法不一致");
} catch (Exception e){
e.printStackTrace();
jsonResult = JSONResult.error("token无效");
}
/*转为json数据*/
String json = new ObjectMapper().writeValueAsString(jsonResult);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
return false;
}
}
在mvc配置类中配置拦截器
@Configuration
public class MyMVCConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
/*除登录接口以外其余的都进行拦截*/
registry.addInterceptor(new MyInterceptor()).excludePathPatterns("/user/login")
.addPathPatterns("/**");
}
}
五、测试
未登录状况下请求测试接口结果如下:
请求登录拿到JWT:
在请求头中携带JWT,请求测试接口:
注意
就算客户端服务器重启了,只要token没过期,token始终是有效的,它不像session,如果服务器重启了session就会失效。这就是使用JWT的好处!!!