在阅读本文之前,我们还应该对session、cookie、JWT有一个基本的了解。在本篇文章中小码仔不再对它们做出过多赘述,如果对这三者认识还不够清晰的小可爱可以先移步这里:看完这篇 Session、Cookie、Token,和面试官扯皮就没问题了对其做基本的了解和认识。
如果你已对以上三者有了的基本概念和了解,但是对于JWT的使用还充满疑问的话,那么本篇文章就是为你而写。本文我们将使用SpringBoot集成JWT来实现一个简单的token验证,从而使我们对JWT的使用有一个基本的了解。
首先我们搭建好SpringBoot框架,SpringBoot环境准备就绪。接下来执行以下操作:
引入JWT依赖,由于是基于Java,所以需要的是java-jwt
。
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.5.0</version> </dependency>
在这一步,我们在annotation包下定义一个用户需要登录才能进行其他接口访问等一系列操作的注解TokenRequired
。
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface TokenRequired { boolean required() default true; }
@Target
旨意为我们自定义注解@TokenRequired
的作用目标,因为我们本次注解的作用目标为方法层级,因此使用 ElementType.METHOD
。
@Retention
旨意为我们自定义注解 @TokenRequired
的保留位置,@TokenRequired
的保留位置被定义为RetentionPolicy.RUNTIME
这种类型的注解将被JVM保留,他能在运行时被JVM或其他使用反射机制的代码所读取和使用。
在entity包中,我们使用lombok,简单自定义一个实体类User。
@Data @AllArgsConstructor @NoArgsConstructor public class User { String Id; String username; String password; }
在这一步,我们在util包下面创建一个JwtUtil工具类,用于生成token和校验token。
public class JwtUtil { //过期时间15分钟 private static final long EXPIRE_TIME = 15*60*1000; //生成签名,15分钟后过期 public static String sign(String username,String userId,String password){ //过期时间 Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); //使用用户密码作为私钥进行加密 Algorithm algorithm = Algorithm.HMAC256(password); //设置头信息 HashMap<String, Object> header = new HashMap<>(2); header.put("typ", "JWT"); header.put("alg", "HS256"); //附带username和userID生成签名 return JWT.create().withHeader(header).withClaim("userId",userId) .withClaim("username",username).withExpiresAt(date).sign(algorithm); } //校验token public static boolean verity(String token,String password){ try { Algorithm algorithm = Algorithm.HMAC256(password); JWTVerifier verifier = JWT.require(algorithm).build(); verifier.verify(token); return true; } catch (IllegalArgumentException e) { return false; } catch (JWTVerificationException e) { return false; } } }
在service包下,我们创建一个UserService,并定义一个login方法,用于做登录接口的业务层数据校验,并调取JwtUtil中方法生成token。
@Service("UserService") public class UserService { @Autowired UserMapper userMapper; public String login(String name, String password) { String token = null; try { //校验用户是否存在 User user = userMapper.findByUsername(name); if(user == null){ ResultDTO.failure(new ResultError(UserError.EMP_IS_NULL_EXIT)); }else{ //检验用户密码是否正确 if(!user.getPassword().equals(password)){ ResultDTO.failure(new ResultError(UserError.PASSWORD_OR_NAME_IS_ERROR)); }else { // 生成token,将 user id 、userName保存到 token 里面 token = JwtUtil.sign(user.getUsername(),user.getId(),user.getPassword()); } } } catch (Exception e) { e.printStackTrace(); } return token; } }
Algorithm.HMAC256()
:使用HS256
生成token
,密钥则是用户的密码,唯一密钥的话可以保存在服务端。
withAudience()
存入需要保存在token
的信息,这里我把用户ID存入token
中。
接下来我们需要写一个拦截器去获取token并验证token。
public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired UserService userService; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { // 从 http 请求头中取出 token String token = httpServletRequest.getHeader("token"); // 如果不是映射到方法直接通过 if(!(object instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod=(HandlerMethod)object; Method method=handlerMethod.getMethod(); //检查有没有需要用户权限的注解 if (method.isAnnotationPresent(TokenRequired.class)) { TokenRequired userLoginToken = method.getAnnotation(TokenRequired.class); if (userLoginToken.required()) { // 执行认证 if (token == null) { throw new RuntimeException("无token,请重新登录"); } // 获取 token 中的 user id String userId; try { userId = JWT.decode(token).getClaim("userId").asString(); } catch (JWTDecodeException j) { throw new RuntimeException("401"); } User user = userService.findUserById(userId); if (user == null) { throw new RuntimeException("用户不存在,请重新登录"); } // 验证 token try { if(!JwtUtil.verity(token,user.getPassword())){ throw new RuntimeException("无效的令牌"); } } catch (JWTVerificationException e) { throw new RuntimeException("401"); } 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 { } }
AuthenticationInterceptor
拦截器实现了HandlerInterceptor
接口的三个方法:
boolean preHandle ():
预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller返回值,返回值为true会调用下一个拦截器或处理器,或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。
void postHandle():
后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
void afterCompletion():
整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。
该拦截器的执行流程为:
在配置类上添加了注解@Configuration
,标明了该类是一个配置类并且会将该类作为一个SpringBean添加到IOC容器内。
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { // 将我们上步定义的实现了HandlerInterceptor接口的拦截器实例authenticationInterceptor添加InterceptorRegistration中,并设置过滤规则,所有请求都要经过authenticationInterceptor拦截。 registry.addInterceptor(authenticationInterceptor()) .addPathPatterns("/**"); } }
WebMvcConfigurer
接口是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件来实现基本的配置需要。
InterceptorConfig
内的addInterceptor
需要一个实现HandlerInterceptor
接口的拦截器实例,addPathPatterns
方法用于设置拦截器的过滤路径规则。
在addInterceptors
方法中,我们将第6步定义的实现了HandlerInterceptor
接口的拦截器实例authenticationInterceptor
,添加至InterceptorRegistration
中,并设置过滤路径。现在,我们所有请求都要经过authenticationInterceptor
的拦截,拦截器authenticationInterceptor
通过preHandle
方法的业务过滤,判断是否有@TokenRequired
来决定是否需要登录。
@RestController @RequestMapping("user") public class UserController { @Autowired UserService userService; /** * 用户登录 * @param user * @return */ @PostMapping("/login") public ResultDTO login( User user){ String token = userService.login(user.getUsername(), user.getPassword()); if (token == null) { return ResultDTO.failure(new ResultError(UserError.PASSWORD_OR_NAME_IS_ERROR)); } Map<String, String> tokenMap = new HashMap<>(); tokenMap.put("token", token); return ResultDTO.success(tokenMap); } @TokenRequired @GetMapping("/hello") public String getMessage(){ return "你好哇,我是小码仔"; } }
不加注解的话默认不验证,登录接口一般是不验证的。所以我在getMessage()
中加上了登录注解,说明该接口必须登录获取token后,在请求头中加上token并通过验证才可以访问。
我在代码中对getMessage()
添加了@TokenRequired
注解,此刻访问该方法时必须要通过登录拿取到token值,并在请求头中添加token才可以访问。我们现在做以下校验:
如上图所示,请求结果显示:无token,请重新登录。
回顾一下本次JWT使用的基本业务判断流程:
不足之处:本次集成只是做一个简单的JWT使用介绍,没有实现token的过期刷新机制,此种情况下用户每隔15分钟就需要重新登录一次,如果在实际生产环境中使用,可能会被用户打死,因此实际开发中并不推荐。
关于token的刷新机制,小码仔将在下篇文章中为大家解读并附上源码。
本次集成代码地址:https://github.com/bailele1995/springboot-jjwt.git
文章参考:
https://my.oschina.net/odetteisgorgeous/blog/1920762
https://www.zhihu.com/search?type=content&q=spring%20boot%20jwt
https://mp.weixin.qq.com/s/q4upZNTul5Z5WSq2wHC9lg
https://mp.weixin.qq.com/s/Vh-75A7qN8lDo_0DQXCkzg
https://mp.weixin.qq.com/s/KlXc5hWEfgj-Q9cMabeOdA
https://juejin.cn/post/6844904034181070861
来源:https://juejin.cn/post/6844904136626929677