一.本文介绍

    上篇文章讲到Spring Boot整合Swagger的时候其实我就在思考关于接口安全的问题了,在这篇文章了我整合了JWT用来保证接口的安全性。我会先简单介绍一下JWT然后在上篇文章的基础上整合JWT。

二.JWT简介

    JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。我理解的JWT主要的两点作用:1.用来校验权限角色等;2.用来校验传输的信息是否被篡改过。JWT有这样的功能和它的结构是有关的,它由三部分组成,它们之间用圆点(.)连接。这三部分分别是:

  • Header
  • Payload
  • Signature

因此,一个典型的JWT看起来是这个样子的:

xxxxx.yyyyy.zzzzz

接下来,具体看一下每一部分:

Header

header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。

例如:

Springboot集成JWT 存入redis springboot整合jwt入门_spring

然后,用Base64对这个JSON编码就得到JWT的第一部分

Payload

JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。

  • Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
  • Public claims : 可以随意定义。
  • Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。

下面是一个例子:

Springboot集成JWT 存入redis springboot整合jwt入门_spring_02

对payload进行Base64编码就得到JWT的第二部分

注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。

Signature

为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。

例如:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。

三.Spring Boot整合JWT

    首先还是需要引入JWT的相关依赖如下

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

    写一个JWT的工具类用于创建token并校验token的合法性代码如下

package cn.test.util;


import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.util.StringUtils;

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

public class JWTUtil {

    public static final String SECRET = "JKKLJOoasdlfj";

    public static final Long EXPIRE = 5 * 60 * 1000L;

    public static String createToken(Long userId) throws Exception {
       // header Map
        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");
        // build token
        // param backups {iss:Service, aud:APP}
        String token = JWT.create().withHeader(map) // header
                .withClaim("iss", "Service") // payload
                .withClaim("aud", "APP").withClaim("user_id", null == userId ? null : userId.toString())
                //.withIssuedAt() // sign time
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRE)) // expire time
                .sign(Algorithm.HMAC256(SECRET)); // signature
        return token;
    }

    public static Map<String, Claim> verifyToken(String token) {
        DecodedJWT jwt = null;
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            jwt = verifier.verify(token);
        } catch (Exception e) {
             //token 校验失败, 抛出Token验证非法异常
             e.printStackTrace();
        }
        return jwt.getClaims();
    }

    public static Long getUserId(String token) {
        Map<String, Claim> claims = verifyToken(token);
        Claim userIdClaim = claims.get("user_id");
        if (null == userIdClaim || StringUtils.isEmpty(userIdClaim.asString()))       {
            // token 校验失败, 抛出Token验证非法异常
        }
        return Long.valueOf(userIdClaim.asString());
    }
}

    其中userId作为token的主要载荷数据,大体思路是配合拦截器使用,第一次登录时把userId对应的权限存到redis中,拦截器对于每个请求都根据token里的userId对应的权限对接口进行权限控制,在这里需要注意几点:1.token我认为放到cookie里更合适防止直接被获取;2.如果允许的话最好使用https;3.token里的校验信息最好加上客户端的环境比如说ip和User Agent等;设置token的超时时间等在代码里已经有体现了。拦截器代码如下

package cn.test.interceptor;


import cn.test.util.CookieUtil;
import cn.test.util.JWTUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;

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

public class VerifyTokenInterceptor implements HandlerInterceptor {

    private Logger logger = LoggerFactory.getLogger(VerifyTokenInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
        try {
            String token = CookieUtil.get(request, "token").getValue();
            Long userId = JWTUtil.getUserId(token);
            logger.info("****userId = {}*****",userId);
            //todo 校验token是否合法,如果不合法拦截器做处理
        }catch (Exception e){
            //发生异常跳转到指定接口或做其他处理
            response.sendRedirect(request.getContextPath()+"/login/verify");
        }
        return true;
    }
}

    在这里需要注意,正常使用拦截器需要把拦截路径配置在配置文件里但Spring Boot不需要使用这样的配置文件,我们需要一个配置类,加上@Configuration注解的类的作用实际和配置文件是一样的,我们把拦截器注册到Spring中代码如下

package com.daojia.config;

import com.daojia.interceptor.VerifyTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new VerifyTokenInterceptor())
                .addPathPatterns("/**")
                //登录接口不用通过拦截器否则会形成死循环,因为第一次登录没有token信息会一直跳转到登录接口
                .excludePathPatterns("/login/verify");
        super.addInterceptors(registry);
    }
}

    其中用到的CookieUtil代码如下

package cn.test.util;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * cookie工具类
 */
public class CookieUtil {

    /**
     * 设置
     * @param response
     * @param name
     * @param value
     * @param maxAge
     */
    public static void set(HttpServletResponse response,
                           String name,
                           String value,
                           int maxAge) {
        Cookie cookie = new Cookie(name, value);
        cookie.setPath("/");
        cookie.setMaxAge(maxAge);
        response.addCookie(cookie);
    }

    /**
     * 获取cookie
     * @param request
     * @param name
     * @return
     */
    public static Cookie get(HttpServletRequest request,
                             String name) {
        Map<String, Cookie> cookieMap = readCookieMap(request);
        if (cookieMap.containsKey(name)) {
            return cookieMap.get(name);
        }else {
            return null;
        }
    }

    /**
     * 将cookie封装成Map
     * @param request
     * @return
     */
    private static Map<String, Cookie> readCookieMap(HttpServletRequest request) {
        Map<String, Cookie> cookieMap = new HashMap<>();
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            for (Cookie cookie: cookies) {
                cookieMap.put(cookie.getName(), cookie);
            }
        }
        return cookieMap;
    }
}

模拟登录代码如下

package cn.test.controllers;

import cn.test.util.CookieUtil;
import cn.test.util.JWTUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/login")
public class LoginController {

    @RequestMapping("/verify")
    public String verifyLogin(HttpServletResponse response){
        try {
            String token = JWTUtil.createToken(12345l);
            CookieUtil.set(response,"token",token,5 * 60 * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "success";
    }
}

整个流程是:用户第一次登录或者在没登录的情况下访问其他接口会经过登录拦截并强制跳转到登录接口,用户经过登录接口会形成token信息并放入到cookie中,下次再访问时会带上cookie,如果cookie还在有效时间内就可以通过拦截器的校验。

四.总结

    Spring Boot整合JWT主要作用是为了校验权限和数据是否被篡改,首先需要引入JWT的依赖,然后写JWT生成token和校验token的工具类,然后配合拦截器进行使用,这里要注意把拦截器通过@Configuration注解注册到Spring中。