Springboot整合jwt实现token登录验证功能

1,824 阅读4分钟

一、导入jwt依赖

自己认为比较重要的知识点,可以提前学习一下:

  1. jwt的含义Json web token,基于json的开放标准,用于通信双方传递信息,可以实现加密。
  2. jwt的请求流程,客户端登录的时候会向服务端发送登录请求,服务端会给此用户生成一个专属token字符串。以后用户的请求在Header里边加入token字符串,服务端会检查此token是否正确,并且可以通过此token得到用户的信息,token不光可以确定用户信息是否存在,也可以通过token反向得到当前发送请求的是用户信息。(通过什么信息生成的token,那么就可以通过token反向得到什么信息,本篇文章用的是用户的id,那么也可以得到用户id;如果想用整个用户类来生成token也是可以的)
  3. jwt的结构,分成三部分,Header头部,Payload负载,Signature签名。这里不做详细解释,可以去查一下相关内容。
  4. token可以设置过期时间,当时自己一直没想明白为什么假设可以2小时内有效。因为在 Payload 中,存放了token的生成时间,以及自定义的有效时间,那么过期时间 = 生成时间 + 有效时间。将生成时间、过期时间一起去生成这个token,token里就会自动鉴别是否有效。
<!-- jwt -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

二、实现jwt工具类(生成token,检查token)

jwtToken可以根据自己的需要自行设定,可以理解为盐。写了一个main主函数,可以用于测试,我自己填入的token肯定是不管用了,可以重新用 Long id = 1L 再生成一个。

import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class JWTUtils {

    private static final String jwtToken = "123456******!@#$$";

    public static String createToekn(Long userId) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", userId);
        JwtBuilder jwtBuilder = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,密钥为 jwtToken
                .setClaims(claims) // body数据,唯一确认,自行设置
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)); // 一天内的有效时间
        String token = jwtBuilder.compact();
        return token;
    }

    // TODO
    public static Map<String, Object> checkToken(String token) {
        try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
            return (Map<String, Object>) parse.getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        Long id = 1L;
        String tk = createToekn(id);
        System.out.println(tk);
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MzkyOTI2NDksInVzZXJJZCI6MSwiaWF0IjoxNjM5MjA2MjQ5fQ.8dAHN7hWI6iiBT_JHqJqZsyzL7V2OYVesg7kChVuHj8";
        Map<String, Object> map = checkToken(token);
        Iterator iter = map.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry) iter.next();
            Object key = entry.getKey();
            Object value = entry.getValue();
            System.out.print(key);
            System.out.print("----");
            System.out.println(value);
        }
    }
}

三、设置拦截器

这里已经不算是jwt的部分,可以选择性的看。 拦截器可以配置请求路径,哪些路径的请求信息我们必须要验证用户是否登录,或者我们需要这个用户的信息。把所有可能用到的类都放出来了,除了SysUser部分,这个是需要自己项目需要来做。

1、WebConfig

这里我们拦截test请求。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        /**
         * 拦截 test 接口,后续实际遇到需要拦截的端口时,再配置为真正的拦截接口
         */
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test");

    }
}

2、LoginInterceptor

import com.alibaba.fastjson.JSON;
import com.***.service.SysUserService;
import com.***.utils.UserThreadLocal;
import com.***.vo.ErrorCode;
import com.***.vo.LoginUserVo;
import com.***.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        /**
         * 在执行 Controller方法(Handler)之前进行
         * 1、需要判断,请求的接口路径是否为 HandlerMethod(Controller方法)
         * 2、判断 Token 是否为空,如果为空,即为未登录
         * 3、如果 Token 不为空,进行登录验证 checkToken
         * 4、如果认证成功,放行即可
         */
        if (!(handler instanceof HandlerMethod)) {
            /**
             * handler 可能是 RequestResourceHandler,Springboot 程序访问静态资源,默认去 classpath 下的 static 目录查询
             */
            return true;
        }
        String token = request.getHeader("Authorization");

        log.info("=================request start===========================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}", requestURI);
        log.info("request method:{}", request.getMethod());
        log.info("token:{}", token);
        log.info("=================request end===========================");

        if (StringUtils.isBlank(token)) {
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        Result res = sysUserService.findUserInfoByToken(token);
        if (!res.isSuccess()) {
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        /**
         * 登录成功,放行
         * 在 Controller 中,直接获取登录信息
         */
        LoginUserVo loginUserVo = (LoginUserVo) res.getData();
        UserThreadLocal.put(loginUserVo);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        /**
         * 如果不删除,ThreadLocal 中用完的信息,有内存泄漏的风险
         */
        UserThreadLocal.remove();
    }
}

3、UserThreadLocal

public class UserThreadLocal {

    private UserThreadLocal() {
    }

    /**
     * 线程变量隔离
     */
    private static final ThreadLocal<LoginUserVo> LOCAL = new ThreadLocal<>();

    public static void put(LoginUserVo loginUserVo) {
        LOCAL.set(loginUserVo);
    }

    public static LoginUserVo get() {
        return LOCAL.get();
    }

    public static void remove() {
        LOCAL.remove();
    }

}

4、Result

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {

    private boolean success;

    private Integer code;

    private String msg;

    private Object data;

    public static Result success(Object data) {
        return new Result(true, 200, "success", data);
    }

    public static Result fail(Integer code, String msg) {
        return new Result(false, code, msg, null);
    }
}

5、ErrorCode

public enum ErrorCode {

    PARAMS_ERROR(10001, "参数有误"),
    ACCOUNT_PWD_NOT_EXIST(10002, "用户名或密码不存在"),
    TOKEN_ERROR(10003, "Toekn不合法"),
    ACCOUNT_EXIST(10004,"账号已存在"),
    NO_PERMISSION(70001, "无访问权限"),
    SESSION_TIME_OUT(90001, "会话超时"),
    NO_LOGIN(90002, "未登录"),
    ;

    private int code;

    private String msg;

    ErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}