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

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 : https://github.com/vir56k/demo/tree/master/springboot/auth_jwt_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微服务实战》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容