来源:https://www.fangzhipeng.com/architecture/2017/08/11/jwt.html
介绍
1、什么是JWT
JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且字自包含的标准,用于将各方之间的信息地传输为JSON对象。 该信息是通过数字签名进行验证。使用HMAC算法或使用RSA的公钥/私钥对JWT进行签名,所以它的安全性非常高。
进一步解释它的特点或者概念:
• 紧凑型(compact):由于是加密后的字符串,JWT数据体积非常的小,可通过 POST参数或HTTP请求头发送。 另外,数据体积小意味着传输速度很快。
• 自包含(self-contained):JWT包含了用户的所有信心,所以避免了每次查询数据库,降低了服务器的负载。
2、JWT的应用场景
常用于以下场景:
• 验证:这是使用JWT的最常见的场景。 一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源。 单点登录是一个广泛使用JWT的功能,因为它的开销很小,并且能够在不同的域中轻松使用。
• 信息交换:JWT是在各方之间安全传输信息的好方法,因为它们可以被签名,例如使用公钥/私钥对. 另外,当使用标题和有效载荷计算签名时,还可以验证内容是否未被篡改。
3、JWT结构:
(1):hearder:标题通常由两部分组成:令牌的类型,即JWT,以及使用的哈希算法,如HMAC SHA256或RSA。 比如:
{
“alg”:“HS256”,
“typ”:“JWT”
}
(2):Payload:这是JWT的第二部分,包含了用户的一些信息和Cliam(声明、权利),有三种类型的Cliam:保留,公开和私人声明。 一个典型的payload应该如下:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
将payload进行Base64 编码作为JWT的第二部分。
(3)Signature:要创建签名部分,需要使用到用Base64编码后header和payloader,以及秘钥,将它们签名,一个典型的格式如下:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
集成
1、引入依赖:
<!--JWT--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
2、拦截所有的请求,进行验证
/** * @author caib * 1.x 的版本使用 WebMvcConfigurerAdapter * 2.x 的版本使用 WebMvcConfigurer * * @date */ @Configuration public class InterceptorConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); // 拦截所有请求,通过判断是否有 @LoginRequired 注解 决定是否需要登录 } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } }
3、自定义token 拦截器
/** * @author caib * @date */ public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired private RedisCacheUtils redisTemplate; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token // 如果不是映射到方法直接通过 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证 if (method.isAnnotationPresent(PassToken.class)) { PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } } //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(UserLoginToken.class)) { UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId ; try { userId = JWT.decode(token).getAudience().get(0); } catch (Exception e) { throw new BizException("无效的token,请重新登录"); } //方法1:将token存放再redis中,进行验证 String redisToken = redisTemplate.getCacheObject("token_" + userId); // 验证 token if ( !token.equals(redisToken)) { throw new BizException("无效的token,请重新登录"); } //方法2 通过查询数据库用户信息进行验证token,其中的password 自行获取(注意数据库保存的密码是否加密) // 验证 token JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(password)).build(); try { jwtVerifier.verify(token); } catch (JWTVerificationException e) { throw new RuntimeException("401"); } //更新token有效期 (待定) return true; } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
4、注解表示该方法是否需要进行token验证
跳过token验证:
/** * @author caib * @date */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface PassToken { boolean required() default true; }
需要token验证
/** * @author caib * @date */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface UserLoginToken { boolean required() default true; }
5、用户登录记录token信息,其中userid为用户登录名,password 为登录密码,并将token保存在redis中
public String getToken(String userid,String password) { String token=""; token= JWT.create().withAudience(userid)// 将 user id 保存到 token 里面 .sign(Algorithm.HMAC256(password));// 以 password 作为 token 的密钥 return token; }
6、自定义全局异常处理(ControllerAdvice)
其中的Result为自定义的Controller自定义返回内,MyException 为自定义异常。
@ControllerAdvice @ResponseBody @Slf4j public class GlobalExceptionHandler { /** * 所有异常报错 * @param request * @param e * @return * @throws Exception */ @ExceptionHandler(value=Exception.class) public Result allExceptionHandler(HttpServletRequest request, Exception e) throws Exception{ log.error("服务器异常原因是:",e); return new Result("ERROR",20001,"服务器内部异常",null); } /** * 自定义异常处理 * @param request * @param e * @return */ @ExceptionHandler(value = MyException.class) public Result bizExceptionHandler(HttpServletRequest request,MyException e){ log.error("业务异常:",e); return new Result("ERROR",Integer.parseInt(e.getErrorCode()),e.getErrorMsg(),null); } }