SpringBoot JWT实现token登录

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

SpringBoot JWT实现token登录

雨夜归人93   2022-05-22 我要评论

1. 什么是JWT

Json web token (JWT) 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。简答理解就是一个身份凭证,用于服务识别。
JWT本身是无状态的,这点有别于传统的session,不在服务端存储凭证。这种特性使其在分布式场景,更便于扩展使用。

2. JWT组成部分

JWT有三部分组成,头部(header),载荷(payload),是签名(signature)。

  • 头部

头部主要声明了类型(jwt),以及使用的加密算法( HMAC SHA256)

  • 载荷

载荷就是存放有自定义信息的地方,例如用户标识,截止日期等

  • 签名

签名进行对之前的数据添加一层防护,防止被篡改。
签名生成过程: base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密。

// base64加密后的header和base64加密后的payload使用.连接组成的字符串
String str=base64(header).base64(payload);
// 加盐secret进行加密
String sign=HMACSHA256(encodedString, 'secret');

3. JWT加密方式

jwt加密分为两种对称加密和非对称加密。

  • 对称加密

对称加密指使用同一秘钥进行加密,解密的操作。加密解密的速度比较快,适合数据比较长时的使用。常见的算法为DES、3DES等

  • 非对称加密

非对称指通过公钥进行加密,通过私钥进行解密。加密和解密花费的时间长、速度相对较慢,但安全性更高,只适合对少量数据的使用。常见的算法RSA、ECC等。
两种加密方法没有谁更好,只有哪种场景更合适。

4.实战

本例采用了spring2.x,jwt使用了nimbus-jose-jwt版本,当然其他的jwt版本也都类似,封装的都是不错的。

1.maven关键配置如下

<dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>9.12.1</version>
        </dependency>
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.72</version>
        </dependency>

2.jwt工具类

对于这里的秘钥:采用了userId+salt+uuid的方式保证,即使是同一个用户每次生成的serect都是不同的

对于校验token有效性,包含三个过程:

  • 格式是否合法
  • token是否在有效期内
  • token是否在刷新的有效期内

对于token超过有效期,但在刷新有效期内,返回特定的code,前端进行识别,发起请求刷新token,达到用户无感知的过程。

public class JwtUtil {
    private static final Logger log = LoggerFactory.getLogger(JwtUtil.class);

    private static final String BEARER_TYPE = "Bearer";
    private static final String PARAM_TOKEN = "token";
    /**
     * 秘钥
     */
    private static final String SECRET = "dfg#fh!Fdh3443";
    /**
     * 有效期12小时
     */
    private static final long EXPIRE_TIME = 12 * 3600 * 1000;
    /**
     * 刷新时间7天
     */
    private static final long REFRESH_TIME = 7 * 24 * 3600 * 1000;


    public static String generate(PayloadDTO payloadDTO)  {
        //创建JWS头,设置签名算法和类型
        JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256)
                .type(JOSEObjectType.JWT)
                .build();
        //将负载信息封装到Payload中
        Payload payload = new Payload(JSON.toJSONString(payloadDTO));
        //创建JWS对象
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);
        try {
            //创建HMAC签名器
            JWSSigner jwsSigner = new MACSigner(payloadDTO.getUserId() + SECRET+payloadDTO.getJti());
            //签名
            jwsObject.sign(jwsSigner);
            return jwsObject.serialize();
        } catch (JOSEException e) {
            log.error("jwt生成器异常",e);
            throw new BizException(TOKEN_SIGNER);
        }
    }


    public static String freshToken(String token)   {
        PayloadDTO payloadDTO;
        try {
            //从token中解析JWS对象
            JWSObject jwsObject = JWSObject.parse(token);
            payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class);
            // 校验格式是否合适
            verifyFormat(payloadDTO, jwsObject);
        }catch (ParseException e) {
            log.error("jwt解析异常",e);
            throw new BizException(TOKEN_PARSE);
        } catch (JOSEException e) {
            log.error("jwt生成器异常",e);
            throw new BizException(TOKEN_SIGNER);
        }
        // 校验是否过期,未过期直接返回原token
        if (payloadDTO.getExp() >= System.currentTimeMillis()) {
            return token;
        }
        // 校验是否处于刷新时间内,重新生成token
        if (payloadDTO.getRef() >= System.currentTimeMillis()) {
            getRefreshPayload(payloadDTO);
            return generate(payloadDTO);
        }
        throw new BizException(TOKEN_EXP);
    }



    private static void verifyFormat(PayloadDTO payloadDTO, JWSObject jwsObject) throws JOSEException {
        //创建HMAC验证器
        JWSVerifier jwsVerifier = new MACVerifier(payloadDTO.getUserId() + SECRET+payloadDTO.getJti());
        if (!jwsObject.verify(jwsVerifier)) {
            throw new BizException(TOKEN_ERROR);
        }
    }


    public static String getTokenFromHeader(HttpServletRequest request) {
        // 先从header取值
        String value = request.getHeader("Authorization");
        if (!StringUtils.hasText(value)) {
            // header不存在从参数中获取
            value = request.getParameter(PARAM_TOKEN);
            if (!StringUtils.hasText(value)) {
                throw new BizException(TOKEN_MUST);
            }
        }
        if (value.toLowerCase().startsWith(BEARER_TYPE.toLowerCase())) {
            return value.substring(BEARER_TYPE.length()).trim();
        }
        return value;
    }


    public static PayloadDTO verify(String token)  {
        PayloadDTO payloadDTO;
        try {
            //从token中解析JWS对象
            JWSObject jwsObject = JWSObject.parse(token);
            payloadDTO = JSON.parseObject(jwsObject.getPayload().toString(), PayloadDTO.class);
            // 校验格式是否合适
            verifyFormat(payloadDTO, jwsObject);
        }catch (ParseException e) {
            log.error("jwt解析异常",e);
            throw new BizException(TOKEN_PARSE);
        } catch (JOSEException e) {
            log.error("jwt生成器异常",e);
            throw new BizException(TOKEN_SIGNER);
        }
        // 校验是否过期
        if (payloadDTO.getExp() < System.currentTimeMillis()) {
            // 校验是否处于刷新时间内
            if (payloadDTO.getRef() >= System.currentTimeMillis()) {
                throw new BizException(TOKEN_REFRESH);
            }
            throw new BizException(TOKEN_EXP);
        }
        return payloadDTO;
    }

    public static PayloadDTO getDefaultPayload(Long userId) {
        long currentTimeMillis = System.currentTimeMillis();
        PayloadDTO payloadDTO = new PayloadDTO();
        payloadDTO.setJti(UUID.randomUUID().toString());
        payloadDTO.setExp(currentTimeMillis + EXPIRE_TIME);
        payloadDTO.setRef(currentTimeMillis + REFRESH_TIME);
        payloadDTO.setUserId(userId);
        return payloadDTO;

    }

    public static void getRefreshPayload(PayloadDTO payload) {
        long currentTimeMillis = System.currentTimeMillis();
        payload.setJti(UUID.randomUUID().toString());
        payload.setExp(currentTimeMillis + EXPIRE_TIME);
        payload.setRef(currentTimeMillis + REFRESH_TIME);
    }
}

3.权限拦截
本例中采用了自定义注解+切面的方式来实现token的校验过程。
自定义Auth注解提供了是否开启校验token,sign的选项,实际操作中可以添加更多的功能。

@Target(value = ElementType.METHOD)
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Auth {
    /**
     * 是否校验token,默认开启
     */
    boolean token() default true;

    /**
     * 是否校验sign,默认关闭
     */
    boolean sign() default false;
}

切面部分指定了对Auth进行切面,这种方法比采用拦截器方式更加灵活些。

@Component
@Aspect
public class AuthAspect {
    @Autowired
    private HttpServletRequest request;

    @Pointcut("@annotation(com.rain.jwt.config.Auth)")
    private void authPointcut(){}

    @Around("authPointcut()")
    public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取目标对象对应的字节码对象
        Class<?> targetCls=joinPoint.getTarget().getClass();
        //获取方法签名信息从而获取方法名和参数类型
        MethodSignature ms= (MethodSignature) joinPoint.getSignature();
        //获取目标方法对象上注解中的属性值
        Auth auth=ms.getMethod().getAnnotation(Auth.class);
        // 校验签名
        if (auth.token()) {
            String token = JwtUtil.getTokenFromHeader(request);
            JwtUtil.verify(token);
        }
        // 校验签名
        if (auth.sign()) {
            // todo
        }
        return joinPoint.proceed();
    }
}

4.测试接口

@RestController
@RequestMapping(value="/user")
@Api(tags = "用户")
public class UserController {


    @PostMapping(value = "/login")
    @Auth(token = false)
    @ApiOperation("登录")
    public Result<String> login(String username,String password) {
        // 用户常规校验
        Long userId = 100L;
        // 用户信息存入缓存
        // 生成token
        String token = JwtUtil.generate(JwtUtil.getDefaultPayload(userId));
        return Result.success(token);
    }

    @GetMapping(value = "refreshToken")
    @Auth
    @ApiOperation("刷新token")
    public Result<String> refreshToken(String token) {
        String freshToken = JwtUtil.freshToken(token);
        return Result.success(freshToken);
    }

    @GetMapping(value = "test")
    @Auth
    @ApiOperation("测试")
    public Result<String> test() {
        return Result.success("测试成功");
    }
}

5.总结

许多同学使用jwt经常将获取到的token放在redis中,在服务器端控制其有效性。这是一种处理token的方式,但这种方式跟jwt的思路是背道而去的,jwt本身就提供了过期的信息,将token的生命周期放入服务器中,又何必采用jwt的方式呢?直接来个uuid不香么。
最后来个项目地址

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们