JWT的使用:Spring Cloud微服务接口鉴权

0 JWT是什么

JWT(JSON Web Token)是一种开放标准,它以一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。
其认证原理是,客户端向服务器申请授权,服务器认证以后,生成一个token字符串并返回给客户端,此后客户端在请求受保护的资源时携带这个token,服务端进行验证再从这个token中解析出用户的身份信息。

0.1 JWT的结构

一个JWT是一个字符串,其由Header(头部)、Payload(负载)和Signature(签名)三个部分组成,中间以.号分隔,其格式为Header.Payload.Signature,如下所示:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJNSU5HIiwiZXhwIjoxNTQ3MzQ1MjgxLCJ1c2VyTmFtZSI6ImFkbWluIiwiaWF0IjoxNTQ3MzQ1MjIxfQ.
NfsnnoORftR4oP7hFHjmDgj5HYxKd-RXjEW9upn9Tgk

Header
Header本质是一个JSON对象,由包含了两个属性:

{
  "alg": "HS256",
  "typ": "JWT"
}

alg表示签名的算法,typ则表示token的类型。然后,将Header进行Base64URL 编码转成字符串作为JWT的第一组成部分。
Payload
JWT的第二组成部分被称作载荷,其本质也是一个JSON对象,用来存放认证所需的一些额外声明,其包含有一种类型:标准中注册的声明(registered),公共的声明(public), 和私有的声明(private claims),其中公有声明和私有声明可自定义字段。
标准中注册的声明 (建议但不强制使用) :

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

仅有的声明和私有的声明可以添加一些额外的用户属性,例如:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

由于JWT 是不加密的,所以不应该把用户敏感类信息放在这个部分。
Payload部分进行Base64URL 编码转成字符串,即为JWT的第二组成部分。

Signature
JWT的第三组成部分是签名,它是对前面两个部分的签名。比如,如果JWT中Header指定的算法是 MAC SHA256,那么Signature为,其中secret加解密使用的密钥:

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

关于JWT更详细的介绍,可到官网查看:https://jwt.io/introduction/

0.2 JWT的使用场景

由于JWT传输过程中可携带额外的信息(如用户信息),使用JWT后,服务端不再需要保存用户的状态信息,服务端需要保存的只有加解密使用的密钥。由于JWT的无状态特性(不需存储),在一个分布式的面向服务的框架中,使用JWT做接口鉴权是再适合不过的了。

1 JWT用作接口鉴权

由于JWT的天然无状态,在JWT生成的一刻,其过期时间就已经注定了,并且不可更改,所以,如果要对JWT实现token续签的话,一般的做法是额外生成一个refreshToken用于获取新token,refreshToken需存储于服务端,其过期时间可以比JWT的过期时间要稍长。
在Spring Cloud微服务框架下,要实现接口的鉴权,最好的方式就是通过Spring Cloud Gateway网关服务来完成。网关提供了统一服务的访问接口,客户端在调用服务时,须先经过网关,网关进行权限的校验,对非法请求进行过滤后再进行请求的转发,以此来完成鉴权。
接下来,我们通过在网关中使用JWT实现一个简单的鉴权服务。

其授权基本流程:
1 用户提交账号密码,服务生成token和refreshToken并返回,其中refreshToken设置过期时间,并关联用户身份和当前的token;
2 用户请求服务时,前置网关对请求进行过滤,从请求中取出token,在验证通过后将token中包含的身份信息作为参数所加到当前请求;
3 用户请求到达具体的服务后,取出包含的身份信息,进行具体的业务处理;

token的刷新流程:
1 用户携带refreshToken参数请求token刷新接口,服务端在判断refreshToken未过期后,取出关联的用户信息和当前token,使用当前用户信息重新生成token,并将旧的token置于黑名单中,返回新的token;
2 用户携带新token重新进行请求;

该示例中JWT的实现使用了开源的Java JWT,地址为:https://github.com/auth0/java-jwt

1.1 代码实现

1.1.1 登录授权:token的生成
	public Map<String,Object> login(@RequestParam String userName,
                                    @RequestParam String password){
        Map<String,Object> resultMap = new HashMap<>();
        //账号密码校验
        if(StringUtils.equals(userName, "admin")&&
                StringUtils.equals(password, "admin")){

            //生成JWT
            String token = buildJWT(userName);
            //生成refreshToken
            String refreshToken = UUID.randomUUID().toString().replaceAll("-","");
            //保存refreshToken至redis,使用hash结构保存使用中的token以及用户标识
            String refreshTokenKey = String.format(jwtRefreshTokenKeyFormat, refreshToken);
            stringRedisTemplate.opsForHash().put(refreshTokenKey,
                    "token", token);
            stringRedisTemplate.opsForHash().put(refreshTokenKey,
                    "userName", userName);
            //refreshToken设置过期时间
            stringRedisTemplate.expire(refreshTokenKey,
                    refreshTokenExpireTime, TimeUnit.MILLISECONDS);
            //返回结果
            Map<String, Object> dataMap = new HashMap<>();
            dataMap.put("token", token);
            dataMap.put("refreshToken", refreshToken);
            resultMap.put("code", "10000");
            resultMap.put("data", dataMap);
            return resultMap;
        }
        resultMap.put("isSuccess", false);
        return resultMap;
    }
    private String buildJWT(String userName){
        //生成jwt
        Date now = new Date();
        Algorithm algo = Algorithm.HMAC256(secretKey);
        String token = JWT.create()
                .withIssuer("MING")
                .withIssuedAt(now)
                .withExpiresAt(new Date(now.getTime() + tokenExpireTime))
                .withClaim("userName", userName)//保存身份标识
                .sign(algo);
        return token;
    }
1.1.2 token的刷新
public Map<String,Object> refreshToken(@RequestParam String refreshToken){
        Map<String,Object> resultMap = new HashMap<>();
        String refreshTokenKey = String.format(jwtRefreshTokenKeyFormat, refreshToken);
        String userName = (String)stringRedisTemplate.opsForHash().get(refreshTokenKey,
                "userName");
        if(StringUtils.isBlank(userName)){
            resultMap.put("code", "10001");
            resultMap.put("msg", "refreshToken过期");
            return resultMap;
        }
        String newToken = buildJWT(userName);
        //替换当前token,并将旧token添加到黑名单
        String oldToken = (String)stringRedisTemplate.opsForHash().get(refreshTokenKey,
                "token");
        stringRedisTemplate.opsForHash().put(refreshTokenKey, "token", newToken);
        stringRedisTemplate.opsForValue().set(String.format(jwtBlacklistKeyFormat, oldToken), "",
                tokenExpireTime, TimeUnit.MILLISECONDS);
        resultMap.put("code", "10000");
        resultMap.put("data", newToken);
        return resultMap;
    }
1.1.3 网关鉴权:token的校验
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter.class);

    @Value("${jwt.secret.key}")
    private String secretKey;

    @Value("${auth.skip.urls}")
    private String[] skipAuthUrls;

    @Value("${jwt.blacklist.key.format}")
    private String jwtBlacklistKeyFormat;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();
        //跳过不需要验证的路径
        if(Arrays.asList(skipAuthUrls).contains(url)){
            return chain.filter(exchange);
        }
        //从请求头中取出token
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        //未携带token或token在黑名单内
        if (token == null ||
                token.isEmpty() ||
                    isBlackToken(token)) {
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = "{\"code\": \"401\",\"msg\": \"401 Unauthorized.\"}"
                    .getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        //取出token包含的身份,用于业务处理
        String userName = verifyJWT(token);
        if(userName.isEmpty()){
            ServerHttpResponse originalResponse = exchange.getResponse();
            originalResponse.setStatusCode(HttpStatus.OK);
            originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            byte[] response = "{\"code\": \"10002\",\"msg\": \"invalid token.\"}"
                    .getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
            return originalResponse.writeWith(Flux.just(buffer));
        }
        //将现在的request,添加当前身份
        ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization-UserName", userName).build();
        ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
        return chain.filter(mutableExchange);
    }

    /**
     * JWT验证
     * @param token
     * @return userName
     */
    private String verifyJWT(String token){
        String userName = "";
        try {
            Algorithm algorithm = Algorithm.HMAC256(secretKey);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer("MING")
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            userName = jwt.getClaim("userName").asString();
        } catch (JWTVerificationException e){
            LOGGER.error(e.getMessage(), e);
            return "";
        }
        return userName;
    }

    /**
     * 判断token是否在黑名单内
     * @param token
     * @return
     */
    private boolean isBlackToken(String token){
        assert token != null;
        return stringRedisTemplate.hasKey(String.format(jwtBlacklistKeyFormat, token));
    }
}

完整代码见:GitHub

参考链接:
http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值