SpringCloud Gateway基于jwt实现用户鉴权+GatewayFilter自定义拦截器 | Eddie'Blog
SpringCloud Gateway基于jwt实现用户鉴权+GatewayFilter自定义拦截器

SpringCloud Gateway基于jwt实现用户鉴权+GatewayFilter自定义拦截器

eddie 471 2021-02-03

简介

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

提前声明:Spring Cloud Gateway 底层使用了高性能的通信框架Netty。

Gateway VS Zuul

性能

-GatewayZuul 1.xZuul 2.x
靠谱性官方支持曾经靠谱过专业放鸽子
性能Netty同步阻塞, 性能慢Netty
RPS>3200020000左右25000左右
Spring Cloud已整合已整合暂无整合计划

综合对比

-GatewayZuul 1.xZuul 2.x
长连接支持长不支持支持
编程体验比较难比较简单比较难
调式&链路追踪比较难无压力比较难

项目相关

项目版本

-SpringcloudSpringBoot
-Greenwich.SR12.1.5.RELEASE

项目结构

D:.
├─auth-service
│  └─src
│      ├─main
│      │  ├─java
│      │  │  └─com
│      │  │      └─example
│      │  │          └─springcloud
│      │  │              ├─controller
│      │  │              └─service
│      │  └─resources
│      └─test
│          └─java
├─auth-service-api
│  └─src
│      ├─main
│      │  ├─java
│      │  │  └─com
│      │  │      └─example
│      │  │          └─springcloud
│      │  │              ├─entity
│      │  │              └─service
│      │  └─resources
│      └─test
│          └─java
└─gateway-sample
    └─src
        ├─main
        │  ├─java
        │  │  └─com
        │  │      └─example
        │  │          └─springcloud
        │  │              ├─config
        │  │              └─filter
        │  └─resources
        └─test
            └─java

auth-service-api(接口层)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-demo-dec</artifactId>
        <groupId>com.example</groupId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>auth-service-api</artifactId>

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

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

</project>

com.example.springcloud.entity.Account

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Account implements Serializable {

    private String username;

    private String token;

    private String refreshToken;

}

com.example.springcloud.entity.AuthResponse

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {

    private Account account;

    private Long code;

}

com.example.springcloud.entity.AuthResponseCode

public class AuthResponseCode {

	public static final Long SUCCESS = 1L;

	public static final Long INCORRECT_PWD = 1000L;

	public static final Long USER_NOT_FOUND = 1001L;

}

com.example.springcloud.service.AuthService

@FeignClient("auth-service")
public interface AuthService {

    /**
     *  创建token
     * @param username str
     * @param password str
     * @return AuthResponse
     */
	@PostMapping("/login")
	@ResponseBody
	public AuthResponse login(@RequestParam("username") String username,
							  @RequestParam("password") String password);

    /**
     *  验证token
     * @param token str
     * @param username name
     * @return AuthResponse
     */
	@GetMapping("/verify")
	public AuthResponse verify(@RequestParam("token") String token,
							   @RequestParam("username") String username);

    /**
     * 刷新token
     * @param refresh str
     * @return AuthResponse
     */
	@PostMapping("/refresh")
	@ResponseBody
	public AuthResponse refresh(@RequestParam("refresh") String refresh);

}

auth-service

com.example.springcloud.controller.DemoController

@Slf4j
@RestController
public class DemoController {

    @Autowired
    private JwtService jwtService;

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 生成token
     * @param username str
     * @param password str
     * @return AuthResponse
     */
    @PostMapping("/login")
    @ResponseBody
    public AuthResponse login(@RequestParam String username,
                              @RequestParam String password) {

        Account account = Account.builder()
                .username(username)
                .build();

        String token = jwtService.token(account);
        account.setToken(token);
        account.setRefreshToken(UUID.randomUUID().toString());

        redisTemplate.opsForValue().set(account.getRefreshToken(), account);

        return AuthResponse.builder()
                .account(account)
                .code(AuthResponseCode.SUCCESS)
                .build();
    }

    /**
     * 刷新token
     * @param refreshToken str
     * @return AuthResponse
     */
    @PostMapping("/refresh")
    @ResponseBody
    public AuthResponse refresh(@RequestParam String refreshToken) {
        Account account = (Account) redisTemplate.opsForValue().get(refreshToken);
        if (account == null) {
            return AuthResponse.builder()
                    .code(AuthResponseCode.USER_NOT_FOUND)
                    .build();
        }

        String jwt = jwtService.token(account);
        account.setToken(jwt);
        account.setRefreshToken(UUID.randomUUID().toString());

        redisTemplate.delete(refreshToken);
        redisTemplate.opsForValue().set(account.getRefreshToken(), account);

        return AuthResponse.builder()
                .account(account)
                .code(AuthResponseCode.SUCCESS)
                .build();
    }

    @GetMapping("/verify")
    public AuthResponse verify(@RequestParam String token,
                               @RequestParam String username) {

        boolean success = jwtService.verify(token, username);

        return AuthResponse.builder()
                .code(success ? AuthResponseCode.SUCCESS : AuthResponseCode.USER_NOT_FOUND)
                .build();
    }
}

application.yml

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:20000/eureka/

info:
  app:
    description: test
    name: auth-service

logging:
  file: ${spring.application.name}.log

management:
  endpoint:
    env:
      enabled: false
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include: '*'

server:
  port: 65100

spring:
  application:
    name: auth-service
  redis:
    database: 5
    host: 192.168.8.100
    port: 6379

com.example.springcloud.AuthApplication

@SpringBootApplication
@EnableEurekaClient
public class AuthApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(AuthApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args);
    }
}

com.example.springcloud.service.JwtService

@Slf4j
@Service
public class JwtService {

    private static final String KEY = "changIt";
    private static final String ISSUER = "eddie";

    private static final String USER_NAME = "username";
    private static final long TOKEN_EXP_TIME = 60 * 1000; // 60秒

    /**
     * 生成token
     * @param account 账户
     * @return str
     */
    public String token(Account account) {
        Date now = new Date();
        // 算法
        Algorithm algorithm = Algorithm.HMAC256(KEY);

        String token = JWT.create()
                .withIssuer(ISSUER) // 发行者, 这个在生产环境也是需要加密的
                .withIssuedAt(now) // 当前时间
                .withExpiresAt(new Date(now.getTime() + TOKEN_EXP_TIME))// 过期时间
                .withClaim(USER_NAME, account.getUsername())
//                .withClaim("ROLE","") // 企业级别应用基本都会传入 ROLE 权限
                .sign(algorithm);

		log.info("jwt generated user={}, token={}", account.getUsername(), token);
		return token;
    }

    /**
     * 校验token
     * @param token str
     * @param username str
     * @return b
     */
    public boolean verify(String token, String username) {
        log.info("verifying jwt - username={}", username);

        try {
            Algorithm algorithm = Algorithm.HMAC256(KEY);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer(ISSUER)
                    .withClaim(USER_NAME, username)
                    .build();

            verifier.verify(token);
            return true;
        } catch (Exception e) {
            log.error("auth failed", e);
            return false;
        }
    }
}

gateway-sample

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-demo-dec</artifactId>
        <groupId>com.example</groupId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <packaging>jar</packaging>
    <artifactId>gateway-sample</artifactId>

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

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

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

        <dependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>auth-service-api</artifactId>
            <version>${project.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
    </dependencies>
</project>

application.yml

spring:
  application:
    name: gateway-sample
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      routes:  # 断言配置
        - id: qq
          uri: https://www.qq.com
          predicates:
            - Path=/qq/**
          filters:
            - StripPrefix=1

        - id: feignclient
          uri: lb://FEIGN-CLIENT
          predicates:
            - Path=/yml/**
          filters:
            - StripPrefix=1 

server:
  port: 65000

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:20000/eureka/

management:
  endpoint:
    env:
      enabled: false
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include: '*'

ignore:
  jwt:
    skip-auth-urls:
      - /sayHi2

com.example.springcloud.GatewayApplication

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(GatewayApplication.class, args);
	}
}

com.example.springcloud.AuthFilter

@Data
@Slf4j
@Component("authFilter")
@ConfigurationProperties("ignore.jwt")
public class AuthFilter implements GatewayFilter, Ordered {

    private static final String AUTH = "Authorization";

    private static final String USERNAME = "jwt-user-name";

    private String[] skipAuthUrls;

    @Autowired
    private AuthService authService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("Auth start");

        String url = exchange.getRequest().getURI().getPath();
        System.out.println(url);

		if (null != skipAuthUrls && Arrays.asList(skipAuthUrls).contains(url)) {
			log.info("ignore jwt auth");
			return chain.filter(exchange);
		}

        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders header = request.getHeaders();
        String token = header.getFirst(AUTH);
        String username = header.getFirst(USERNAME);

        ServerHttpResponse response = exchange.getResponse();
        if (StringUtils.isBlank(token)) {
            log.error("token not found");
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            return response.setComplete();
        }

        AuthResponse resp = authService.verify(token, username);
        if (resp.getCode() != 1L) {
            log.error("invalid token");
            response.setStatusCode(HttpStatus.FORBIDDEN);
            return response.setComplete();
        }

        ServerHttpRequest.Builder mutate = request.mutate();
        assert username != null;
        mutate.header("jwt-user-name", username);
        ServerHttpRequest buildReuqest = mutate.build();

        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add("jwt-username",username);
        return chain.filter(exchange.mutate()
                .request(buildReuqest)
                .response(response)
                .build());
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

com.example.springcloud.filter.TimerFilter

@Slf4j
@Component
public class TimerFilter implements GatewayFilter, Ordered {
//public class TimerFilter implements GlobalFilter, Ordered {  // GlobalFilter 全局过滤器

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        StopWatch timer = new StopWatch();
        timer.start(exchange.getRequest().getURI().getRawPath());

//        exchange.getAttributes().put("requestTimeBegain", System.currentTimeMillis());
        return chain.filter(exchange).then(
                Mono.fromRunnable(() -> {
                    timer.stop();
                    log.info(timer.prettyPrint());
                })
        );
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

com.example.springcloud.config.GatewayConfiguration

@Configuration
public class GatewayConfiguration {

	@Autowired
	private TimerFilter timerFilter;

	@Autowired
	private AuthFilter authFilter;

	@Bean
	@Order
	public RouteLocator customizedRoutes(RouteLocatorBuilder builder) {
		return builder.routes()
				.route(r -> r.path("/java/**")
						.and().method(HttpMethod.GET)
						.and().header("name")
						.filters(f -> f.stripPrefix(1)
								.addResponseHeader("java-param", "gateway-config")
								.filter(timerFilter)
								.filter(authFilter)
						)
						.uri("lb://FEIGN-CLIENT")
				)
				.route(r -> r.path("/seckill/**")
								.and().after(ZonedDateTime.now().plusMinutes(1))
//                        .and().before()
//                        .and().between()
								.filters(f -> f.stripPrefix(1))
								.uri("lb://FEIGN-CLIENT")
				)
				.build();

	}

}

请求测试

按顺序启动服务

- EurekaServerApplication :20000/
- FeignClientApplication :40002/
- FeignClientApplication :40004/
- AuthApplication :65100/
- GatewayApplication :65000/

基于 auth-service 服务请求

- 生成Token
    - POST http://localhost:65100/login
    - Body -> x-www-form-unlencoded -> username=me / password=123456
- 校验Token
    - GET http://localhost:65100/verify?username=${生成token时候的用户名}&token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJlZGRpZSIsImV4cCI6MTYxMjE5MDY3MSwiaWF0IjoxNjEyMTkwNjExLCJ1c2VybmFtZSI6Im1lIn0.oDWI17FUGhNKZpQOhFHR2UqG2XgecfpySVAxMiUz6oc
- 刷新token
    - POST http://localhost:65100/refresh
    - Body -> x-www-form-unlencoded -> refreshToken=${生成token时候的refreshToken}

基于 gateway-sample 服务请求

- 拦截请求
    - http://localhost:65000/java/sayHi
    - Headers
        - name:eddie
        - Authorization:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJlZGRpZSIsImV4cCI6MTYxMjM0Mjc3NCwiaWF0IjoxNjEyMzQyNzE0LCJ1c2VybmFtZSI6Im1lIn0.RKtuUTcKBMBEzJM0GFZYbaSBuUc7VbiHrAIa8uqRqfI
        - jwt-user-name:me
- 放行请求
    - http://localhost:65000/java/sayHi2
    - Headers
        - name:eddie

TIPS: 必需添加 Headers:name=$, 不然会报404. 除非 route 不指定添加头部信息

额外知识点


The complete sample project for this tutorial can be downloaded code.