Bootstrap

【第16章】Spring Cloud之Gateway全局过滤器(安全认证)


前言

我们已经接入了网关,所有的外部访问需要通过网关才能访问到我们的微服务,这一章我们在网关层进行统一的安全认证,保障服务安全和数据安全。

在前面的的【第3章】SpringBoot实战篇之登录接口(含JWT和拦截器) 已经实现了对用户身份的认证,思路是差不多的,我们这里通过网关层的过滤器来简单实现下。


一、公共模块

我们需要在在公共模块实现JWT以工具类的形式出现,方便其他模块的使用

1. 引入依赖

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

2. 工具类

package org.example.common.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.Map;

public class JwtUtils {

    private static final String SECRET="167589447321";

    public JwtUtils() {
    }

    /**
     * 生成token
     * @param claims 用户信息
     * @return String
     */
    public static String create(Map<String, Object> claims) {
        return JWT.create()
                .withClaim("claims", claims)
                .withIssuer("auth0")
                .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24))
                .sign(Algorithm.HMAC256(SECRET));
    }

    /**
     * 验证token
     * @param token token
     * @return boolean
     */
    public static boolean verify(String token) {
        DecodedJWT decodedJWT;
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    // specify any specific claim validations
                    .withIssuer("auth0")
                    // reusable verifier instance
                    .build();

            decodedJWT = verifier.verify(token);
        } catch (JWTVerificationException exception){
            // Invalid signature/claims
            return false;
        }
        return true;
    }

    /**
     * 解析token数据
     * @param token token
     * @return Map
     */
    public static Map<String, Object> getClaims(String token) {
        return JWT.require(Algorithm.HMAC256(SECRET))
                .withIssuer("auth0")
                .build()
                .verify(token)
                .getClaim("claims")
                .asMap();
    }

}

二、用户服务

这里我们新增用户服务,用于管理用户信息和生成token信息

1. 新建模块

在这里插入图片描述

2. 引入依赖

<dependencies>
   <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
	<dependency>
       <groupId>org.example</groupId>
       <artifactId>base-common</artifactId>
       <version>1.0.0-SNAPSHOT</version>
   </dependency>
</dependencies>

3. 启动类

package org.example.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * Create by zjg on 2024/7/21
 */
@EnableDiscoveryClient
@SpringBootApplication
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

4. 基本配置

server:
  port: 9007
spring:
  application:
    name: user-service
  cloud:
    nacos:
      config:
        file-extension: yaml
        server-addr: ${NACOS_SERVER_ADDR}
        namespace: ${NACOS_NAMESPACE}
        username: ${NACOS_USERNAME}
        password: ${NACOS_PASSWORD}
        shared-configs[0]:
            data-id: base-discovery.yaml
            group: DEFAULT_GROUP
            refresh: true

5. 登录接口

package org.example.user.controller;

import org.example.common.util.JwtUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;

/**
 * Create by zjg on 2024/7/21
 */
@RestController
@RequestMapping("/user/")
public class UserController {
    @RequestMapping("login")
    public String login(@RequestParam("username") String username,@RequestParam("password") String password){
        String message="用户名/密码不正确";
        String admin="admin";
        if(admin.equals(username)&&admin.equals(password)){
            Map<String, Object> claims=new HashMap<>();
            claims.put("username",username);
            return JwtUtils.create(claims);
        }
        return message;
    }
}

三、网关服务

1. 引入依赖

<dependency>
    <groupId>org.example</groupId>
    <artifactId>base-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

2. 应用配置

spring:
  cloud:
    gateway:
      routes:
      - id: provider-service
        uri: lb://provider-service
        predicates:
        - Path=/provider/**
        - BlackRemoteAddr=192.168.1.1/24,127.0.0.1,192.168.0.102
      - id: consumer-service
        uri: lb://consumer-service
        predicates:
        - Path=/consumer/**
        filters:
        - BlackList=/consumer/hello1,/consumer/hello2
      - id: user-service
        uri: lb://user-service
        predicates:
        - Path=/user/**

3. 自定义过滤器

package org.example.gateway.filter;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.example.common.model.Result;
import org.example.common.util.JwtUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * Create by zjg on 2024/7/21
 */
@Component
public class LoginGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String uri = request.getURI().getPath();
        ObjectMapper objectMapper = new ObjectMapper();
        if(uri.equals("/user/login")||uri.equals("/user/login/")){
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            if(queryParams.containsKey("username")&&queryParams.containsKey("password")){
                return chain.filter(exchange);
            }else {
                response.setStatusCode(HttpStatus.BAD_REQUEST);
                Result result = Result.error(HttpStatus.BAD_REQUEST.value(), "登录失败", "用户名和密码不能为空!");
                try {
                    return response.writeWith(Flux.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(result))));
                } catch (JsonProcessingException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        HttpHeaders headers = request.getHeaders();
        String authorization = headers.getFirst("Authorization");
        if(Boolean.FALSE.equals(StringUtils.hasText(authorization))||Boolean.FALSE.equals(JwtUtils.verify(authorization.startsWith("Bearer")?authorization.substring(authorization.indexOf("Bearer")+7):authorization))){
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            Result result = Result.error(HttpStatus.UNAUTHORIZED.value(), "认证失败", "token验证失败,请重新获取token后重试!");
            try {
                return response.writeWith(Flux.just(response.bufferFactory().wrap(objectMapper.writeValueAsBytes(result))));
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
        request = exchange.getRequest().mutate()
        						.headers(httpHeaders -> httpHeaders.add("Source-Mark", "Z2F0ZXdheQ==")).build();
        return chain.filter(exchange.mutate().request(request).build());
    }

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

四、单元测试

1.介绍

在这里插入图片描述

需要访问一个登录接口,获取token之后访问提供者的接口

大体思路:

  • 用户请求登录接口>网关>用户>公共模块
  • 用户请求提供者接口>网关>公共模块>提供者

2. 登录接口

在这里插入图片描述

3. 提供者接口

3.1 无token

在这里插入图片描述

3.2 有token

在这里插入图片描述
在这里插入图片描述


总结

回到顶部

网关层面已经实现了服务安全认证,进一步增加了我们微服务程序的安全性和稳定性。

;