SpringBoot+JWT实战(附源码)

6,559 阅读13分钟

在阅读本文之前,我们还应该对session、cookie、JWT有一个基本的了解。在本篇文章中小码仔不再对它们做出过多赘述,如果对这三者认识还不够清晰的小可爱可以先移步这里:看完这篇 Session、Cookie、Token,和面试官扯皮就没问题了对其做基本的了解和认识。

如果你已对以上三者有了的基本概念和了解,但是对于JWT的使用还充满疑问的话,那么本篇文章就是为你而写。本文我们将使用SpringBoot集成JWT来实现一个简单的token验证,从而使我们对JWT的使用有一个基本的了解。

SpringBoot集成JWT

首先我们搭建好SpringBoot框架,SpringBoot环境准备就绪。接下来执行以下操作:

1.引入依赖

引入JWT依赖,由于是基于Java,所以需要的是java-jwt

<dependency>
      <groupId>com.auth0</groupId>
      <artifactId>java-jwt</artifactId>
      <version>3.5.0</version>
</dependency>

2.自定义注解

在这一步,我们在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或其他使用反射机制的代码所读取和使用。

3.定义实体类

在entity包中,我们使用lombok,简单自定义一个实体类User。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    String Id;
    String username;
    String password;
}

4.定义一个JWT工具类

在这一步,我们在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;
        }
    }
}

5.业务校验并生成token

在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中。

6.定义拦截器

接下来我们需要写一个拦截器去获取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接口的三个方法:

  1. boolean preHandle ():

预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller返回值,返回值为true会调用下一个拦截器或处理器,或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。

  1. void postHandle():

后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView对模型数据进行处理或对视图进行处理,modelAndView也可能为null。

  1. void afterCompletion():

整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。

该拦截器的执行流程为:

  1. 从 http 请求头中取出 token;
  2. 检查有没有需要用户权限的注解,如果需要,检验token是否为空;
  3. 如果token不为空,查询用户信息并校验token;
  4. 校验通过,则进行业务访问处理,校验失败则返回token失效信息。

7.配置拦截器

在配置类上添加了注解@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 来决定是否需要登录。

8.定义接口方法并添加注解

@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才可以访问。我们现在做以下校验:

  1. 直接访问,不在请求头里添加token:

如上图所示,请求结果显示:无token,请重新登录。

  1. 访问登录接口,获取token,并在请求头中添加token信息: 此时,访问成功。
  2. 15分钟后,token失效,我们再次在请求头中添加token信息访问: 此时token已失效,返回:无效的令牌。

总结

回顾一下本次JWT使用的基本业务判断流程:

  1. 用户访问页面,前端请求相关接口,经过拦截器,拦截器中从 http 请求头中取出 token;
  2. 检查该接口有没有@TokenRequired注解,如果没有,直接放行;如果有,检验token是否为空;
  3. 如果token为空,访问失败;token不为空,则查询用户信息并校验token;
  4. 校验通过,则进行业务访问处理,校验失败则返回token失效信息。

不足之处:本次集成只是做一个简单的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