jwt介绍
token认证
随着 Restful API、微服务的兴起,基于 Token 的认证现在已经越来越普遍。基于token的用户认证是一种服务端无状态的认证方式,所谓服务端无状态指的token本身包含登录用户所有的相关数据,而客户端在认证后的每次请求都会携带token,因此服务器端无需存放token数据。
当用户认证后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage 等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
市面上基于token的认证方式大都采用的是JWT(Json Web Token)。
JWT
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。
JWT令牌结构:
JWT令牌由Header、Payload、Signature三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header
头部包括令牌的类型(即JWT)以及使用的哈希算法(如如HMAC、SHA256或RSA),定义第三部分签名使用的算法。使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
{
"alg": "HS256",
"typ": "JWT"
}
Payload
第二部分是负载,内容 是一个json对象,它是存放有效信息的地方,可以存放自定义字段信息,一般用户的非隐私信息,此部分可以解码还原原始内容。 。内置字段信息:jti唯一标识 iat创建时间 exp过期时间 sub面向对象。将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分
{
"sub": "1234567890",
"name": "456",
"admin": true
}
Signature
签名:用于防止jwt内容被篡改
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
base64UrlEncode(header):jwt令牌的第一部分。base64UrlEncode(payload):jwt令牌的第二部分。 secret:签名所使用的密钥。
在idea中使用token
导入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
单元测试
/**
* jwt生成与解析介绍
*/
public class JWTTest {
/**
* 生成JWT(一般在登录成功后将生成的JWT当做TOKEN来使用)
*/
@Test
public void testCreateJwt(){
//eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjZTAyNzQ5NC1hZjc4LTQ1MTMtODY2Ni0xYjQxMTNiZjc4ZWQiLCJzdWIiOiJhbGwiLCJ1c2VybmFtZSI6InpoYW5nc2FuIiwidXNlcklkIjoxMjMsImlhdCI6MTYxNDE0OTg3NSwiZXhwIjoxNjE0MTUzNDc1fQ.AxKYDz5vHg_EnGxm8Z9zH6oDpSKQZBjDiiEsNt_aec8
String secret = "itcast"; //对称加密密钥
String token = Jwts.builder().signWith(SignatureAlgorithm.HS256, secret)
.setId(UUID.randomUUID().toString()) //jwt内置属性jti,jwt的唯一标识
.setSubject("all") //jwt内置属性sub,面向用户
.claim("username", "zhangsan") //自定义属性username
.claim("userId",123) //自定义属性userId
.setIssuedAt(new Date())//jwt内置属性iat,创建时间
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000)) //1小时过期,过期时间
.compact();
System.out.println(token);
}
/**
* 解析JWT(一般登陆后访问系统时需要携带TOKEN字符串,系统会使用JWT工具类校验TOKEN字符串)
* jwt解析失败:
* 1)jwt被篡改了 抛出异常SignatureException
* 2)jwt过期了 抛出异常ExpiredJwtException
*/
@Test
public void testParseJwt(){
String secret = "itcast"; //对称加密密钥
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjZTAyNzQ5NC1hZjc4LTQ1MTMtODY2Ni0xYjQxMTNiZjc4ZWQiLCJzdWIiOiJhbGwiLCJ1c2VybmFtZSI6InpoYW5nc2FuIiwidXNlcklkIjoxMjMsImlhdCI6MTYxNDE0OTg3NSwiZXhwIjoxNjE0MTUzNDc1fQ.AxKYDz5vHg_EnGxm8Z9zH6oDpSKQZBjDiiEsNt_aec8";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
System.out.println("头:" + claimsJws.getHeader());
System.out.println("载荷:" + claimsJws.getBody());
System.out.println("userId:" + claimsJws.getBody().get("userId"));
System.out.println("签名:" + claimsJws.getSignature());
}
}
工具类
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class AppJwtUtil {
// TOKEN的有效期一天(S)
private static final int TOKEN_TIME_OUT = 3_600;
// 加密KEY
private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
// 最小刷新间隔(S)
private static final int REFRESH_TIME = 300;
// 生产ID
public static String getToken(Long id){
Map<String, Object> claimMaps = new HashMap<>();
claimMaps.put("id",id);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString())
.setIssuedAt(new Date(currentTime)) //签发时间
.setSubject("system") //说明
.setIssuer("heima") //签发者信息
.setAudience("app") //接收用户
.compressWith(CompressionCodecs.GZIP) //数据压缩方式
.signWith(SignatureAlgorithm.HS512, generalKey()) //加密方式
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
.addClaims(claimMaps) //cla信息
.compact();
}
/**
* 获取token中的claims信息
*
* @param token
* @return
*/
private static Jws<Claims> getJws(String token) {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(token);
}
/**
* 获取payload body信息
*
* @param token
* @return
*/
public static Claims getClaimsBody(String token) {
try {
return getJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
}
/**
* 获取hearder body信息
*
* @param token
* @return
*/
public static JwsHeader getHeaderBody(String token) {
return getJws(token).getHeader();
}
/**
* 是否过期
*
* @param claims
* @return -1:有效,0:有效,1:过期,2:过期
*/
public static int verifyToken(Claims claims) {
if(claims==null){
return 1;
}
try {
claims.getExpiration()
.before(new Date());
// 需要自动刷新TOKEN
if((claims.getExpiration().getTime()-System.currentTimeMillis())>REFRESH_TIME*1000){
return -1;
}else {
return 0;
}
} catch (ExpiredJwtException ex) {
return 1;
}catch (Exception e){
return 2;
}
}
/**
* 由字符串生成加密key
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
public static void main(String[] args) {
/* Map map = new HashMap();
map.put("id","11");*/
System.out.println(AppJwtUtil.getToken(1102L));
Jws<Claims> jws = AppJwtUtil.getJws("eyJhbGciOiJIUzUxMiIsInppcCI6IkdaSVAifQ.H4sIAAAAAAAAADWLQQqEMAwA_5KzhURNt_qb1KZYQSi0wi6Lf9942NsMw3zh6AVW2DYmDGl2WabkZgreCaM6VXzhFBfJMcMARTqsxIG9Z888QLui3e3Tup5Pb81013KKmVzJTGo11nf9n8v4nMUaEY73DzTabjmDAAAA.4SuqQ42IGqCgBai6qd4RaVpVxTlZIWC826QA9kLvt9d-yVUw82gU47HDaSfOzgAcloZedYNNpUcd18Ne8vvjQA");
Claims claims = jws.getBody();
System.out.println(claims.get("id"));
}
}
全局过滤器中实现jwt校验
统一认证Token处理逻辑
1.获取请求和响应实例。2.如果路径是登录,则放行。3.如果路径非登录获取请求头token。4.如果token为空,响应401。5.如果token解析过期,响应401。6.成功放行。7.如果解析Token异常,响应401
import com.heima.utils.common.AppJwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class AuthorizeFilter implements Ordered, GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1.获取request和response对象
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
//2.判断是否是登录接口,是直接放行
String path = request.getURI().getPath();
if(path.equals("/user/api/v1/login/login_auth/")){
return chain.filter(exchange);
}
//3.非登录接口,获取token
String token = request.getHeaders().getFirst("token");
//4.判断token是否存在,不存在返回401(无权访问)
if(StringUtils.isBlank(token)){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete(); //请求被拒绝
}
//5.判断token是否有效,无效返回401(无权访问)
try {
Claims claimsBody = AppJwtUtil.getClaimsBody(token);
int result = AppJwtUtil.verifyToken(claimsBody);
if(result==0){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
} catch (Exception e) {
e.printStackTrace();
//篡改token,无效返回401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
//6.放行
return chain.filter(exchange);
}
/**
* 优先级设置 值越小 优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}