📖 官方文档
🌐官方文档
JWT.IO
💰JWT概述
JSON Web Token 是一个开放标准(RFC 7519),它定义了一种紧凑且包含的方式,用于在各方之间安全地传输信息作为JSON对象。由于此信息是经过数字签名的,用于在各方面之间安全地传输信息作为JSON对象。由于此信息是经过数字签名地,因此可以被验证和信任。可以使用秘密(或者HMAC 算法)或使用RSA 或ECDSA 的公钥/私钥对对JWT进行签名。
尽管可以对JWT进行加密以提供双方之间的保密性,但我们将重点关注已签名的令牌。签名的令牌可以验证其中包含的声明的完整性,而加密的令牌则将这些声明隐藏在其他方的面前。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。
🔱应用场景
授权认证:使用JWT的最常见方案。一旦用户登录,每个后续请求将包括令牌,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
信息交换:JWT是在各方之间安全地传输信息的一种好方法。因为可以对JWT进行签名,例如使用公钥/私钥对。此外。由于签名是使用标头和有效负载计算的,因此还可以验证内容是否遭到篡改。
🚀 认证流程
⛔️session认证
传统方式:用户第一次请求登录时候设置session,此后每次访问携带cookie。
问题:
每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着用户的增多,服务端的开销会明显增大。
用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力,这也意味着限制了应用的扩展能力。
因为是基于cookie来进行用户识别的,cookie如何被截获,用户很容易受到跨站请求伪造的攻击。
🔰JWT认证
认证流程:
首先,前端通过web表单将自己的用户名和密码发送到后端的接口,这一过程一般是一个Http POST请求。
后端核对用户名和密码成功后,将用户的id等其他信息作为JWT payload(负载),将其与头部分别进行Based64编码拼接后签名,形成一个JWT。形成的JWT就形同xxx.yyy.zzz的字符串。
后端将JWT字符串作为登陆成功的返回结果返回给前端,前端可以将返回的结果保存在localStorage上,退出登录时前端删除保存的JWT即可。
前端在每次请求时将JWT放入HTTP Header的Authorization位。(解决XSS和XSRF问题)
后端检查是否存在,如存在验证JWT的有效性。(签名是否正确,Token是否过期等)
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,并返回相应结果。
优势:
简洁,可以通过URL,POST参数或者在HTTP Header发送,因为数据量小,传输速度也很快
自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库
因为Token是JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
不需要再服务端保存会话信息,特别适合用于分布式微服务
⛓令牌结构
JSON Web Token以紧凑的形式由三部分组成,三部分由.
分隔,即xxxxx.yyyyy.zzzzz
标头(Header) : Base64编码,由令牌类型和签名算法组成
1 2 3 4 { "alg" : "HS256" , "typ" : "JWT" }
负载(Payload) : Base64编码,用于放置携带信息,不能放敏感信息
1 2 3 4 5 { "name" : "Khighness" "admin" : true "gender" : "male" }
签名(Signature) : 对标头和负载进行签名,防止内容被篡改
1 HMACSHA256(baseUrlEncode(header)) + "." + base64UrlEncode(payload).secret)
💻 JWT-Demo
➕添加依赖
1 2 3 4 5 6 <dependency > <groupId > com.auth0</groupId > <artifactId > java-jwt</artifactId > <version > ${jwt.version}</version > </dependency >
📑Java代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test void generateToken () { HashMap<String, Object> map = new HashMap <>(); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND, 60 ); String token = JWT.create() .withHeader(map) .withClaim("userID" , 1011 ) .withClaim("username" , "KHighness" ) .withExpiresAt(calendar.getTime()) .sign(Algorithm.HMAC256("PARAK" )); System.out.println(token); } @Test void verifyToken () { JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512("PARAK" )).build(); String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MDU5NDU0ODQsInVzZXJJRCI6MTAxMSwidXNlcm5hbWUiOiJLSGlnaG5lc3MifQ.Gvwa3vu_LYogcEPFxKOgFgaH6WnTKoo-UDW977W1GAw" ; DecodedJWT verify = jwtVerifier.verify(token); System.out.println(verify.getClaim("userID" ).asInt()); System.out.println(verify.getClaim("username" ).asString()); }
❗️ 常见异常
异常
描述
TokenExpiredException
令牌过期异常
SignatureVerificationException
签名不一致异常
AlgorithmMismatchException
加密算法不匹配异常
InvalidClaimException
失效的负载异常
🍃 整合Springboot
💬说明
用户在第一次登陆的时候,后台生成token返回给前端存储在sessionStorage中,此后每次前端需要调用后端需要认证的接口时都把token取出来携带在http header中。后台设置拦截器,设置需要认证才能访问的接口,每次处理请求时,先从request的http header中取出token进行认证,通过后才进行相关接口处理。
🔧封装工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import com.auth0.jwt.JWT;import com.auth0.jwt.JWTCreator;import com.auth0.jwt.algorithms.Algorithm;import com.auth0.jwt.interfaces.DecodedJWT;import org.springframework.stereotype.Component;import java.util.Calendar;import java.util.Map;public class KKJWTUtil { private static final String secret = "@KHIGHNESS" ; private static final int accessTokenExpireTime = 604800 ; private static final String issuer = "parak.top" ; public static String generateToken (Map<String, String> chaims) { JWTCreator.Builder builder = JWT.create(); chaims.forEach((k, v) -> { builder.withClaim(k, v); }); Calendar calendar = Calendar.getInstance(); calendar.add(Calendar.SECOND, accessTokenExpireTime); builder.withExpiresAt(calendar.getTime()); builder.withIssuer(issuer); String token = builder.sign(Algorithm.HMAC256(secret)); return token; } public static DecodedJWT verifyToken (String token) { return JWT.require(Algorithm.HMAC256(secret)).build().verify(token); } }
用户控制器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.auth0.jwt.interfaces.DecodedJWT;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.ObjectUtils;import org.springframework.web.bind.annotation.*;import top.parak.common.KKCondition;import top.parak.common.KKDataResponse;import top.parak.common.KKJWTUtil;import top.parak.entity.User;import top.parak.service.UserService;import javax.servlet.http.HttpServletRequest;import javax.validation.Valid;import java.util.HashMap;import java.util.Map;@Slf4j @CrossOrigin @RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; @PostMapping("/register") public KKDataResponse register (@Valid @RequestBody User user) { String username = user.getUsername(); String password = user.getPassword(); log.info("用户注册 => [{}]" , username); KKCondition condition = new KKCondition (); condition.setName(username); if (!ObjectUtils.isEmpty(userService.queryUserInCondition(condition))) { log.warn("用户名{}已被注册,不可重复注册" , username); return KKDataResponse.errorResponse("该用户名已被注册,不可重复注册" ); } return userService.saveUser(username, password) == 1 ? KKDataResponse.successResponse(true ) : KKDataResponse.errorResponse(false ); } @PostMapping("/login") public KKDataResponse login (@RequestBody String loginDataJson) { JSONObject loginData = JSON.parseObject(loginDataJson); String username = loginData.get("username" ).toString(); String password = loginData.get("password" ).toString(); log.info("用户登录 => [{}]" , username); Map<String, Object> response = new HashMap <>(); if (userService.authenticate(username, password)) { response.put("loginState" , true ); Map<String, String> payload = new HashMap <>(); payload.put("username" , username); String token = KKJWTUtil.generateToken(payload); response.put("kktoken" , token); return KKDataResponse.successResponse(response); } else { response.put("loginState" , false ); response.put("kktoken" , null ); return KKDataResponse.errorResponse(response); } } @GetMapping("/test") public KKDataResponse test (HttpServletRequest request) { String kktoken = (String) request.getHeader("Authorization" ); DecodedJWT verify = KKJWTUtil.verifyToken(kktoken); String username = verify.getClaim("username" ).asString(); log.info("请求用户 => [{}]" , username); return KKDataResponse.successResponse("请求成功" ); } }
设置拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import com.auth0.jwt.exceptions.AlgorithmMismatchException;import com.auth0.jwt.exceptions.InvalidClaimException;import com.auth0.jwt.exceptions.SignatureVerificationException;import com.auth0.jwt.exceptions.TokenExpiredException;import com.auth0.jwt.interfaces.DecodedJWT;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.web.servlet.HandlerInterceptor;import top.parak.common.JwtTokenUtil;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.util.HashMap;import java.util.Map;@Slf4j public class JWTInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String kktoken = request.getHeader("Authorization" ); Map<String, Object> map = new HashMap <>(); try { log.info("请求令牌 => [{}]" , kktoken); DecodedJWT decodedJWT = KKJWTUtil.verifyToken(kktoken); log.info("验证结果 => [{}]" , true ); return true ; } catch (TokenExpiredException e) { log.error("发成异常 => [{}]" , e.getMessage()); map.put("msg" , "令牌过期" ); } catch (SignatureVerificationException e) { log.error("发成异常 => [{}]" , e.getMessage()); map.put("msg" , "签名错误" ); } catch (AlgorithmMismatchException e) { log.error("发成异常 => [{}]" , e.getMessage()); map.put("msg" , "加密算法不匹配" ); } catch (InvalidClaimException e) { log.error("发成异常 => [{}]" , e.getMessage()); map.put("msg" , "失效负载" ); } catch (NullPointerException e) { log.error("发成异常 => [{}]" , e.getMessage()); map.put("msg" , "令牌为空" ); } catch (Exception e) { log.error("发成异常 => [{}]" , e.getMessage()); map.put("msg" , e.getMessage()); } log.error("验证结果 => [{}]" , false ); map.put("state" , false ); String json = new ObjectMapper ().writeValueAsString(map); response.setContentType("application/json;charset=UTF-8" ); PrintWriter writer = response.getWriter(); writer.println(json); writer.close(); return false ; } }
拦截器配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;import top.parak.intercepter.JWTIntercepter;@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(new JWTInterceptor ()) .addPathPatterns("/api/**/**" ) .excludePathPatterns("/api/user/login" ); } }