JWT认证方式的实现

JWT全称Json Web Token, 是当今较为流行的一种认证方式。相比于传统的session认证方式,jwt在设计之初便强调“无状态”,即服务端不会存储任何的认证信息,只扮演一个签发令牌的角色。
本文着重阐述一下具体的实现方法。

基本流程

1,首先在服务器收到登录的请求时,会检查请求头中是否含有令牌(显而易见的是,首次登录必然没有)。服务器会在验证账号密码都通过的情况下,生成一个token(即令牌),添加到响应头中返回给客户端。
2,客户端在收到token之后,之后的每一个请求都需要在请求header中带上这个token用以验证身份。
3,当token失效时,服务器通知客户端令牌过期,重新发送登录请求获取新的token。

需要的依赖


	com.auth0
	java-jwt
	3.1.0

生成token

public static String getToken(final String userId,final String role) {
        String token = null;
        try {
            Long currentTime = System.currentTimeMillis();
            String tokenId = new StringBuilder(String.valueOf(currentTime)).append(userId).toString();
            Date expiresAt = new Date(currentTime + 10L * 60L * 1000L);
            token = JWT.create()
            		// 发行者
                    .withIssuer("")
                    // 自定义属性
                    .withClaim("userId", userId)
                    .withClaim("role",role)
                    // 签发时间
                    .withIssuedAt(new Date(currentTime))
                    // 唯一id
                    .withJWTId(tokenId)
                    // token过期时间
                    .withExpiresAt(expiresAt)
                    // 使用了HMAC256加密算法。
                    .sign(Algorithm.HMAC256("SECRET_KEY"));
        } catch (JWTCreationException exception){
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return token;
    }

其中.sign(Algorithm.HMAC256(“SECRET_KEY”))中使用HMAC256方法对服务端设置的一个key进行加密,防止token被伪造。因此该key务必妥善保存,一旦泄露,意味着客户端可以任意签发token。

校验token

public static Boolean decryptToken(final String token) {
        if (token == null){
            return false;
        }
        DecodedJWT jwt = null;
        try {
            // 使用了HMAC256加密算法。
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(GlobalConstant.SECRET_KEY))
                    .withIssuer("")
                    .build(); //Reusable verifier instance
            // 对字段进行校验,超时抛出JWTVerificationException类的子类InvalidClaimException
            jwt = verifier.verify(token);
        } catch (JWTVerificationException exception){
            //Invalid signature/claims
            return false;
        } catch (IllegalArgumentException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnsupportedEncodingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return true;
    }

verifier.verify(token)中已经对签名是否超时进行了校验,超时会抛出InvalidClaimException,附上一段源码:

private void assertValidDateClaim(Date date, long leeway, boolean shouldBeFuture) {
        Date today = this.clock.getToday();
        today.setTime((long)Math.floor((double)(today.getTime() / 1000L * 1000L)));
        boolean isValid;
        String errMessage;
        if (shouldBeFuture) {
            today.setTime(today.getTime() - leeway * 1000L);
            isValid = date == null || !today.after(date);
            errMessage = String.format("The Token has expired on %s.", date);
        } else {
            today.setTime(today.getTime() + leeway * 1000L);
            isValid = date == null || !today.before(date);
            errMessage = String.format("The Token can't be used before %s.", date);
        }

        if (!isValid) {
            throw new InvalidClaimException(errMessage);
        }
    }

发送与接收token

	@ApiOperation("登录")
    @PostMapping("login")
    public ResponseData login(@RequestBody Account account, HttpServletRequest request, HttpServletResponse response){
    	// 获取token
        String token = request.getHeader("Authorization");
        //token为空,创建token
        if (token == null){
        
        	// 校验账号密码逻辑
        	// ...
        	//校验账号密码逻辑
        	
        	//发送token
			response.addHeader("Authorization",getToken(user.getUserId(), user.getRole()));
		}else {
            //校验token
            if(decryptToken(token)){
                // 已登录的状态
            }else {
                // token过期,重新登录
            }
        }
    }

以上代码就可以基本实现登录认证了,除此之外还有需要注意的一点,在前端接收返回的token时,会提示Refused to get unsafe header “Authorization”,这是因为Authorization的header是我们自己定义的,解决方法是在跨域处理部分加上Access-Control-Expose-Headers。

        response.setHeader("Access-Control-Expose-Headers","Authorization");

对于token过期的思考

使用jwt的认证方式易于扩展,但有人也许会提出疑问,当用户执行登出操作后,或者用户修改密码后,当前的token理应被失效,然而jwt似乎无法立即将当前的token失效。而倘若采用服务器记录每一个token的方式,则又与jwt的设计初衷相违背。浏览了国内外的一些做法,较为常见的是设置一个黑名单列表,将弃用的token加入到黑名单中。
实现一个黑名单的方法十分简单,redis的特性可以完美适应这个需求,当用户登出后,我们直接将设个token的tokenId作为redis键值对的key,token设置为value,token的超时时间设置为过期时间,从而避免了使用时间上未过期但已被弃用的token进行访问的问题。

你可能感兴趣的:(Java学习)