基于 spring cloud 的简易 JWT 颁发与刷新构建方法

JWT 是一种通用的无状态的用于后端鉴权的字符串,在 spring cloud 中一般使用 jjwt 包,结合 spring cloud security 进行颁发与鉴权,但由于 spring cloud security 本身过重,如果仅仅只需将 token 用于鉴定请求是否合法,则并不需要使用 spring cloud security 以达到这一目的,只需使用 spring cloud gateway , 在 spring cloud gateway 中实现 GlobalFilter 接口,在这里面进行鉴定即可。

接下来看具体实现过程:

1. 首先,编写颁发 token 与解析 token

public class JWTUtil {
    // token 过期时间
    private static long expireTime = 12L * 60L * 60L * 1000;
    // 生成以及解析 token 的密钥
    private static String key = "123456789abcedfghijklmn";

    private static ObjectMapper objectMapper;

    public JWTUtil(ObjectMapper objectMapper){
        this.objectMapper = objectMapper;
    }

    // 创建 token 的静态方法
    public static String createToken(String subject){
        
        Map<String, Object> claims = new HashMap<>();
        // 创建私有声明
        claims.put("uid", "springGql");
        claims.put("user_name", "initialing");

        String token = Jwts.builder()
                            // 设置私有声明
                            .setClaims(claims)
                            // 设置 token 私有标识,可以是任意字符串
                            .setId("id")
                            // 设置 token 载荷,可以存入用户名等信息
                            .setSubject(subject)
                            // 设置颁发 token 时间, 这里设当前时间
                            .setIssuedAt(new Date())
                            // 设置过期时间
                            .setExpiration(new Date(System.currentTimeMillis() + expireTime))
                            // 设置加密算法和密钥
                            .signWith(SignatureAlgorithm.HS512, key)
                            .compact();
        return token;
    }

    // 解析 token 成 Claim 方便后续操作
    public static Claims parseJwt(String token) throws Exception {
        Claims claims = Jwts.parser()
                            .setSigningKey(key)
                            .parseClaimsJws(token)
                            .getBody();
        return claims;
    }

    // 获取 token body 内容
    public static JWTModel getModel(String token){
        try {
            Claims claims = parseJwt(token);
            String subject = claims.getSubject();
            JWTModel jwtModel = objectMapper.readValue(subject, JWTModel.class);
            return jwtModel;
        } catch (Exception e){
            return null;
        }
    }
}

上面代码中的 JWTModel 类是信息载荷类,可为以下样式:

public class JwtModel {

    public JwtModel(Integer id, String userName){
        this.userName = userName;
        this.id = id;
    }
    
    private Integer id;

    private String userName;

    public Integer getId(){
        return this.id;
    }
    
    public void setId(Integer id){
        this.id = id;
    }
    
    public String getUserName(){
        return this.userName;
    }
    
    public void setUserName(String userName){
        this.userName = userName;
    }
}

2. 在 login 调用方法内查询用户,颁发 token ,伪代码如下所示:

@RestController
@RefreshScope
public class TestController{

    private ObjectMapper objectMapper;
    
    public TestController(ObjectMapper objectMapper){
        this.objectMapper = objectMapper;
    }

    @GetMapping("/login")
    public CommonResult login(@RequestParam(value = "userName") String userName, 
                            @RequestParam(value = "password") String password, 
                            HttpServletResponse response){
        // 获取并对比用户信息的代码
        
        // 生成 JWTModel
        JWTModel jwtModel = new JWTModel(id, username);
        // 颁发 token 
        String token = JWTUtil.createToken(objectMapper.writeValueAsString(jwtModel));
        // 返回头添加 Access-Token 属性,存入 token 值
        response.addHeader("Access-Token", token);
        return new CommonResult(200, "success");
    }
}

上述代码中 CommonResultRESTFull 通用返回格式,可定义为

public class CommonResult<T> {
    private Integer code;
    private String message;
    private T data;
    public CommonResult(Integer code, String message, T data){
        this.code = code;
        this.message = message;
        this.data = data;
    }
    public CommonResult(Integer code, String message){
        this(code,message,null);
    }
}

*注:前端可对每次调用的返回头信息进行查询,如果有 Access-Token 信息,变将其存至本地,这里使用 axios 举例:

/**
* 创建axios实例
*/
const instance = axios.create({
    timeout: 60000,
    responseType: "json",
})
/**
* 统一返回拦截处理
*/
instance.interceptors.response.use(function(response){
    // 如果返回存在access-token,存入localStorage中
    if(response.headers["access-token"]){
        localStorage.setItem("token",response.headers["access-token"]);
    }
    return response;
    },function(error){
        return Promise.resolve(error);
    }
)
/**
* 统一请求拦截处理
*/
instance.interceptors.request.use(function(config){
    // 如果在本地存在 token 便传给后端
    let token = storage.getItem("token");
    if(token){
        config.headers["Authorization"] = token;
    }
        return config;
    },function(error){
        return Promise.reject(error);
    }
)

3. 最后一步就是使用 spring cloud gateway 来做后台统一判断请求是否合法,具体代码如下:

@Component
@Slf4j
public class TokenFilter implements GlobalFilter, Ordered {
    // 跳过鉴定的url
    private String[] skipAuthUrl = {
            "/login"
    };
    private ObjectMapper objectMapper;

    public TokenFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求地址
        String url = exchange.getRequest().getURI().getPath();     
        // 如果在跳过列表里,则不进行鉴定
        if(skipAuthUrl != null && (Arrays.asList(skipAuthUrl).contains(url))){
            return chain.filter(exchange);
        }
        // 获取token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        ServerHttpResponse response = exchange.getResponse();
        try {
            JwtUtil.parseJwt(token);
            return chain.filter(exchange);
        } catch (ExpiredJwtException e){
            if(e.getMessage().contains("Allowed clock skew")){
                Date et = e.getClaims().getExpiration();
                long exMillis = et.getTime();
                long nowMillis = System.currentTimeMillis();
                // 过期时间在2天内自动刷新token
                if(nowMillis - exMillis < 1000L * 60L * 60L * 24L * 2L){
                    String subject = e.getClaims().getSubject();
                    try {
                        // 设置刷新时间为8小时
                        String newJwt = JwtUtil.createJwt(subject);
                        // 返回头设置新 token
                        response.getHeaders().add("Access-Token",newJwt);
                        ServerHttpRequest req = exchange.getRequest().mutate().header("Authorization", newJwt).build();
                        exchange = exchange.mutate().request(req).build();
                    } catch (Exception ne){
                        log.error("******** 刷新token失败"+ne.getMessage(),ne);
                    }
                    return chain.filter(exchange);
                }
                log.error("******** 过期"+e.getMessage(),e);
                return reqReject(response,"认证过期");
            }else{
                return reqReject(response,"认证失败");
            }
        } catch (Exception e) {
            log.error(e.getMessage(),e);
            return reqReject(response,"认证失败");
        }

    }
    
    private Mono<Void> reqReject(ServerHttpResponse response, String message){
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
        CommonResult result = new CommonResult(403,message);
        String returnStr = "";
        try{
            returnStr = objectMapper.writeValueAsString(result);
        } catch (JsonProcessingException e){
            log.error(e.getMessage(),e);
        }
        DataBuffer buf = response.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        Mono<Void> vm = response.writeWith(Flux.just(buf));
        return vm;
    }

    @Override
    public int getOrder(){
        return -1;
    }
}

以上就是利用 jjwt 包进行 token 颁发与解析的具体过程,该过程可轻量化的产出 token 返回给前端,并通过实现 GlobalFilter 方法,在 spring cloud gateway 中进行 token 守卫,拦截 token 过期或无 token 的请求,并且在过期一定时间内刷新 token


作者:勾勒两只企鹅
链接:基于 spring cloud 的简易 JWT 颁发与刷新构建方法 - 掘金