JWT -- Json Web token

该文介绍了一种使用JWT(JSON Web Token)在分布式系统中进行身份验证的方法。通过引入Auth0的java-jwt库,创建、验证和管理JWT。配置了JWT的密钥和有效期,自定义了异常处理和拦截器,确保了Token的有效性和安全性。在客户端,通过拦截器验证和更新Token,实现了与认证中心的交互。
摘要由CSDN通过智能技术生成

JWT 的背景知识可以看这篇文章: JSON Web Token 入门教程

JWT 由三个部分组成:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

在分布式系统下,存在跨session的问题,则使用JWT实现分布式下,身份验证的功能

JWT有很多的第三方实现,Java JWT 类库对比及使用 - 简书

此处我们选择Auth0,此案例实现的功能如下

 认证中心

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

配置:

server:
  port: 8080

jwt:
  secret: "123321" #私钥
  expireTime: 1 #JWT有效期,单位是分钟
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {
    private String secret;
    private int expireTime;
}

自定义异常

public class TokenUnavailableException extends RuntimeException{
    public TokenUnavailableException(String message) {
        super(message);
    }
}

import com.lb.bean.JwtResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class ExceptionAdvice {

    @ExceptionHandler(TokenUnavailableException.class)
    private ResponseEntity<Object> handlerTokenUnavailableException(TokenUnavailableException e) {
        return new ResponseEntity<>(JwtResponse.error(e.getMessage()), HttpStatus.BAD_REQUEST);
    }
}

JWT - 生成,检验

package com.lb.config;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.lb.bean.JwtResponse;
import com.lb.exception.TokenUnavailableException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Map;

@Component
public class JwtService {

    @Autowired
    JwtProperties jwtProperties;

    public String createToken(String subject, Map<String, String> claimMap) {
        JWTCreator.Builder builder = JWT.create()
//                .withIssuer("")
//                .withNotBefore(null)
//                .withIssuedAt(null)
//                .withAudience("")
                .withSubject(subject) //主题
                .withExpiresAt(Date.from(
                        LocalDateTime.now().plusMinutes(jwtProperties.getExpireTime())
                                .atZone(ZoneId.systemDefault()).toInstant())
                );
        claimMap.forEach(builder::withClaim);
        return builder.sign(Algorithm.HMAC256(jwtProperties.getSecret()));
    }

    public JwtResponse validateToken(String token) {
        try {
            JWTVerifier jwtVerifier =
                    JWT.require(Algorithm.HMAC256(jwtProperties.getSecret()))
                            .build();
            DecodedJWT decodedJwt = jwtVerifier.verify(token);

            return JwtResponse.ok(decodedJwt.getClaims());
        } catch (Exception e) {
            //校验失败
            throw new TokenUnavailableException(e.getMessage());
        }
    }

    public boolean isNeedUpdate(String token) {

        try {
            Date expiresAt = JWT.require(Algorithm.HMAC256(jwtProperties.getSecret()))
                    .build()
                    .verify(token)
                    .getExpiresAt();
            // 需要更新 时间小于一半
            return (expiresAt.getTime() - System.currentTimeMillis()) < ((jwtProperties.getExpireTime() * 60000) >> 1);
        } catch (TokenExpiredException e) {
            return true;
        } catch (Exception e) {
            //校验失败
            throw new TokenUnavailableException(e.getMessage());
        }
    }
}

controller

@RestController
public class JwtController {
    @Autowired
    private JwtService jwtService;

    @PostMapping("create")
    public ResponseEntity<String> generateJwtToken(@RequestBody JwtRequest request) {
        String token = jwtService.createToken(request.getSubject(), request.getClaimsMap());
        return ResponseEntity.ok(token);
    }

    @PostMapping("validate")
    public ResponseEntity<JwtResponse> validateToken(@RequestParam String token) {
        JwtResponse response = jwtService.validateToken(token);
        return ResponseEntity.ok(response);
    }

    @PostMapping("expire")
    public ResponseEntity<Boolean> expire(@RequestParam String token) {
        return ResponseEntity.ok(jwtService.isNeedUpdate(token));
    }
}

客户端模块

请求认证中心,生成token,验证token,更新token

自定义注解,跳过JWT验证的API

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
    boolean required() default true;
}

自定义拦截器,验证JWT

package com.lb.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lb.annotation.PassToken;
import com.lb.bean.JwtResponse;
import com.lb.exception.JwtException;
import com.lb.util.JwtHttpEntityUtil;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;


public class JwtAuthenticationInterceptor implements HandlerInterceptor {
    private static ObjectMapper MAPPER = new ObjectMapper();
    @Resource(name = "facePlusRestTemplate")
    RestTemplate restTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果不是映射到方法直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //检查时候包含passtoken注解
        if (method.isAnnotationPresent(PassToken.class)) {
            PassToken passToken = method.getAnnotation(PassToken.class);
            if (passToken.required()) {
                return true;
            }
        }
        String token = request.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            throw new JwtException(HttpStatus.UNAUTHORIZED, "token 不存在");
        }
        HttpEntity formEntity = JwtHttpEntityUtil.getValidateEntity(token);
        ResponseEntity<String> stringResponseEntity = restTemplate.postForEntity("http://localhost:8080/validate", formEntity, String.class);
        if (stringResponseEntity == null) {
            throw new JwtException(HttpStatus.SERVICE_UNAVAILABLE, "服务不可用,请稍后再试");
        }
        String body = stringResponseEntity.getBody();
        JwtResponse jwtRes = MAPPER.readValue(body, JwtResponse.class);
        if (!jwtRes.isFlag()) {
            throw new JwtException(HttpStatus.UNAUTHORIZED, jwtRes.getMsg());
        }
        Map<String, String> claimsMap = jwtRes.getClaimsMap();
        request.setAttribute("claims", jwtRes.getClaimsMap());

        stringResponseEntity = restTemplate.postForEntity("http://localhost:8080/expire", formEntity, String.class);
        if (stringResponseEntity != null && Boolean.parseBoolean(stringResponseEntity.getBody())) {
            formEntity = JwtHttpEntityUtil.getCreateTokenEntity(claimsMap.get("sub"), claimsMap.get("pwd"));
            token = restTemplate.postForObject("http://localhost:8080/create", formEntity, String.class);
            response.setHeader("token", token);
        }
        return true;
    }
}

注册拦截器

import com.lb.exception.FacePlusThrowErrorHandler;
import com.lb.interceptor.JwtAuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.FormHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.nio.charset.StandardCharsets;

@Configuration
public class ApplicationConfig implements WebMvcConfigurer {
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        return new RestTemplate(factory);
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        factory.setReadTimeout(5000);
        factory.setConnectTimeout(15000);
        return factory;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截所有请求
        registry.addInterceptor(jwtAuthenticationInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public JwtAuthenticationInterceptor jwtAuthenticationInterceptor() {
        return new JwtAuthenticationInterceptor();
    }
    
    //自定义处理400,500返回
    @Bean
    public RestTemplate facePlusRestTemplate(ClientHttpRequestFactory factory) {
        RestTemplate restTemplate = new RestTemplate(factory);
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        restTemplate.getMessageConverters().add(new FormHttpMessageConverter());
        restTemplate.setErrorHandler(new FacePlusThrowErrorHandler());
        return restTemplate;
    }
}
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;

import java.io.IOException;

public class FacePlusThrowErrorHandler  implements ResponseErrorHandler {
    @Override
    public boolean hasError(ClientHttpResponse response) throws IOException {
        return false;
    }

    @Override
    public void handleError(ClientHttpResponse response) throws IOException {
        throw new JwtException(response.getStatusCode(), response.getBody().toString());
    }
}
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.HashMap;
import java.util.Map;

public class JwtHttpEntityUtil {
    private static ObjectMapper MAPPER = new ObjectMapper();
    public static HttpEntity<String> getCreateTokenEntity(String userName, String password) throws JsonProcessingException {
        HttpHeaders headers = new HttpHeaders();
        MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
        headers.setContentType(type);
        headers.add("Accept", MediaType.APPLICATION_JSON.toString());
        HashMap<String, Object> map = new HashMap<>();
        map.put("subject", userName);
        Map<String, String> calims = new HashMap<>();
        calims.put("pwd", password);
        map.put("claimsMap", calims);
        String stu = MAPPER.writeValueAsString(map);
        return new HttpEntity<String>(stu, headers);
    }

    public static HttpEntity getValidateEntity(String token) throws JsonProcessingException {
        MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<String, String>();
        paramMap.set("token",token);
        //2、添加请求头
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type","application/x-www-form-urlencoded");
        return new HttpEntity(paramMap, headers);
    }
}

客户端Controller 

@RestController
public class MyController {

    @Autowired
    RestTemplate restTemplate;

    @PassToken
    @PostMapping("login")
    public String login(String userName, String password, HttpServletResponse response) throws JsonProcessingException {
        //模拟数据库查询用户
        if ("admin".equals(userName) && "admin".equals(password)) {
            HttpEntity<String> formEntity = JwtHttpEntityUtil.getCreateTokenEntity(userName, password);
            String token = restTemplate.postForObject("http://localhost:8080/create", formEntity, String.class);
            response.setHeader("token", token);
            return "登陆成功";
        } else {
            return "用户名密码不正确";
        }
    }

    @GetMapping("test")
    public String test(HttpServletRequest request) {
        return request.getAttribute("claims").toString();
    }
}

以上代码省略了 异常类异常处理类

结果

 create API -- 拦截器放行login API,并返回token

 

 validate 、expire API -- 验证和更新token

 以上案例是为了,实现注册中心是单独服务,如果不需要它是单独的服务,直接在项目内提供工具类直接生成token,以及token的验证。更加简单

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值