JMS平台Spring Boot

Spring boot-手把手教你使用Token

2020-11-21  本文已影响0人  Renaissance_

在进行接口开发的过程中,为了保证接口调用的安全性,需要对客户端的请求进行鉴权,此文将介绍在Spring boot项目中如何使用拦截器和JWT进行token校验和获取。

背景

概念

  1. 客户端向服务端请求Token,并上送自己的clientId和clientSecret(客户端和服务端双方约定)。
  2. 服务端校验客户端上送的clientId和clientSecret是否合法,如果合法则返回Token,并赋予Token一个有效期,比如2个小时。
  3. 客户端将Token放到本地本地缓存中,比如浏览器中的cookie中。
  4. 客户端向客户端发起新的业务请求,并在请求头中带上Token。
  5. 服务端校验请求头中的Token是否正确,如果正确则进行业务逻辑处理并返回结果,如果失败,则拒绝访问。
  6. 如果Token失效,则客户端重新请求Token。
image.png

JWT

组成

JWT由三部分组成,由类型和加密算法的head(头部),包含公共信息和自定义信息的playboard(负载),以及signature(签名)组成。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIzOTU4MTU4MDEiLCJleHAiOjE2MDYxMDc5NTEsImlhdCI6MTYwNjEwMDc1MX0.xai-cVqrbfFeb3TNgVkHXYiT47sXUK76D47QLZecsqg
头部

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 就是头部信息,这是由base64加密后的密文,base64是一种对称加密算法,解密后的json格式如下。头部信息由type(类型)和 alg(加密算法)组成。类型就是"JWT",加密算法一般使用 HMAC SHA256加密算法。

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

eyJhdWQiOiIzOTU4MTU4MDEiLCJleHAiOjE2MDYxMDc5NTEsImlhdCI6MTYwNjEwMDc1MX0 就是负载信息,加密后的json格式如下。负载信息一般由标准申明,公共声明,私有声明组成。

{
    "aud":"395815801",
    "exp":1606107951,
    "iat":1606100751
}

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明和私有的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

签名

第三部分签名是由base64加密后的头部信息和负载信息以及secret组成的签名,签名算法是有头部信息定以的加密算法,一般是HMAC SHA256。然后头部,负载,签名三部分组成了token。

获取Token

   // token-有效时间 键值对
    private static MutablePair<String, Long> authPair = MutablePair.of(null, 0L);

  /**
   * 获取authorization
     * @return
     */
    private static String getAuthorization(){
        String cacheToken = authPair.getLeft();
        Long expiresMill = authPair.getRight();
        long currentTimeMillis = System.currentTimeMillis();

        // 过期 或 token为空 刷新
        String token = null;
        if (currentTimeMillis >= expiresMill || StringUtils.isEmpty(cacheToken)) {
            TokenDTO tokenDTO = queryToken();
            if (ObjectUtil.isNotNull(tokenDTO)) {
                // 比第三方的过期时间早30s刷新
                int availableTime = tokenDTO.getTokenAvailableTime() - 30;
                long newExpiresMill = currentTimeMillis + (availableTime * 1000);
                authPair.setLeft(tokenDTO.getAccessToken());
                authPair.setRight(newExpiresMill);
                token = authPair.getLeft();
            }
        } else {
            token = cacheToken;
        }

        return StrUtil.isEmpty(token) ? null : "Bearer " + token;
    }

提供Token

pom.xml引入依赖

      <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

新增controller,提供token接口

     @Autowired
     CallbackService callbackService;

    /**
     * 获取token
     * @param requestDTO
     * @return
     */
    @PostMapping("/api/callback/query_token")
    public ResultDataDTO queryToken(@RequestBody RequestDTO<String> requestDTO) {
        ResultDataDTO resultDataDTO = callbackService.queryToken(request.getData());
        return resultDataDTO;
    }

CallbackService 生成token

 /**
     * token有效时间默认2小时,单位(ms)
     */
    private static final Integer tokenAvailableTime = 60 * 60 * 1000 * 2;

    @Override
    public ResultDataDTO queryToken(OperatorDTO operatorDTO) {
        String token = null;
        // 单位(s)
        Integer availableTime = tokenAvailableTime / 1000;
        long endTimeMill = System.currentTimeMillis() + tokenAvailableTime;
        // 有效起始
        Date validStartDate = new Date();
        // 有效结束
        Date validEndDate = new Date(endTimeMill);

       token = JWT.create()
                    .withAudience(operatorDTO.getClientID())
                    .withIssuedAt(validStartDate)
                    .withExpiresAt(validEndDate)
                    .sign(Algorithm.HMAC256(operatorDTO.getClientID()));

        TokenDTO tokenDTO = TokenDTO.builder()
                .AccessToken(token)
                .OperatorID(operatorDTO.getClientID())
                .TokenAvailableTime(availableTime)
                .build();

         return ResultDataDTO.success(tokenDTO);
    }

返回的示例如下

{
    "TokenAvailableTime":7200,
    "OperatorID":"395815801",
"AccessToken":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIzOTU4MTU4MDEiLCJleHAiOjE2MDYxMDc5NTEsImlhdCI6MTYwNjEwMDc1MX0.xai-cVqrbfFeb3TNgVkHXYiT47sXUK76D47QLZecsqg"
}

校验Token

定义自定义注解,在需要token校验的方法上加上即可

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckToken {

    boolean required() default true;
    // 组织机构代码
    String clientId();
}

新增AuthenticationInterceptor对第三方请求进行拦截,实现HandlerInterceptor接口

@Slf4j
public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    Environment environment;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authorization = request.getHeader("Authorization");
        String token = StrUtil.sub(authorization, StrUtil.length("Bearer "), StrUtil.length(authorization));
        // 如果不是映射方法直接通过
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 判断是否需要进行校验token
        if(method.isAnnotationPresent(CheckToken.class)){
            CheckToken checkToken = method.getAnnotation(CheckToken.class);
            if(checkToken.required()){
                if(ObjectUtil.isNull(token)){
                    throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.TOKEN_ERROR.getCode(),
                            RetCodeEnum.TOKEN_ERROR.getName()));
                }
                // 验证token
                String clientId = environment.resolvePlaceholders(checkToken.clientId());
                JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(clientId)).build();
                try {
                    jwtVerifier.verify(token);
                } catch (JWTVerificationException e){
                    throw Exceptions.fail(ErrorMessage.errorMessage(RetCodeEnum.TOKEN_ERROR.getCode(),
                            RetCodeEnum.TOKEN_ERROR.getName()));
                }
            }
        }

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

注册AuthenticationInterceptor拦截器,对指定请求路径进行拦截

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(authenticationInterceptor()).addPathPatterns("/api/callback/**");
    }

    @Bean
    public AuthenticationInterceptor authenticationInterceptor(){
        return new AuthenticationInterceptor();
    }
}
    @CheckToken(clientId = "${client.id}")
    @PostMapping("/api/callback/notification_result")
    public ResultDataDTO notifyStartChargeResult(@RequestBody RequestDTO<String> requestDTO){
        return callbackService.notifyResult(request.getData());
    }

总结

在此文中,我们大致了解了Token的定义,获取,校验等方法。此外,Token 的无状态,可扩展性,多平台跨域等特性,也让Token广泛应用在安全校验领域中。在接下来的几篇文章中,我将介绍如何使用Spring AOP进行加密,解密,验签等操作。

参考:
https://www.jianshu.com/p/576dbf44b2ae

上一篇下一篇

猜你喜欢

热点阅读