一、引言 Web应用软件开发过程中,不可忽视也是最重要的一点,就是保存用户的登录状态。传统的Session方式,主要依赖于服务器的会话信息存储。随着后端服务的扩展,单台服务器性能不足以支撑。多实例服务器的部署,则会导致用户登录状态无法在多个服务器实例中同步。为解决此问题,提出了JWT令牌的方式。用户登陆成功以后,根据必要信息生成token值返回前端,此后每一次http请求均携带此token,后端即可解析token得到用户的登陆状态。
二、操作步骤 1.导入JWT依赖项 SpringBoot框架下项目的第三方工具包均通过Maven进行管理,通过导入Maven依赖的方式即可使用JWT令牌。JWT令牌技术适用于整个Web应用软件,并不只局限于Java语言,针对Java语言应选择JJWT依赖进行导入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-api</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-impl</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt-jackson</artifactId > <scope > runtime</scope > </dependency >
此三项依赖缺一不可,其中依赖版本可由父模块pom.xml文件进行依赖管理。
2.自定义token生成方法 该字符串为生成的JWS示例,JWS即为被签名的JWT。JWS被.分割为三个部分:其中左部为Header,包含签名算法和令牌类型等信息;中部为Payload,包含实际数据;右部为Signature,是对头部和载荷的签名,用于验证消息完整性和真实性。
1 eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJKb2UifQ.1KP0SsvENi7Uz1oQc07aXTL7kpQG5jBNIybqr60AlD4
在生成token的方法中,应为JWT构造器提供完整的Header、Payload和Signature三个参数,同时为符合业务真实性,还应设置token过期时间。例如存储用户信息需要id和username两个字段。
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 public class JwtUtil { public static final SecretKey secretKey = Keys.hmacShaKeyFor("abcdefghijklmnopqrstuvwxyzabcdef" .getBytes(StandardCharsets.UTF_8)); public static String createToken (Long id, String username) { return Jwts.builder() .setSubject("LOGIN_USER" ) .setExpiration(new Date (System.currentTimeMillis() + 60 * 60 * 1000L )) .addClaims(Map.of("id" , id, "username" , username)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); } }
Header通常不需要手动填充,JWT库会默认设置常见的字段,如alg和typ。Payload部分需由手动填充,subject和claim等字段均属于Payload部分。Signature可自定义密钥key和签名算法,通常密钥key应和签名算法匹配。
3.自定义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 public class JwtUtil { public static final SecretKey secretKey = Keys.hmacShaKeyFor("abcdefghijklmnopqrstuvwxyzabcdef" .getBytes(StandardCharsets.UTF_8)); public static Claims parseToken (String token) { if (token == null ) { throw new RuntimeException (); } JwtParser jwtParser = Jwts.parserBuilder() .setSigningKey(secretKey) .build(); Claims claims; try { claims = jwtParser.parseClaimsJws(token).getBody(); } catch (ExpiredJwtException e) { throw e; } catch (JwtException e) { throw e; } return claims; } }
获取token并解析时,若token已过期则抛出过期异常,其余所有异常均抛出token无效异常。将解析成功的Claims参数返回,由用户登录拦截器接收并读取登录用户的信息。
4.存储线程变量 定义包含id和username两字段的POJO,实例化登录用户的必要信息。
1 2 3 4 5 6 7 @Data @AllArgsConstructor public class LoginUser { private Long id; private String username; }
使用SpringBoot框架的项目中,从前端发起的每一次http请求,都会开辟一条独立的线程进行运行。将登录用户的必要信息,存入线程变量,能够保证一次http请求的线程内的用户信息的安全。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class LoginUserHolder { public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal <>(); public static void setLoginUser (LoginUser loginUser) { threadLocal.set(loginUser); } public static LoginUser getLoginUser () { return threadLocal.get(); } public static void remove () { threadLocal.remove(); } }
5.配置登录拦截器 登录相关之外的所有资源接口,其访问请求处理之前均须通过拦截器进行处理。从请求头中读取token后进行解析,从解析结果中获取当前登陆用户的id和username信息,并存入线程变量以待后续业务使用。
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 @Component public class AuthenticationInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("access-token" ); Claims claims = JwtUtil.parseToken(token); Long id = claims.get("id" , Long.class); String username = claims.get("username" , String.class); LoginUserHolder.setLoginUser(new LoginUser (id, username)); return true ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { LoginUserHolder.remove(); } }
当一次http访问请求处理完毕后,从线程变量中移除登录用户的信息,防止资源占用导致服务性能下降。
三、写在最后 JWT令牌技术良好的解决了Session会话在多服务器实例部署下,无法实现Session信息同步的问题。同时,其负载有效数据的相关API较之Session操作更加简单易读。所提供的多种签名算法,也能够在一定程度上保证JWT令牌的安全性,防止其被恶意篡改所导致的安全问题。