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

1,783 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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;
    }
}
  1. 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);
    }
)
  1. 最后一步就是使用 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