image

SpringBoot前后端分离项目集成JWT实现用户验证

  • WORDS 5939

SpringBoot前后端分离项目中集成JWT实现Token验证

在前后端分离开发的项目中,为了保存和验证用户的登录信息,SessionCookie都是很好的选择,但是浏览器的跨域规则会导致后端发送给前端的 Cookie不会被保存或前端发送给后端的请求每次的 SessionID都不一致。为了能从请求中拿到用户的一些信息和解决上面的问题以及安全性考虑,就可以采用 Token

准备工作

引入 JWTJava依赖

<properties>
    <java-jwt.version>3.4.0</java-jwt.version>
</properties>

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>${java-jwt.version}</version>
</dependency>

编写工具类

/**
 * @Author: lisang
 * @DateTime: 
 * @Description: JWT工具类
 */
@Component
// 使用配置文件绑定
@ConfigurationProperties("spring.jwt")
@Getter
public class JWTUtils {

    /**
     * Token前缀
     */
    private static String tokenPrefix;

    /**
     * 加密密钥
     */
    private static String secret;

    /**
     * Token的过期时间
     */
    private static long expireTime;

    // 静态属性的注入使用Set方法
    public void setExpireTime(long expireTime) {
        JWTUtils.expireTime = expireTime;
    }
    public void setTokenPrefix(String tokenPrefix) {
        JWTUtils.tokenPrefix = tokenPrefix;
    }
    public void setSecret(String secret) {
        JWTUtils.secret = secret;
    }

    /**
     * 生成Token
     * @param value 需要包含在Token中的内容
     * @return 生成的Token
     */
    public static String createToken(String value) {
        return  tokenPrefix + JWT.create().withSubject(value)
                .withExpiresAt(new Date(System.currentTimeMillis() + expireTime))
                .sign(Algorithm.HMAC512(secret));
    }

    /**
     * 验证Token是否有效
     * @param token Token字符串
     * @return 返回Token中携带的参数或抛出异常
     */
    public static String validationToken(String token) {
        try{
            return JWT.require(Algorithm.HMAC512(secret))
                    .build()
                    .verify(token.replace(tokenPrefix, ""))
                    .getSubject();
        }catch (TokenExpiredException ex) {
            throw new AuthException("Token已过期");
        }catch (Exception ex){
            throw new AuthException("Token验证失败");
        }
    }

    /**
     * 验证Token是否需要更新
     * @param token Token字符串
     * @return 需要更新返回True 不需要返回False Token验证失败抛出异常
     */
    public static boolean isUpdateToken(String token) {
        Date expiresAt = null;
        try {
            expiresAt = JWT.require(Algorithm.HMAC512(secret)).build().verify(token).getExpiresAt();
        }catch (TokenExpiredException ex) {
            return true;
        }catch (Exception ex) {
            throw new AuthException("Token验证失败");
        }
        // 剩余有效期小于1天需要更新
        return (expiresAt.getTime() - System.currentTimeMillis()) < 8640000;
    }
}

后端返回Token

后端需要在控制器方法中拿到 Response响应请求对象

// 登录成功之后通过用户信息生成Token
String token = JWTUtils.createToken(CommonConstant.TOKEN_VALUE);
// 将Token添加到响应头
response.addHeader(CommonConstant.HEADER_KEY, token);
// 设置响应头显示,不然Axios拿不到Token
response.setHeader("Access-Control-Expose-Headers", CommonConstant.HEADER_KEY);

拦截器验证Token

添加一个验证拦截器类 AuthInterceptor并添加到拦截器链中,拦截需要验证的所有请求。

/**
 * @Author: lisang
 * @DateTime: 
 * @Description: 登陆验证拦截器
 */
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果用户没有点击保持登录 那么登录信息就存放在session中
       	// 先从session中获取用户信息,获取不到再从Token获取
        HttpSession session = request.getSession();
        String sessionValue = String.valueOf(session.getAttribute(CommonConstant.SESSION_KEY));
        if(!BaseUtils.isEmpty(sessionValue) && sessionValue.equals(CommonConstant.TOKEN_VALUE)){
            return true;
        }
        // 获取请求头中的Token
        String tokenValue = request.getHeader(CommonConstant.HEADER_KEY);
        // 判断为空则抛出异常
        ExceptionUtils.throwAuthExceptionIfTrue(BaseUtils.isEmpty(tokenValue), "Token不存在");
        // 验证Token Token过期或者解析失败都会在验证方法中抛出异常
        String sub = JWTUtils.validationToken(tokenValue);
        // 如果Token解析出来的值和放入Token的值不同 也会抛出异常 实际开发中通常通过缓存验证
        ExceptionUtils.throwAuthExceptionIfTrue(!sub.equals(CommonConstant.TOKEN_VALUE), "Token参数错误");
        return true;
    }
}

跨域拦截器

在有自定义请求头的情况下,针对预检请求,要在 Response响应请求中允许自定义请求头的发送,因此使用拦截器来解决跨域问题。、

/**
 * @Author: lisang
 * @DateTime: 
 * @Description: 跨域拦截器 处理跨域请求 OPTION预检请求直接返回204状态码
 */
public class CorsInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");
        if (HttpMethod.OPTIONS.toString().equals(request.getMethod())){
            // 允许自定义请求头发送
            response.setHeader("Access-Control-Allow-Headers", "Image-Auth-Token, Content-Type");
            // 设置预检请求的缓存时间
            response.setHeader("Access-Control-Max-Age", "3600");
            response.setStatus(HttpStatus.NO_CONTENT.value());
            return false;
        }
        return true;
    }
}

定义JWT配置

使用了配置文件绑定,直接在 application.yml文件中定义

 spring:
   jwt:
     token-prefix: ''
     secret: ''
     # Token三天过期
     expire-time: 259200000

前端Axios的处理

添加 Axios请求和响应拦截器,拿到 Token后保存到浏览器本地缓存并在发送请求时自动添加

const TOKEN_KEY = 'image-auth-token';

// Axios Request拦截器
axios.interceptors.request.use(req => {
    // 判断缓存中是否存在Token 存在则添加到请求头
    if(!cacheManager.isEmpty(TOKEN_KEY)){
        req.headers[TOKEN_KEY] = cacheManager.getCache<String>(TOKEN_KEY);
    }
    return req;
})

// Axios response拦截器
axios.interceptors.response.use(res => {
    // 从响应中拿到Token 如果存在就添加到缓存
    const authToken = res.headers[TOKEN_KEY];
    if(authToken){
        cacheManager.setCache<String>(TOKEN_KEY, authToken, 72);
    }
    return res.data;
}, error => {}

关联文章

0 条评论