JWT原理与应用
|总字数:2.3k|阅读时长:10分钟|浏览量:|
出现背景
基于 session 认证所显露的问题:
- Session:每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言 session 都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
 
- 扩展性:用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
 
- CSRF:因为是基于 cookie 来进行用户识别的,cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击。
 
| 对比项 | 
JWT | 
Session | 
| 存储位置 | 
客户端(Token) | 
服务器(Session) | 
| 状态管理 | 
无状态 | 
有状态 | 
| 安全性 | 
需要加密+签名 | 
依赖服务器安全 | 
| 适用于 | 
分布式系统、微服务 | 
传统 Web 应用 | 
基本概念
什么是 JWT?
JWT(JSON Web Token)是一种用于身份认证和信息传递的轻量级安全令牌标准,基于 JSON 格式,它常用于用户身份验证、授权以及 信息传递,尤其在单点登录(SSO)和 前后端分离架构下被广泛使用。
作用:只需要服务端生成 token,客户端保存这个 token,每次请求携带这个 token,服务端认证解析即可。简单便捷,无需通过 Redis 缓存,而是直接根据 token 取出保存的用户信息,以及对 token 可用性校验
优缺点
优点
- 占资源少:不在服务端保存信息
 
- 扩展性好:分布式中,Session 需要做多机数据共享,通常存在数据库或者 Redis 中,而 JWT 不需要
 
- 跨语言:Token 是以 JSON 加密的形式保存在客户端的,所以 JWT 是跨语言的,原则上任何 web 形式都支持
 
缺点
- 无法废弃已颁布的令牌:一旦签发一个 JWT,在到期之前就会始终有效,无法中途废弃。例如:在 payload 中存储了一些信息,当信息需要更新时,则重新签发一个 JWT,但由于旧的 JWT 还没过期,拿着这些旧的 JWT 依旧可以登录
 
解决方法:服务端部署额外的逻辑,例如:设置黑名单,一旦签发了新的 JWT,旧的就加入黑名单(如存在 Redis 里面),避免被再次使用(违背 JWT 初衷)
- 过期需要重新生成 JWT:Cookie 续签方案一般都是框架自带的,如:Session 有效期 30 分钟,若 30 分钟内有访问,有效期刷新至 30 分钟。对于 JWT,改变 JWT 的有效时间,就要签发新的 JWT
 
解决方法:
- 每次请求刷新 JWT,即每个 HTTP 请求都返回一个新的 JWT,这个方法每次请求都要做 JWT 的加密解密,会带来性能问题
 
- 在 Redis 中单独为每个 JWT 设置过期时间,每次访问时刷新 JWT 的过期时间,引入 Redis 后就把无状态的 JWT 变成了有状态(违背了 JWT 的初衷)
 
JWT 的构成
一个 token 分 3 部分,按顺序为:头部(header),载荷(payload),签证(signature),每部分使用 . 进行分隔:Header.Payload.Signature
JWT 的头部承载两部分信息:
- 
alg:签名算法(如 HS256、RS256)
 
- 
typ:令牌类型(JWT)
 
1 2 3 4
   | {   "alg": "HS256",   "typ": "JWT" }
  | 
 
playload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
 
- iss(issuer):签发人
 
- exp(expiration time):过期时间
 
- sub(subject):主题
 
- aud(audience):接收 JWT 的一方
 
- nbf(Not Before):生效时间
 
- iat (Issued At):签发时间
 
- jti(JWT ID):JWT 的唯一标识
 
- 
公共的声明:如 username、role 等。
 
- 
私有的声明,应用程序自定义字段。
 
1 2 3 4 5
   | {   "sub": "1234567890",   "name": "John Doe",   "iat": 1516239022 }
  | 
 
signature
JWT 的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64 后的)
 
- payload (base64 后的)
 
- secret
 
1 2 3 4 5
   | HMACSHA256(   base64UrlEncode(header) + "." +   base64UrlEncode(payload), 	your-256-bit-secret )
   | 
 
基本使用
jjwt
1 2 3 4 5
   | <dependency>     <groupId>io.jsonwebtoken</groupId>     <artifactId>jjwt</artifactId>     <version>0.9.1</version> </dependency>
   | 
 
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
   | @SpringBootTest class Jwt01ApplicationTests {
      private long time=1000*60*60*24;     private String signature="admin";          @Test     void contextLoads() {         JwtBuilder jwtBuilder= Jwts.builder();         String jwtToken = jwtBuilder                                  .setHeaderParam("typ", "JWT")                 .setHeaderParam("alg","HS256")                                  .claim("username","tom")                 .claim("role","admin")                 .setSubject("admin-text")                 .setExpiration(new Date(System.currentTimeMillis()+time))                 .setId(UUID.randomUUID().toString())                                  .signWith(SignatureAlgorithm.HS256,signature)                 .compact();
          System.out.println(jwtToken);     }
      @Test     public void parse(){         String token="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRvbSIsInJvbGUiOiJhZG1pbiIsInN1YiI6ImFkbWluLXRleHQiLCJleHAiOjE2MzY0NjM4OTcsImp0aSI6Ijg5OTkzZGM1LWE5OGQtNDhkZS1hMDg0LTgzMWNlNjcxMGJmZCJ9.hHFAbj6d9e0_y1JXtf2q3ZBbO1M3iIbO_s0v6YqUPeM";         JwtParser parser = Jwts.parser();         Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(token);         Claims body = claimsJws.getBody();         System.out.println(body.get("username"));         System.out.println(body.get("role"));         System.out.println(body.getId());         System.out.println(body.getSubject());         System.out.println(body.getExpiration());     } }
   | 
 
java-jwt
1 2 3 4 5
   | <dependency>     <groupId>com.auth0</groupId>     <artifactId>java-jwt</artifactId>     <version>3.18.2</version> </dependency>
   | 
 
- JWTUtils 工具类
 
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
   | public class JWTUtils {     private static final String SING="!abcd";
      
 
      public static String getToken(Map<String,String> map){         Calendar instance = Calendar.getInstance();         instance.add(Calendar.DATE,7);         JWTCreator.Builder builder = JWT.create();                  map.forEach((k,v)->{             builder.withClaim(k,v);         });         String token = builder.withExpiresAt(instance.getTime())                 .sign(Algorithm.HMAC256(SING));         return token;     }
      
 
      public static DecodedJWT verify(String token){         return JWT.require(Algorithm.HMAC256(SING)).build().verify(token);     }
     
 
     public static DecodedJWT getTokenInfo(String token){        DecodedJWT verify = JWT.require(Algorithm.HMAC256(SING)).build().verify(token);        return verify;    } }
  | 
 
- 拦截器
 
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
   | public class JWTInterceptor implements HandlerInterceptor {          @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         String token = request.getHeader("token");         HashMap<String, Object> map = new HashMap<>();         try {             DecodedJWT verify = JWTUtils.verify(token);             return true;         }catch (SignatureVerificationException e){             e.printStackTrace();             map.put("msg","无效签名");         }catch (TokenExpiredException e){             e.printStackTrace();             map.put("msg","token过期");         }catch (AlgorithmMismatchException e){             e.printStackTrace();             map.put("msg","算法不一致");         }catch (Exception e){             e.printStackTrace();             map.put("msg","token无效");         }         map.put("state",false);                  String json = new ObjectMapper().writeValueAsString(map);         response.setContentType("application/json;charset=utf-8");         response.getWriter().print(json);         return false;     } }
  | 
 
1 2 3 4 5 6 7 8 9 10
   | @Configuration public class InterceptorConfig implements WebMvcConfigurer {          @Override     public void addInterceptors(InterceptorRegistry registry) {         registry.addInterceptor(new JWTInterceptor())                 .addPathPatterns("/user/test")                 .excludePathPatterns("/user/login");     } }
   | 
 
- 接口获取 jwt
 
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
   | @RestController @Slf4j public class UserController {     @Autowired     private UserService userService;
      @GetMapping("/user/login")     public Map<String,Object> login(User user){         log.info("用户名:[{}]",user.getName());         log.info("密码:[{}]",user.getPwd());         HashMap<String, Object> map = new HashMap<>();         try {             User login = userService.login(user);             HashMap<String,String> payload = new HashMap<>();             payload.put("id",login.getId());             payload.put("name",login.getName());                          String token = JWTUtils.getToken(payload);
              map.put("state",true);             map.put("msg","认证成功");             map.put("token",token);         }catch (Exception e){             map.put("state",false);             map.put("msg",e.getMessage());         }         return map;     }
      @RequestMapping("/user/test")     public Map<String,Object> test(HttpServletRequest request){         HashMap<String, Object> map = new HashMap<>();         String token = request.getHeader("token");         DecodedJWT verify = JWTUtils.verify(token);         log.info("用户id:[{}]",verify.getClaim("id").asString());         log.info("用户name:[{}]",verify.getClaim("name").asString());
          map.put("state",true);         map.put("msg","请求成功");         return map;     } }
   | 
 
前端传递 token
- 放在请求头中
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
   |  $.ajax({     type: "post",     url: "http:///test/getInfo",     headers: {               Accept: "application/json; charset=utf-8",         token: "" + token       },     data:JSON.stringify(jsonDate),     contentType: "application/json",       dataType: "json",     success: function(data){       console.log('ok');     },     error:function(){         console.log('error');     } })
  | 
 
- 使用 beforeSend 设置请求头
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
   | $.ajax({            type: "post",            url: "http://aliyun.seatang.cn:8080/onlinejudge/test/getInfoById",            beforeSend: function(request) {                      request.setRequestHeader("token", token);                 request.setRequestHeader("Content-Type","application/json");             },            data:JSON.stringify(jsonDate),            dataType: "json",            success: function(data){                console.log('ok');            },            error:function(){                console.log('error');            }        })
  | 
 
- 前端保存 token
 
1 2 3 4 5 6
   | data: {   token:localStorage.getItem("token"); }, success: function (token) {   localStorage.setItem("token",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
   | public class JWTUtil {
           private static final long EXPIRE_TIME = 5*60*1000; 	     private static final String SECERT="chen";          
 
 
 
 
      public static boolean verify(String token, String username, String secret) {         try {             Algorithm algorithm = Algorithm.HMAC256(secret);             JWTVerifier verifier = JWT.require(algorithm)                     .withClaim("username", username)                     .build();             DecodedJWT jwt = verifier.verify(token);             return true;         } catch (Exception exception) {             return false;         }     }
      
 
 
      public static String getUsername(String token) {         try {             DecodedJWT jwt = JWT.decode(token);             return jwt.getClaim("username").asString();         } catch (JWTDecodeException e) {             return null;         }     }
      
 
 
 
 
      public static String sign(String username) {         try {             Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);             Algorithm algorithm = Algorithm.HMAC256(SECERT);                          return JWT.create()                     .withClaim("username", username)                     .withExpiresAt(date)                     .sign(algorithm);         } catch (UnsupportedEncodingException e) {             return null;         }     } }
  |