Spring Cloud 学习笔记(6) gateway 结合 JWT 实现身份认证

502 阅读5分钟

1. 背景

Spring cloud gateway 是一个api网关,可以作为 api 接口的统一入口点。实际使用过程中往往需要 对 一个 URL 进行身份认证,比如必须携带token令牌才能访问具体的URL等,这个过程可以统一在 gateway 网关实现。

JWT 是一种数字签名(令牌)的格式。借助于 java 类库的 JWT 实现我们可以很方便的实现 生成token,和验证,解析token。

gateway 集合 JWT 可以实现基础的身份认证功能。

2.知识

spring-cloud-gateway 提供了一个建立在Spring生态系统之上的API网关,旨在提供一种简单而有效的方法路由到api,并为它们提供横切关注点,如:安全性、监控/指标和弹性等。

JWT : JWT 是一种数字签名(令牌)的格式。 JSON Web Token (JWT)是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

实现思路

  • 1、写一个 gateway 网关,它是对外的 访问接入点。任何URL 都要先经过这个 网关。
  • 2、我们还需要一个 接口用于生成token,比如 /login ,它接收账户和秘密,如何验证通过,则返回一个有效的 token。
  • 3、上面的 有效的 token 借助于 JWT 来生成。
  • 4、后续 再次访问 其他资源时,都要在请求头包含 上一步生成的 token,可以理解为一个令牌,钥匙。
  • 5、当一个请求进来时,检查是否有 token,这个token是否合法,借助于 JWT 来实现。
  • 6、我们将 借助于JWT 生成token和校验token 的类写在一个名字叫做 auth-service 的微服务里。

用一张图来看:

image.png

3. 示例

(1) 实现需要一个 gateway 的过滤器 AuthorizationFilter,它会截获所有的 请求。

@Slf4j
@Component
public class AuthorizationFilter extends AbstractGatewayFilterFactory<AuthorizationFilter.Config> {
    @Autowired
    private AuthorizationClient1 authorizationClient;

    @Autowired
    private IgnoreAuthorizationConfig ignoreAuthorizationConfig;

    public AuthorizationFilter() {
        super(AuthorizationFilter.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            log.info("## 触发在 过滤器:AuthorizationFilter2");
            String targetUriPath = exchange.getRequest().getURI().getPath();
            if (isSkipAuth(targetUriPath)) {
                log.info("## 跳过 身份验证, targetUriPath={}", targetUriPath);
                return goNext(exchange, chain);
            }
            String token = exchange.getRequest().getHeaders().getFirst("token");
            if (token == null || token.isEmpty()) {
                log.info("## 无效的token = {}, targetUriPath= {}", token, targetUriPath);
                return responseInvalidToken(exchange, chain);
            }
            if (!verifyToken(token)) {
                log.info("## token 校验失败,参数 token = {}, targetUriPath= {}", token, targetUriPath);
                return responseInvalidToken(exchange, chain);
            }
            log.info("## token 校验通过! 参数 token = {}, targetUriPath= {}", token, targetUriPath);
            return chain.filter(exchange);
        };
    }

修改配置文件:

spring:
  application:
    name: api-gateway

  cloud:
    gateway:
      default-filters:
        - AuthorizationFilter
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      globalcors:
        corsConfigurations:
          '[/auth/**]':
            allowedOrigins: '*'
            allowedHeaders:
              - x-auth-token
              - x-request-id
              - Content-Type
              - x-requested-with
              - x-request-id
            allowedMethods:
              - GET
              - POST
              - OPTIONS
      routes:
        - id: auth-service
          uri: lb://auth-service
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
        - id: hello-service-1
          uri: lb://hello-service
          predicates:
            - Path=/hello/**
          filters:
            - StripPrefix=1

(2)过滤到特殊的 不需要校验的URL

@Autowired
    private IgnoreAuthorizationConfig ignoreAuthorizationConfig;


    /**
     * 是否跳过 认证检查
     *
     * @param targetUriPath 请求的资源 URI
     * @return
     */
    private boolean isSkipAuth(String targetUriPath) {
        boolean isSkip = ignoreAuthorizationConfig.getUrlList().contains(targetUriPath);
        log.info("## isSkip={}, ignoreAuthorizationConfig={}, targetUriPath={}", isSkip, ignoreAuthorizationConfig, targetUriPath);
        return isSkip;
    }

@Data
@Component
@ConfigurationProperties(prefix = "ignore.authorization")
public class IgnoreAuthorizationConfig {

    /**
     * 忽略 身份认证的 url列表
     */
    private Set<String> urlList;
}

还要修改配置文件:

ignore:
  authorization:
    urlList:
      - /auth/login
      - /auth/logout

(3) 通过调用 auth 服务来进行 校验 token 合法性

/**
     * 验证 token 的合法性
     *
     * @param token
     * @return
     */
    private boolean verifyToken(String token) {
        try {
            String verifyToken = authorizationClient.verifyToken(token);
            log.info("## verifyToken, 参数token={}, result = {}", token, verifyToken);
            return verifyToken != null && !verifyToken.isEmpty();
        } catch (Exception ex) {
            ex.printStackTrace();
            log.info("## verifyToken,参数token={}, 发生异常 = {}", token, ex.toString());
            return false;
        }
    }

AuthorizationClient1 类 负责发起网络请求到 auth 微服务。

/**
 * @author zhangyunfei
 * @date 2019/2/20
 */
@Slf4j
@Service
public class AuthorizationClient1 {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 备注:
     * 1、如果使用 RestTemplate LoadBalanced, 则触发异常: blockLast() are blocking, which is not supported in thread reactor-http-nio-3
     * 2、so,只能 停止 LoadBalanced,写死一个 ip
     */

//        private static final String URL_VERIFY_TOKEN = "http://auth-service/verifytoken";
    private static final String URL_VERIFY_TOKEN = "http://127.0.0.1:8082/verifytoken";

    public String verifyToken(String token) {
        log.info("## verifyToken 准备执行:verifyToken");

        HttpHeaders headers = new HttpHeaders();
        LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        HttpEntity entity = new HttpEntity<>(paramMap, headers);
        paramMap.add("token", token);
        String url = URL_VERIFY_TOKEN;
        ResponseEntity<String> forEntity = restTemplate
                .exchange(url, HttpMethod.POST, entity, new ParameterizedTypeReference<String>() {
                });
        HttpStatus statusCode = forEntity.getStatusCode();
        String res = forEntity.getBody();
        log.info("## verifyToken 执行结束:verifyToken,statusCode={}, 结果={}", statusCode, res);
        return res;
    }

}

(4)写一个 auth 身份认证的微服务

职责:

  • 1、/login 生成token
  • 2、校验token是否合法
@RestController()
public class AuthController {
    private Logger logger = LoggerFactory.getLogger("AuthController");

    /**
     * 鉴权: 通过token 获得用户的信息。
     * - 成功:返回用户信息
     * - 失败:返回 401
     * - 失败的情形: 1、token 过期。2、token 为空或无效。
     *
     * @param token
     * @return
     */
    @RequestMapping(value = {"/authority"}, method = RequestMethod.POST)
    public String authority(@RequestParam String token, @RequestParam String resource) {
        logger.info("## auth" + token);
        return "{ userId:123, userName:\"zhang3\" }";
    }

    /**
     * 验证 token 的合法性
     *
     * @param token
     * @return
     */
    @RequestMapping(value = {"/verifytoken"}, method = RequestMethod.POST)
    public ResponseEntity<String> verifyToken(@RequestParam String token) {
        logger.info("## verifyToken 参数 token={}", token);
        String userName = JwtUtils.decode(token);
        if (userName == null || userName.isEmpty()) {
            logger.info("## verifyToken 参数 token={}, 失败", token);
            return new ResponseEntity<>("internal error", HttpStatus.UNAUTHORIZED);
        }
        UserInfo user = new UserInfo(userName, "", 18);
        logger.info("## verifyToken 参数 token={}, 成功,用户信息={}", token, user);
        return new ResponseEntity<>(JSON.toJSONString(user), HttpStatus.OK);
    }


    /**
     * 根据token 获得我的个人信息
     *
     * @param token
     * @param resource
     * @return
     */
    @RequestMapping(value = "/mine", method = RequestMethod.POST)
    public String mine(@RequestParam String token, @RequestParam String resource) {
        logger.info("## auth" + token);
        return "{ userId:123, userName:\"zhang3\", group:\"zh\", country:\"china\" }";
    }

    /**
     * 身份认证:即 通过账户密码获得 token
     *
     * @param name
     * @param password
     * @return
     */
    @RequestMapping(value = {"/authorization", "/login"})
    public String authorization(@RequestParam String name, @RequestParam String password) {
        String token = JwtUtils.sign(name);
        logger.info("## authorization name={}, token={}", name, token);
        return token;

    }
}

(5) 访问

可以在 postman 里发起请求访问:登录http://localhost:9000/auth/login?name=wang5&password=1访问业务http://localhost:9000/hello/hi?name=zhang3

4. 扩展

我的 demo : github.com/vir56k/demo…

JWT辅助类

package eureka_client.demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;

public class JwtUtils {
    private static final String SECRET = "zhangyunfei789!@";
    private static final long EXPIRE = 1000 * 60 * 60 * 24 * 7;  //过期时间,7天

    /**
     * 构建一个 token
     * 传入 userID
     *
     * @param userID
     * @return
     */
    public static String sign(String userID) {
        try {
            Date now = new Date();
            long expMillis = now.getTime() + EXPIRE;
            Date expDate = new Date(expMillis);

            Algorithm algorithmHS = Algorithm.HMAC256(SECRET);
            String token = JWT.create()
                    .withIssuer("auth0")
                    .withJWTId(userID)
                    .withIssuedAt(now)
                    .withExpiresAt(expDate)
                    .sign(algorithmHS);
            return token;
        } catch (JWTCreationException exception) {
            //Invalid Signing configuration / Couldn't convert Claims.
            return null;
        }
    }

    /**
     * 解析 token
     * 返回  是否有效
     * @param token
     * @return
     */
    public static boolean verify(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer("auth0")
                    .build(); //Reusable verifier instance
            DecodedJWT jwt = verifier.verify(token);
            String userID = jwt.getId();
            return userID != null && !"".equals(userID);
        } catch (JWTVerificationException exception) {
            //Invalid signature/claims
            return false;
        }
    }

    /**
     * 解析 token
     * 返回  userid
     * @param token
     * @return
     */
    public static String decode(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer("auth0")
                    .build(); //Reusable verifier instance
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getId();
        } catch (JWTVerificationException exception) {
            //Invalid signature/claims
            return null;
        }
    }
}

5.参考:

《Spring Cloud微服务实战》