项目中需要做接口的token校验,项目大致分为以下几个模块:
eureka:eureka的注册中心
eureka-server:服务提供者
api:服务消费者,使用fegin消费服务
gateway:网关,用于进行token校验

1. eureka注册中心

配置了一个服务中心,并无具体的代码操作
pom文件中引入eureka的依赖,在启动类中加入@EnableEurekaServer

     <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
      </dependency>

配置文件内容:

server:
  port: 8083
spring:
  application:
    name: register-center
eureka:
  instance:
    #注册中心的ip地址
    hostname: 127.0.0.1
  client:
    register-with-eureka: false
    fetch-registry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
2. eureka-server
 提供服务,返回数据均为json串,使用restful方式的接口,接口中使用RequestMapping代替GetMapping和PostMapping
3. 服务的消费端api

这里需要用到redis及md5加密,引入jar包

 <!--redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version> 2.6.1</version>
</dependency>
<dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-crypto</artifactId>
      <version>5.0.0</version>
</dependency>
<!--  jwt鉴权  -->
<dependency>
     <groupId>com.auth0</groupId>
     <artifactId>java-jwt</artifactId>
     <version>3.4.1</version>
</dependency> 
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>        

登录的controller:

@RestController
@RequestMapping("/route-api")
public class LoginController {

    @Autowired
    private MemberService memberService;

    private ResponseDto responseDto;

    @Autowired
    private TokenUtil tokenUtil;

    /**
     * 登录方法
     * @param phone
     * @param password
     * @return
     */
    @GetMapping(value = "/login")
    public String login(@RequestParam("phone") String phone, @RequestParam("password") String password){
        //账号密码校验
        if(StrUtil.isNotEmpty(phone) && StrUtil.isNotEmpty(password)){
            String result = memberService.login(phone,password);
            if (result != null ){
                String object = (String)JSON.parse(result);
                DataResponseDto responseData = JSON.parseObject(object,DataResponseDto.class);
                if (responseData.getCode().equals(StatusCodeConstants.SUCCESS)){
                   Map<String, String> map = tokenUtil.getToken(phone, "1");
                    //返回结果
                    responseDto = new ResponseDto(StatusCodeConstants.SUCCESS,"成功并返回数据", map.get("token"), map.get("refreshToken"),responseData.getData());
                }else {
                    responseDto = new ResponseDto(responseData.getCode(),responseData.getMessage(),null);
                }
            }else {
                responseDto = new ResponseDto(StatusCodeConstants.PHONE_OR_PASSWORD_ERROR,"手机号或密码错误",null);
            }
        }else {
            responseDto = new ResponseDto(StatusCodeConstants.PHONE_OR_PASSWORD_ERROR,"手机号或密码错误",null);
        }
        return JSON.toJSONString(responseDto);
    }

    /**
     * 刷新JWT
     * @param refreshToken
     * @return
     */
    @GetMapping("/refresh")
    public String refreshToken(@RequestParam("refreshToken") String refreshToken, @RequestParam("phone") String phone){
        responseDto = tokenUtil.refreshToken(phone, "1", refreshToken);
        return JSON.toJSONString(responseDto);
    }

    /**
     * 退出时删除key
     * @param phone
     * @return
     */
    @GetMapping("/logout")
    public String logout( @RequestParam("phone") String phone){
        tokenUtil.removeToken(phone, "1");
        responseDto = new ResponseDto(StatusCodeConstants.SUCCESS, "退出成功",null);
        return JSON.toJSONString(responseDto);
    }
}

TokenUtil代码:

@Component
@PropertySource(value = "classpath:jwt.properties")
public class TokenUtil {

    @Value("${token.expire.time}")
    private long tokenExpireTime;

    @Value("${refresh.token.expire.time}")
    private long refreshTokenExpireTime;

    private Map<String , String> map = new HashMap<>(2);

    /**
     * 固定的头
     */
    private static final String OPERATE = "OPERATE";
    private static final String USER = "USER";
    private static final String WX = "WX";

    private ResponseDto responseDto;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 生成token和refreshToken
     * @param phone
     * @param type
     * @return
     */
    public Map<String, String> getToken(String phone, String type){
        //生成refreshToken
        String refreshToken = UUID.randomUUID().toString().replaceAll("-","");
        String prefix = this.getPrefix(type);
        String token = this.buildJWT(phone, prefix);
        String key = SecureUtil.md5(prefix + phone);
        //向hash中放入数值
        stringRedisTemplate.opsForHash().put(key,"token", token);
        stringRedisTemplate.opsForHash().put(key,"refreshToken", refreshToken);
        //设置key过期时间
        stringRedisTemplate.expire(key,
                refreshTokenExpireTime, TimeUnit.MILLISECONDS);
        map.put("token", token);
        map.put("refreshToken", refreshToken);
        return map;
    }

    /**
     * 刷新token
     * @param phone
     * @param type
     * @param refreshToken
     * @return
     */
    public ResponseDto refreshToken(String phone, String type, String refreshToken){
        String prefix = this.getPrefix(type);
        String key = SecureUtil.md5(prefix + phone);
        String oldRefresh = (String) stringRedisTemplate.opsForHash().get(key, "refreshToken");
        if (StrUtil.isBlank(oldRefresh)){
            responseDto = new ResponseDto(StatusCodeConstants.REFRESH_TOKEN_TIME_OUT,"refreshToken过期",null);
        }else {
            if (!oldRefresh.equals(refreshToken)){
                responseDto = new ResponseDto(StatusCodeConstants.REFRESH_TOKEN_ERROR,"refreshToken错误",null);
                System.out.println("refreshToken错误");
            }else {
                String token = this.buildJWT(phone, prefix);
                stringRedisTemplate.opsForHash().put(key,"token", token);
                stringRedisTemplate.opsForHash().put(key,"refreshToken", refreshToken);
                stringRedisTemplate.expire(key,
                        refreshTokenExpireTime, TimeUnit.MILLISECONDS);
                responseDto = new ResponseDto(StatusCodeConstants.SUCCESS,"成功并返回数据", token, refreshToken);
            }
        }
        return responseDto;
    }

    /**
     * 删除key
     * @param phone
     * @param type
     */
    public boolean removeToken(String phone, String type){
        String prefix = this.getPrefix(type);
        String key = SecureUtil.md5(prefix + phone);
        return stringRedisTemplate.delete(key);
    }

    /**
     * 获取前缀
     * @param type 1 操作端  2  用户端 3 小程序
     * @return
     */
    private String getPrefix(String type){
        String prefix = null;
        if ("1".equals(type)){
            prefix =OPERATE;
        }else if ("2".equals(type)){
            prefix = USER;
        }else if ("3".equals(type)){
            prefix =WX;
        }
        return prefix;
    }

    /**
     * 生成jwt
     * @param phone 手机号
     * @param prefix 前缀
     * @return
     */
    private String buildJWT(String phone, String prefix){
        //生成jwt
        Date now = new Date();
        Algorithm algo = Algorithm.HMAC256(prefix);
        String token = JWT.create()
                //签发人
                .withIssuer("userPhone")
                //签发时间
                .withIssuedAt(now)
                //过期时间
                .withExpiresAt(new Date(now.getTime() + tokenExpireTime))
                //自定义的存放的数据
                .withClaim("phone", phone)
                //签名
                .sign(algo);
        return token;
    }
}

因为我这里是需要给操作端、用户端、小程序提供接口,所以用type定义不同的头以区分key,也可以选择使用不同的datebase。

ResponseDto 是自定义的返回参数类:

@Setter
@Getter
public class ResponseDto<T>{

    /**
     * 状态码
     */
    private Integer code;

    /**
     * 状态信息
     */
    private String message;

    /**
     * 数据
     */
    private T data;

    /**
     * token
     */
    private String token;

    /**
     * 刷新token
     */
    private String refreshToken;

    public ResponseDto(){
    }

    public ResponseDto(Integer code, String message, T data){
        this.code = code;
        this.data = data;
        this.message = message;
    }

    public ResponseDto(Integer code, String message, String token,String refreshToken){
        this.code = code;
        this.message = message;
        this.token = token;
        this.refreshToken = refreshToken;
    }

    public ResponseDto(Integer code, String message, String token,String refreshToken,T data){
        this.code = code;
        this.message = message;
        this.token = token;
        this.refreshToken = refreshToken;
        this.data = data;
    }
}

service层主要是在进行调用方法操作:

@FeignClient(name = "service-provider")
public interface MemberService {

    /**
     * 调取登录的方法
     * @param phone
     * @param password
     * @return
     */
    @RequestMapping("/login")
    String login(@RequestParam("phone") String phone, @RequestParam("password") String password);

}

需要在启动类上加@EnableFeignClients和@EnableEurekaClient注解
yaml配置文件:

eureka:
  instance:
    prefer-ip-address: true
    #Eureka客户端向服务端发送心跳的时间间隔,单位为秒(客户端告诉服务端自己会按照该规则),默认30
    lease-renewal-interval-in-seconds: 10
    #Eureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒,超过则剔除(客户端告诉服务端按照此规则等待自己),默认90
    lease-expiration-duration-in-seconds: 12
  client:
    registry-fetch-interval-seconds: 10 #eureka client刷新本地缓存时间,默认30
    serviceUrl:
      defaultZone: http://127.0.0.1:8083/eureka/

server:
  port: 8082
spring:
  application:
    name: server-client

  #设置允许 一个项目中有name一样的FeignClient
  main:
    allow-bean-definition-overriding: true
  #redis配置
  redis:
    host: 
    port: 
    password: 
    database: 1
    timeout: 60s
   ## springboot2.0之后将连接池由jedis改为lettuce
    lettuce:
      pool:
        max-idle: 30
        max-active: 8
        max-wait: 10000
        min-idle: 10

jwt.properties中的配置:

#token过期时间:30分钟
token.expire.time=1800000

#refreshToken过期时间:12小时
refresh.token.expire.time=43200000

四.gateway中转发和过滤接口

这里在引入时候,需要注意:Spring Cloud Gateway 不使用 Web 作为服务器,而是 使用 WebFlux 作为服务器,Gateway 项目已经依赖了 starter-webflux,所以这里 千万不要依赖 starter-web

 <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-gateway</artifactId>
   </dependency>

   <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
   </dependency>

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

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

   <dependency>
       <groupId>cn.hutool</groupId>
       <artifactId>hutool-core</artifactId>
       <version>4.5.2</version>
   </dependency>

   <!-- 使用阿里的fastjson解析json数据 -->
   <dependency>
       <groupId>com.alibaba</groupId>
       <artifactId>fastjson</artifactId>
       <version>1.2.57</version>
   </dependency>

全局过滤器代码,其中StatusCodeConstants定义了一些常量:

@Component
@PropertySource(value = "classpath:jwt.properties")
public class ApiGlobalFilter implements GlobalFilter, Ordered {

    /**
     * 不进行token校验的请求地址
     */
    @Value("#{'${jwt.ignoreUrlList}'.split(',')}")
    public List<String> ignoreUrl;

    /**
     * 拦截所有的请求头
     * @param exchange
     * @param chain
     * @return
     */
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestUrl = exchange.getRequest().getPath().toString();
        boolean status = CollectionUtil.contains(ignoreUrl, requestUrl);
        if (!status){
            String token = exchange.getRequest().getHeaders().getFirst("token");
            //type用于区分不同的端,在做校验token时需要
            String type= exchange.getRequest().getHeaders().getFirst("type");
            ServerHttpResponse response = exchange.getResponse();
            //没有数据
            if (StrUtil.isBlank(token) || StrUtil.isBlank(type)) {
                JSONObject message = new JSONObject();
                message.put("code", StatusCodeConstants.TOKEN_NONE);
                message.put("message", "鉴权失败,无token或类型");
                byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
                DataBuffer buffer = response.bufferFactory().wrap(bits);
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                response.getHeaders().add("Content-Type", "text/json;charset=UTF-8");
                return response.writeWith(Mono.just(buffer));
                //有数据
            }else {
            	String prefix = this.getPrefix(type);
                //校验token
                String userPhone = verifyJWT(token ,prefix);
                if (StrUtil.isEmpty(userPhone)){
                    JSONObject message = new JSONObject();
                    message.put("message", "token错误");
                    message.put("code", StatusCodeConstants.TOKEN_ERROR);
                    byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
                    DataBuffer buffer = response.bufferFactory().wrap(bits);
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    response.getHeaders().add("Content-Type", "text/json;charset=UTF-8");
                    return response.writeWith(Mono.just(buffer));
                }
                //将现在的request,添加当前身份
                ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization-UserName", userPhone).build();
                ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
                return chain.filter(mutableExchange);
            }
        }
        return chain.filter(exchange);
    }

    /**
     * JWT验证
     * @param token
     * @return userPhone
     */
    private String verifyJWT(String token, String prefix){
        String userPhone;
        try {
            Algorithm algorithm = Algorithm.HMAC256(prefix);
           JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer("userPhone")
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            userPhone = jwt.getClaim("phone").asString();
        } catch (JWTVerificationException e){
        	e.printStackTrace();
            return "";
        }
        return userPhone;
    }
    
	/**
     * 根据type获取前缀
     * @param type
     * @return
     */
    private String getPrefix(String type){
        String prefix = null;
        if ("1".equals(type)){
            prefix = "OPERATE";
        }else if ("2".equals(type)){
            prefix = "USER";
        }else if ("3".equals(type)){
            prefix = "WX";
        }
        return prefix;
    }

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

jwt.properties中的配置了不拦截的请求:

jwt.ignoreUrlList=/route-api/login,/route-api/refresh

配置文件:

eureka:
  client:
    serviceUrl:
      defaultZone: http://127.0.0.1:8083/eureka/

server:
  port: 8087
spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        #netty 路由过滤器,http或https开头 lb://代表是eureka服务的名称  predicates:表示会过滤掉的请求头
        - id: gateway-route
          uri: lb://server-client
          predicates:
            - Path=/api/**,/route-api/**
    #处理跨域请求问题
    globalcors:
      cors-configurations:
        '[/**]':
          allowedOrigins: "*"
          allowedMethods: "*"
  #redis配置
  redis:
    host: 
    port: 6379
    password: 
    database: 1
    timeout: 60s
 ## springboot2.0之后将连接池由jedis改为lettuce
    lettuce:
      pool:
        max-idle: 30
        max-active: 8
        max-wait: 10000
        min-idle: 10
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐