说明
更新时间:2020/8/19 11:09,更新了JWT相关内容
本文主要对JWT的学习总结,本文会持续更新,不断地扩充
本文仅为记录学习轨迹,如有侵权,联系删除
一、什么是JWT
JWT全称是JSON Web Token,是目前最流行的跨域认证解决方案,常用于web项目的token校验,用户校验,权限校验等,也可以用于信息的加密传输。
基于session的认证方式
传统的认证方式也就是我们最常用的基于session的认证方式,用户向服务器发送用户名和密码,服务器经过验证后,将数据保存在session里面,并且向用户返回一个sessionid,存入客户端的cookie中,之后的每次访问都会将sessionid通过cookie传给服务器,用来比对服务器存放的session,以此达到用户的身份认证。
基于JWT的认证方式
这种方式简单来说就是,客户端向服务器发送用户名和密码,服务器经过验证之后,生成一个Token令牌,该令牌主要由3部分组成,里面包含用户信息和签名等数据,之后客户端每次发送请求就需要将该Token发送给服务器端,通过校验签名的方式,实现用户数据的认证。
两种方式对比
- session是保存在服务端的,随着用户认证的数量增多,服务器的压力会越来越大,而jwt是保存在客户端的,不会对服务器造成影响。
- 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而jwt不需要。
二、JWT的组成
主要组成又3部分,Header(头部)、Payload(负载)和Signature(签名)由这3部分组成,类似于Header.Payload.Signature这样的形式,里面就包含了需要的用户信息(建议非敏感信息),一个实际的例子如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
(1)Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",//alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)
"typ": "JWT"typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
}
注意:
Header 主要由令牌和所使用的签名算法组成,它会使用Base64编码将Header进行编码后形成一串字符串,组成JWT的头部。
(2)Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除此之外,还可以用来存放自定义的用户信息,比如用户名,用户id等自己需要的信息。不i过一般不建议存放像密码这类敏感信息,容易造成安全问题。
userId:"123",
userName:"灰太狼",
age:"12"
它最后也会经过Base64编码形成一串字符串,组成JWT的负载。
(3)Signature
Signature 部分是对前两部分的签名,防止数据篡改。前面两个部分都是使用Base64进行编码的,前端可以解开知道里面的信息,Signature 需要使用编码后的header和payload以及我们提供的一个密钥,使用header中指定的签名算法进行签名,用来保证JWT没有被篡改过,注意,里面用到的密钥是绝对保密的,只能服务器端这边自己知道,防止信息泄露。
签名的目的:当客户端携带token(JWT)向服务器发送请求时,里面携带由上面说的3部分数据,服务器接收到数据后,先进行签名的校验,即将客户端发送的token的前两个部分(header和payload)用header中指定的签名算法加上密钥进行签名,然后将生成的签名和客户端发送的token第三部分(签名)进行比对,如果一致则签名校验通过,信息没有被修改。
三、JJWT
JJWT全称是Java JWT,适用于Java和Android的JSON Web令牌,JJWT旨在成为最容易使用和理解的库,用于在JVM和Android上创建和验证JSON Web令牌(JWT)。JJWT是纯Java实现,完全基于JWT, JWS,JWE, JWK和JWA RFC规范以及Apache 2.0许可条款下的开源。
相关地址
JWT官网: https://jwt.io/
JJWT github: https://github.com/jwtk/jjwt#features-unsupported
在平时的使用中可以通过JJWT来进行JWT的相关操作。
基本使用
引入依赖
<!--JJWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
这里在网上找了一个工具类JwtUtil
package com.zsc.utils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
@Data
@AllArgsConstructor
public class JwtUtil {
/**
* 秘钥
* - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
* - 长度必须大于128位
*/
@Value("${jwt.secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
private String secret;
/**
* 有效期,单位秒
* - 默认2周
*/
@Value("${jwt.expire-time-in-second:1209600}")
private Long expirationTimeInSecond;
/**
* 从token中获取claim
*
* @param token token
* @return claim
*/
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parser()
.setSigningKey(this.secret.getBytes())
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
log.error("token解析错误", e);
throw new IllegalArgumentException("Token invalided.");
}
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token)
.getExpiration();
}
/**
* 判断token是否过期
*
* @param token token
* @return 已过期返回true,未过期返回false
*/
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 计算token的过期时间
*
* @return 过期时间
*/
private Date getExpirationTime() {
return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
}
/**
* 为指定用户生成token
*
* @param claims 用户信息
* @return token
*/
public String generateToken(Map<String, Object> claims) {
Date createdTime = new Date();
Date expirationTime = this.getExpirationTime();
byte[] keyBytes = secret.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
System.out.println("key="+key.toString());
return Jwts.builder()
.setClaims(claims)//payload
.setIssuedAt(createdTime)
.setExpiration(expirationTime)//过期时间
// 你也可以改用你喜欢的算法
// 支持的算法详见:https://github.com/jwtk/jjwt#features
.signWith(key, SignatureAlgorithm.HS256)//header
.compact();
}
/**
* 判断token是否非法
*
* @param token token
* @return 未过期返回true,否则返回false
*/
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
}
对于该工具类可以用配置文件的方式配置密钥和过期时间,如果不配置则用默认的密钥和过期时间,配置如下
jwt:
secret: aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrs1232134214
# 有效期,单位秒
expire-time-in-second: 60
使用的时候将该类直接注入即可
@Autowired
private JwtUtil jwtUtil;
......
测试案例
package com.zsc.utils;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.awt.desktop.ScreenSleepEvent;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName : JwtUtilTest
* @Description : JWT工具类测试
* @Author : CJH
* @Date: 2020-08-18 17:06
*/
@Slf4j
@SpringBootTest
public class JwtUtilTest {
@Autowired
private JwtUtil jwtUtil;
//生成Token
public String generateTokenTest(){
Map<String,Object> map = new HashMap<>();
map.put("userId",123);
map.put("userName","灰太狼");
map.put("admin",true);
String token = jwtUtil.generateToken(map);
return token;
}
//获取Token
@Test
public void getClaimsFromTokenTest(){
String token = generateTokenTest();
log.info("token=[{}]",token);
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Claims claims = jwtUtil.getClaimsFromToken(token);
Object userId = claims.get("userId");
Object userName = claims.get("userName");
Object admin = claims.get("admin");
log.info("userId = [{}],userName = [{}],admin = [{}]",userId,userName,admin);
}
//获取token过期时间
@Test
public void getExpirationDateFromTokenTest(){
String token = generateTokenTest();
Date expirationDateFromToken = jwtUtil.getExpirationDateFromToken(token);
log.info("token过期时间=[{}]",expirationDateFromToken);
}
//判断token是否非法
@Test
public void isTokenExpiredTest(){
String token = generateTokenTest();
Boolean aBoolean = jwtUtil.validateToken(token);
log.info("token是否非法=[{}]",aBoolean);
}
}
四、登录接口实战
为了规范化,这里将登录接口实战集成进我的api_demo2项目里面,集成的方式跟上面的JJWT里面的一样,先添加依赖,引入工具类,配置号密钥和过期时间。
依赖
<!--JJWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
配置
jwt:
secret: aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrs1232134214
# 有效期,单位秒
expire-time-in-second: 60
引入工具类
package com.zsc.utils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.Map;
@Slf4j
@RequiredArgsConstructor
@SuppressWarnings("WeakerAccess")
@Component
@Data
@AllArgsConstructor
public class JwtUtil {
/**
* 秘钥
* - 默认aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt
* - 长度必须大于128位
*/
@Value("${jwt.secret:aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrsssttt}")
private String secret;
/**
* 有效期,单位秒
* - 默认2周
*/
@Value("${jwt.expire-time-in-second:1209600}")
private Long expirationTimeInSecond;
/**
* 从token中获取claim
*
* @param token token
* @return claim
*/
public Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(this.secret.getBytes())
.parseClaimsJws(token)
.getBody();
// try {
// return Jwts.parser()
// .setSigningKey(this.secret.getBytes())
// .parseClaimsJws(token)
// .getBody();
// } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
// log.error("token解析错误", e);
// throw new IllegalArgumentException("Token invalided.");
// }
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token)
.getExpiration();
}
/**
* 判断token是否过期
*
* @param token token
* @return 已过期返回true,未过期返回false
*/
private Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 计算token的过期时间
*
* @return 过期时间
*/
private Date getExpirationTime() {
return new Date(System.currentTimeMillis() + this.expirationTimeInSecond * 1000);
}
/**
* 为指定用户生成token
*
* @param claims 用户信息
* @return token
*/
public String generateToken(Map<String, Object> claims) {
Date createdTime = new Date();
Date expirationTime = this.getExpirationTime();
byte[] keyBytes = secret.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
System.out.println("key=" + key.toString());
return Jwts.builder()
.setClaims(claims)//payload
.setIssuedAt(createdTime)
.setExpiration(expirationTime)//过期时间
// 你也可以改用你喜欢的算法
// 支持的算法详见:https://github.com/jwtk/jjwt#features
.signWith(key, SignatureAlgorithm.HS256)//header
.compact();
}
/**
* 判断token是否非法
*
* @param token token
* @return 未过期返回true,否则返回false
*/
public Boolean validateToken(String token) {
return !isTokenExpired(token);
}
}
创建JWT的拦截器JwtInterceptor
package com.zsc.interceptor;
import com.zsc.enums.ResultCode;
import com.zsc.exception.BusinessException;
import com.zsc.utils.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName : JwtInterceptor
* @Description : Jwt拦截器
* @Author : CJH
* @Date: 2020-08-18 22:01
*/
@Slf4j
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 注意,token一般存放在请求头中,所以发起请求时,请求头必须携带有token,字段名为token
*/
String token = request.getHeader("token");
try{
Boolean tokenResult= jwtUtil.validateToken(token);
if (tokenResult){
return true;
}else {
log.error("token无效");
throw new BusinessException(ResultCode.TOKEN_INVALID);
}
}catch (ExpiredJwtException e){
log.error("token异常:[{}]",e.getMessage());
throw new BusinessException(ResultCode.TOKEN_EXPIRED);
}catch (MalformedJwtException e){
log.error("token异常:[{}]",e.getMessage());
throw new BusinessException(ResultCode.TOKEN_FORMAT_ERROR);
}catch (UnsupportedJwtException e){
log.error("token异常:[{}]",e.getMessage());
throw new BusinessException(3011,e.getMessage());
}catch (IllegalArgumentException e){
log.error("token异常:[{}]",e.getMessage());
throw new BusinessException(ResultCode.TOKEN_IS_EMPTY);
}
}
}
在自定义的web配置类中添加拦截器
package com.zsc.config;
import com.zsc.interceptor.JwtInterceptor;
import com.zsc.interceptor.RateLimitInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* @ClassName : WebConfig
* @Description : web配置类
* @Author : CJH
* @Date: 2020-08-17 21:35
*/
@Configuration
@EnableWebMvc
@Slf4j
public class WebConfig implements WebMvcConfigurer {
/**
* 全局限流拦截器
*/
@Resource
private RateLimitInterceptor rateLimitInterceptor;
/**
* jwt拦截器
*/
@Autowired
private JwtInterceptor jwtInterceptor;
/**
* 向web中添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//限流拦截器
registry.addInterceptor(rateLimitInterceptor)
.addPathPatterns("/api/**");
//JWT拦截器
registry.addInterceptor(jwtInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns("/api/users/**");
}
/**
* 静态资源配置
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
/**
* 资源映射到本地目录
*/
registry.addResourceHandler("/uploads/**")
.addResourceLocations("file:F:\\Git\\GitRepository\\java\\java_springboot\\api_demo2\\uploads\\");
}
}
登录接口
@PostMapping("/login")
public ResponseResult login(@RequestBody UserQueryDto userQueryDto){
//构成查询参数
PageQuery<UserQueryDto> userDtoPageQuery = new PageQuery<>();
userDtoPageQuery.setPageIndex(1);
userDtoPageQuery.setPageSize(10);
userDtoPageQuery.setQuery(userQueryDto);
//查询
PageResult<List<UserDto>> result = userServer.query(userDtoPageQuery);
if(!result.getData().isEmpty()){
UserDto userDto = result.getData().get(0);
Map<String,Object> map = new HashMap<>();
map.put("name",userDto.getName());
map.put("age",userDto.getAge());
map.put("email",userDto.getEmail());
map.put("phone",userDto.getPhone());
UserTokenVo userTokenVo = new UserTokenVo(jwtUtil.generateToken(map));
return ResponseResult.success(userTokenVo);
}else{
return ResponseResult.failure(ResultCode.USER_OR_PASSWORD_ERROR);
}
}
到此集成完成,开始测试,为了测试方便,这里新建了一个测试的控制器
package com.zsc.controller;
import com.zsc.common.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName : TestController
* @Description : 用于测试的控制器
* @Author : CJH
* @Date: 2020-08-18 22:43
*/
@RestController
@Slf4j
@Validated//开启校验支持
@RequestMapping("/api/test")
public class TestController {
//测试token接口1
@GetMapping("/test01")
public ResponseResult<String> test01(String str){
return ResponseResult.success(str);
}
}
由于拦截器配置了拦截所有除了"/api/usrs/**"的所有路径,所以,在请求test01接口的时候需要带上token,不然会请求失败,当然上面也做了相应的错误处理
测试登录接口,返回对应的tonken
再次请求test01,带上登录返回的token
到此JWT集成完成,之后开发如果某些接口需要token保护的可以将其路径添加到拦截器里面,以达到认证的目的。