微服务应用(二):微服务之间调用的安全认证

star2017 1年前 ⋅ 303 阅读

微服务之间的相互调用,需要一套认证机制来确认调用是安全的。这不同于在 API 网关的统一认证,主要是防止在微服务暴露在外网的情况下,内部接口被外部恶意调用。

如果微服务是在内网,对外暴露的只有 API 网关,则可以不用做认证。本篇以 JWT 技术来实现安全认证。更多关于 JWT ,可参考分布式应用系列(一):详细理解 JWT(Json Web Token)

微服务架构中,通常会将认证功能独立成一个微服务,即创建一个专门处理认证、授权、解析、核验认证服务,可叫认证中心

其实 API 调用安全认证与 OSS 单点登录认证,在总体流程是上是相似的的,消费者首先请求认证服务,认证服务创建签发令牌(Token)返回给客户端,消费者带着令牌发请求到生产者,接下来就是对令牌的验证,验证通过就返回到业务层。

令牌验证三种方案:

一、令牌是基于 JWT 创建的,此令牌支持自验证,可以直接在生产者端对 JWT 令牌进行解析验证。

二、认证服务在生成令牌时,存到缓存服务器,生产者从缓存取出消费者令牌,与消费者携带的令牌进行比较验证。

三、生产者拿到消费者的令牌,请求认证服务,由认证服务对签发的令牌进行验证,把验证结果返回生产者。

api-auth

认证服务

认证服务主要提供创建令牌、签发令牌、返回令牌给客户端、解析验证令牌。

创建认证服务

创建 Spring Boot Web 应用,添加 JWT 实现的 JAR 库(java-jwt 或 jjwt),这里以 java-jwt 库为例。

  • java-jwt:此库是 JWT 的标准实现;
  • jjwt:此库扩展了压缩功能,即生成的 token 是已压缩后的,非标准的,无法用标准的 JWT 实现来解析它,如果生成和解析都用此库则没有问题,若生成的 token 需要在不同开发语言的系统中解析,则不能使用,无法确保兼容。

  • 微服务信息表

    数据库创建一张表,维护微服务信息表,表字段根据实际需要进行扩充。

    CREATE TABLE `app_info` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `app_id` varchar(100) NOT NULL,
      `secret_key` varchar(100) NOT NULL,
      `app_name` varchar(50) DEFAULT NULL,
      `app_desc` varchar(250) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微服务应用信息表'
    

    核心字段 app_idsecret_key,是查询条件。

  • 添加依赖

    java-jwt 依赖是必须添加的,如果微服务架构的注册中心是 Eureka,可以添加 eureka-client 依赖并配置注册到注册中心,其它依赖如 fastjson、commons-lang、hutool-all 按需添加。

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

编写认证 API

主要两个接口,一个是生成 JWT Token 的 API,另一个是验证 Token 的 API。

生成和验证 API 都需要用到加密算法,建议抽出 JWT 工具类 和 加密算法工具类,便于复用。

  1. 生成和验证 JWT Token API

    AuthController.class

    /**
    * @name: tokenAuth
    * @desc: 认证API
    * @author: gxing
    * @date: 2019-05-27 14:02
    **/
    @RestController
    @RequestMapping("/auth")
    public class AuthController {
    
      private Logger logger = LogManager.getLogger(AuthController.class);
    
      @Autowired
      private AppInfoService appInfoService;
    
      /**
       * 签发 Token
       *
       * @param authQuery 认证参数
       * @param response  响应
       * @return ResultBean
       */
      @RequestMapping("/token")
      public ResultBean getToken(AuthQuery authQuery, HttpServletRequest request, HttpServletResponse response) {
          logger.info("authQuery:{}", JSON.toJSONString(authQuery));
    
          if (StringUtils.isBlank(authQuery.getAppId()) || StringUtils.isBlank(authQuery.getSecretKey())) {
              return new ResultBean().fialByNullParam().setMsg("appId and secretKey must not null");
          }
    
          //根据appId 和 secretKey 到数据库查询
          AppInfo appInfo = appInfoService.queryAppInfo(authQuery);
          if (appInfo == null) {
              return new ResultBean().fialByNullParam().setMsg("auth fail");
          }
    
          String jwtId =Long.toString(System.currentTimeMillis());
          //第二个参数是过期时间,单位:分钟,详见工具类,1440分钟=24小时
          String token = JavaJwtUtil.getTokenByRSA512(jwtId, 1440);
    
          JwtToken jwtToken = new JwtToken(jwtId, token);
    
          return new ResultBean().success().setDate(jwtToken);
      }
    
      /**
       * 验证 Token
       *
       * @param request
       * @return ResultBean
       */
      @RequestMapping("/verify")
      public ResultBean verifyToken(HttpServletRequest request) {
    
          String token = request.getHeader("Authorization");
          String jwtId = request.getHeader("jwtId");
          boolean verify = JavaJwtUtil.verifyTokenByRSA512(token, jwtId);
          if (!verify) {
              return new ResultBean().fial().setMsg("Token 验证失败");
          }
          return new ResultBean().success();
      }
    }
    
  2. 相关实体类

    • AuthQuery:实体类,查询数据库的条件,包含 appIdsecretKey 两个属性。
    • AppInfo:实体类,微服务应用信息,属性与数据库表 app_info 中的字段对应。
    • JwtToken:实体类,封装生成 JWT Token 的必要信息,示例中包含基本地 jwtIdtoken 两个属性。
    • ResultBean:实体类,封装响应结果,包含 code、state、msg、data 属性。

封装 JWT 工具类

抽出生成和验证 JWT Token 功能到工具类,主要方法有:

  • 生成 Token:token不要有敏感信息,通常包含用户ID,jwtId等信息。
  • 验证 Token:检查是否合法,可以指定声明验证。
  • 刷新 RSA 公钥和私钥:刷新密钥对是为了防止泄漏、公钥和私钥通常是写死的,也可以做成配置的。集成配置管理中心后,可以对公钥和私钥进行动态修改,修改后重新初始化公钥、私钥对象。
/**
 * @name: JavaJwtUtil
 * @desc: java_jwt 库工具类,创建签发验证token
 **/
public class JavaJwtUtil {

    private static RSAPrivateKey rsaPrivateKey = RSAUtil.getPrivateKey(RSAUtil.MODULUS, RSAUtil.PRIVATE_EXPONENT);
    private static RSAPublicKey rsaPublicKey = RSAUtil.getPublicKey(RSAUtil.MODULUS, RSAUtil.PUBLIC_EXPONENT);


    /**
     * HMAC256 算法签发Token
     *
     * @param jwtId    用户id
     * @param secret 密钥
     * @return String
     */
    public static String getTokenByHMAC256(String jwtId, String secret) {
        /*默认一天有效期*/
        Long endDateTime = System.currentTimeMillis() + 1000 * 60 * 1440;

        String token = JWT.create()
                .withClaim("jwtId", jwtId)
                .withExpiresAt(new Date(endDateTime))
                .sign(Algorithm.HMAC256(secret));
        return token;
    }

    /**
     * HMAC256 算法签发Token
     *
     * @param jwtId    用户id
     * @param exp    过期时间,单位:分钟
     * @param secret 密钥
     * @return String
     */
    public static String getTokenByHMAC256(String jwtId, int exp, String secret) {
        Long endDateTime = System.currentTimeMillis() + 1000 * 60 * exp;

        String token = JWT.create()
                .withClaim("jwtId", jwtId)
                .withExpiresAt(new Date(endDateTime))
                .sign(Algorithm.HMAC256(secret));
        return token;
    }

    /**
     * RSA512 算法签发Token
     *
     * @param jwtId 用户ID
     * @return String
     */
    public static String getTokenByRSA512(String jwtId) {
        Long endDateTime = System.currentTimeMillis() + 1000 * 60 * 1440;

        Algorithm algorithm = Algorithm.RSA512(rsaPublicKey, rsaPrivateKey);
        String token = JWT.create()
                .withClaim("jwtId", jwtId)
                .withExpiresAt(new Date(endDateTime))
                .sign(algorithm);
        return token;
    }

    /**
     * RSA512 算法签发Token
     *
     * @param jwtId 用户id
     * @param exp 有效期
     * @return String
     */
    public static String getTokenByRSA512(String jwtId, int exp) {

        Long endDateTime = System.currentTimeMillis() + 1000 * 60 * exp;

        Algorithm algorithm = Algorithm.RSA512(rsaPublicKey, rsaPrivateKey);
        String token = JWT.create()
                .withClaim("jwtId", jwtId)
                .withExpiresAt(new Date(endDateTime))
                .sign(algorithm);
        return token;
    }

    /**
     * 验证 HMAC256 Token
     *
     * @param token  令牌
     * @param jwtId    用户id
     * @param secret 密钥
     * @return boolean
     */
    public static boolean verifyTokenByHMAC256(String token, String jwtId, String secret) {

        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret))
                .withClaim("jwtId", jwtId).build();
        return verifyToken(token, jwtVerifier);
    }

    /**
     * 验证 RSA512 Token
     *
     * @param token 令牌
     * @param jwtId   用户ID
     * @return boolean
     */
    public static boolean verifyTokenByRSA512(String token, String jwtId) {

        JWTVerifier jwtVerifier = JWT.require(Algorithm.RSA512(rsaPublicKey, rsaPrivateKey))
                .withClaim("jwtId", jwtId).build();
        return verifyToken(token, jwtVerifier);
    }

    /**
     * 验证 Token
     *
     * @param token 令牌
     * @param jwtVerifier JWTVerifier对象
     * @return boolean
     */
    private static boolean verifyToken(String token, JWTVerifier jwtVerifier) {
        try {
            jwtVerifier.verify(token);
            return true;
        } catch (JWTVerificationException e) {
            e.printStackTrace();
            return false;
        }
    }

    /*
    public static void main(String[] args) {

        String token1 = JavaJwtUtil.getTokenByHMAC256("1001", 60, "AABBCCDD");
        System.out.println(token1);

        String token2 = JavaJwtUtil.getTokenByHMAC256("1001", "aabbccdd");
        System.out.println(token2);

        boolean check = JavaJwtUtil.verifyTokenByHMAC256(token2, "1001", "aabbccdd");
        System.out.println(check);

        String tokenByRSA = JavaJwtUtil.getTokenByRSA512("1001");
        String tokenByRSA = JavaJwtUtil.getTokenByRSA512("1001", 1);

        System.out.println(tokenByRSA);

        boolean check = JavaJwtUtil.verifyTokenByRSA512(tokenByRSA, "1001");
        System.out.println(check);

    }
    */
}

封装 RSA 加密工具类

下面工具类使用模数的指数来生成 RSA 密钥时,必须重新设置 MODULUSPRIVATE_EXPONENTPUBLIC_EXPONENT 属性的值,可取消 main 方法的注释并运行,将打印输出的值复制到这三个对应的属性。

/**
 * @name: RSAUtil
 * @desc: RSA 加解密工具类
 * @author: gxing
 * @date: 2019-05-27 13:57
 **/
public class RSAUtil {

    public static String RSA_ALGORITHM = "RSA";

    /*模数*/
    public static String MODULUS = "";
    /*公钥指数*/
    public static String PUBLIC_EXPONENT = "65537";
    /*私钥指数*/
    public static String PRIVATE_EXPONENT = "";

    /**
     * 公钥加密
     * @param data
     * @return
     * @throws Exception
     */
    public static String encryptByPublicKey(String data) throws Exception {
        RSAPublicKey publicKey = RSAUtil.getPublicKey(MODULUS, PUBLIC_EXPONENT);
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        // 模长
        int key_len = publicKey.getModulus().bitLength() / 8;
        // 加密数据长度 <= 模长-11
        String[] datas = splitString(data, key_len - 11);
        String mi = "";
        // 如果明文长度大于模长-11则要分组加密
        for (String s : datas) {
            mi += bcd2Str(cipher.doFinal(s.getBytes()));
        }
        return mi;
    }

    /**
     * 私钥解密
     * @param data
     * @return
     * @throws Exception
     */
    public static String decryptByPrivateKey(String data) throws Exception {
        RSAPrivateKey privateKey = RSAUtil.getPrivateKey(MODULUS, PRIVATE_EXPONENT);
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        // 模长
        int key_len = privateKey.getModulus().bitLength() / 8;
        byte[] bytes = data.getBytes();
        byte[] bcd = ASCII_To_BCD(bytes, bytes.length);
        // 如果密文长度大于模长则要分组解密
        String ming = "";
        byte[][] arrays = splitArray(bcd, key_len);
        for (byte[] arr : arrays) {
            ming += new String(cipher.doFinal(arr));
        }
        return ming;
    }


    /**
     * 生成公钥和私钥
     * @throws NoSuchAlgorithmException
     */
    public static HashMap<String, Object> getKeys() throws NoSuchAlgorithmException {
        HashMap<String, Object> map = new HashMap<String, Object>();
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(RSA_ALGORITHM);
        keyPairGen.initialize(1024);
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        map.put("public", publicKey);
        map.put("private", privateKey);
        return map;
    }

    /**
     * 使用模和指数生成RSA公钥
     * 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA
     * /None/NoPadding】
     * @param modulus  模
     * @param exponent 指数
     * @return
     */
    public static RSAPublicKey getPublicKey(String modulus, String exponent) {
        try {
            BigInteger b1 = new BigInteger(modulus);
            BigInteger b2 = new BigInteger(exponent);
            KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
            RSAPublicKeySpec keySpec = new RSAPublicKeySpec(b1, b2);
            return (RSAPublicKey) keyFactory.generatePublic(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 使用模和指数生成RSA私钥
     * 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA
     * /None/NoPadding】
     *
     * @param modulus  模
     * @param exponent 指数
     * @return
     */
    public static RSAPrivateKey getPrivateKey(String modulus, String exponent) {
        try {
            BigInteger b1 = new BigInteger(modulus);
            BigInteger b2 = new BigInteger(exponent);
            KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
            RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(b1, b2);
            return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 公钥加密
     *
     * @param data
     * @param publicKey
     * @return
     * @throws Exception
     */
    public static String encryptByPublicKey(String data, RSAPublicKey publicKey) throws Exception {
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        // 模长
        int key_len = publicKey.getModulus().bitLength() / 8;
        // 加密数据长度 <= 模长-11
        String[] datas = splitString(data, key_len - 11);
        String mi = "";
        // 如果明文长度大于模长-11则要分组加密
        for (String s : datas) {
            mi += bcd2Str(cipher.doFinal(s.getBytes()));
        }
        return mi;
    }

    /**
     * 私钥解密
     *
     * @param data
     * @param privateKey
     * @return
     * @throws Exception
     */
    public static String decryptByPrivateKey(String data, RSAPrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        // 模长
        int key_len = privateKey.getModulus().bitLength() / 8;
        byte[] bytes = data.getBytes();
        byte[] bcd = ASCII_To_BCD(bytes, bytes.length);
        // 如果密文长度大于模长则要分组解密
        String ming = "";
        byte[][] arrays = splitArray(bcd, key_len);
        for (byte[] arr : arrays) {
            ming += new String(cipher.doFinal(arr));
        }
        return ming;
    }

    /**
     * ASCII码转BCD码
     */
    public static byte[] ASCII_To_BCD(byte[] ascii, int asc_len) {
        byte[] bcd = new byte[asc_len / 2];
        int j = 0;
        for (int i = 0; i < (asc_len + 1) / 2; i++) {
            bcd[i] = asc_to_bcd(ascii[j++]);
            bcd[i] = (byte) (((j >= asc_len) ? 0x00 : asc_to_bcd(ascii[j++])) + (bcd[i] << 4));
        }
        return bcd;
    }

    public static byte asc_to_bcd(byte asc) {
        byte bcd;

        if ((asc >= '0') && (asc <= '9'))
            bcd = (byte) (asc - '0');
        else if ((asc >= 'A') && (asc <= 'F'))
            bcd = (byte) (asc - 'A' + 10);
        else if ((asc >= 'a') && (asc <= 'f'))
            bcd = (byte) (asc - 'a' + 10);
        else
            bcd = (byte) (asc - 48);
        return bcd;
    }

    /**
     * BCD转字符串
     */
    public static String bcd2Str(byte[] bytes) {
        char temp[] = new char[bytes.length * 2], val;

        for (int i = 0; i < bytes.length; i++) {
            val = (char) (((bytes[i] & 0xf0) >> 4) & 0x0f);
            temp[i * 2] = (char) (val > 9 ? val + 'A' - 10 : val + '0');

            val = (char) (bytes[i] & 0x0f);
            temp[i * 2 + 1] = (char) (val > 9 ? val + 'A' - 10 : val + '0');
        }
        return new String(temp);
    }

    /**
     * 拆分字符串
     */
    public static String[] splitString(String string, int len) {
        int x = string.length() / len;
        int y = string.length() % len;
        int z = 0;
        if (y != 0) {
            z = 1;
        }
        String[] strings = new String[x + z];
        String str = "";
        for (int i = 0; i < x + z; i++) {
            if (i == x + z - 1 && y != 0) {
                str = string.substring(i * len, i * len + y);
            } else {
                str = string.substring(i * len, i * len + len);
            }
            strings[i] = str;
        }
        return strings;
    }

    /**
     * 拆分数组
     */
    public static byte[][] splitArray(byte[] data, int len) {
        int x = data.length / len;
        int y = data.length % len;
        int z = 0;
        if (y != 0) {
            z = 1;
        }
        byte[][] arrays = new byte[x + z][];
        byte[] arr;
        for (int i = 0; i < x + z; i++) {
            arr = new byte[len];
            if (i == x + z - 1 && y != 0) {
                System.arraycopy(data, i * len, arr, 0, y);
            } else {
                System.arraycopy(data, i * len, arr, 0, len);
            }
            arrays[i] = arr;
        }
        return arrays;
    }

    public static void main(String[] args) throws Exception {
        /*HashMap<String, Object> map = RSAUtil.getKeys();
        //生成公钥和私钥
        RSAPublicKey publicKey = (RSAPublicKey) map.get("public");
        RSAPrivateKey privateKey = (RSAPrivateKey) map.get("private");

        //模
        String MODULUS = publicKey.getModulus().toString();
        System.err.println("MODULUS:" + MODULUS);
        //公钥指数
        String PUBLIC_EXPONENT = publicKey.getPublicExponent().toString();
        System.err.println("PUBLIC_EXPONENT:" + PUBLIC_EXPONENT);
        //私钥指数
        String PRIVATE_EXPONENT = privateKey.getPrivateExponent().toString();
        System.err.println("PRIVATE_EXPONENT:" + PRIVATE_EXPONENT);


        //明文
        String ming = "Hello World";
        //使用模和指数生成公钥和私钥
        RSAPublicKey pubKey = RSAUtil.getPublicKey(MODULUS, PUBLIC_EXPONENT);
        RSAPrivateKey priKey = RSAUtil.getPrivateKey(MODULUS, PRIVATE_EXPONENT);

        //加密后的密文
        String mi = RSAUtil.encryptByPublicKey(ming, pubKey);
        System.err.println("加密后密文:"+mi);
        //解密后的明文
        ming = RSAUtil.decryptByPrivateKey(mi, priKey);
        System.err.println("解密后明文" + ming);

        String encStr = encryptByPublicKey("Hello World");
        System.out.println(encStr);
        System.out.println(decryptByPrivateKey(encStr));
        */
    }
}

消费者服务

消费者服务在请求生产者服务前必须先请求 认证服务 拿到到用于认证的 Token,然后每向生产者服务发请求,必须在 请求头 中携带此 Token,通常设置该请求头名为:Authorization

每次向生产者服务请求前都获取 Token 是不合适的,并且 Token 是有有效期的,第一次获取后,在有效期内可继续使用,所以在拿到 Token 后可以存起来,例如存到环境变量,或存到外部缓存系统 Redis 中,如果 Token 过期则重新获取。

获取 Token 两种方式,一种是在应用启动时就向认证服务请求获取 Token,但不支持动态更新;另一种是使用定时器,动态获取,定时器时间必须小于 Token 的过期时间,建议使用此方式。

应用访问认证服务必须提供 appIdsecretKey 两种参数,用于从数据库查询该应用的合法性。可以定义实体类注入配置文件中的属性值,或从环境变量( Environment 或 System)中取出。

@Component
@ConfigurationProperties(prefix = "common.property.app")
public class AuthQuery {

    private String appId;
    private String secretKey;
    //-----省略 set/get 方法------
}

定时器获取 Token

如果 Token 是采用动态改变策略,可以使用定时任务的方式,定期请求认证服务获取 Token 并动态更新的环境变量,定时任务的间隔时间必须小于 Token 的有效时长。

使用定时任务,需要在 Spring Boot 启动类上添加 @EnableScheduling 注解开启定时任务,再编写定时任务的业务,示例如下。

/**
 * @name: TokenScheduledTask
 * @desc: 定时任务动态更新 Token
 **/
@Component
public class TokenScheduledTask {

    @Autowired
    private AuthQuery authQuery;
    private static Logger logger = LogManager.getLogger(TokenScheduledTask.class);

    //20小时,token默认有效期是24小时
    private final static long DELAY = 1000 * 60 * 60 * 20;

    @Autowired(required = false)
    private AuthService authService;

    @Scheduled(fixedDelay = DELAY)
    public void reloadAuthToken() {
        JwtToken jwtToken = this.getToken();
        while (null == jwtToken) {
            try {
                Thread.sleep(1000);
                jwtToken = getToken();
            } catch (InterruptedException e) {
                logger.info("thread sleep error", e);
                e.printStackTrace();
            }
        }
        System.setProperty("jwtId", jwtToken.getJwtId());
        System.setProperty("token", jwtToken.getToken());

    }

    private JwtToken getToken() {
        ResultBean result = authService.getToken(authQuery);
        LinkedHashMap<String, String> resultDate = (LinkedHashMap<String, String>) result.getDate();
        if (null == resultDate) {
            return null;
        }
        return new JwtToken(resultDate.get("jwtId"), resultDate.get("token"));
    }
}

应用启动获取 Token

如果验证的 Token 不是动态改变的,可以在应用启动时就请求获取到 Token。

编写初始化 Token 配置类,实现 CommandLineRunner 接口,重写 run 方法。可以使用 RestTemplate 发送请求,如果认证服务、消费者服务都注册到了 Eureka Server(注册中心),也可以通过 Feign Client 来发送请求。启动初始化示例如下:

/**
 * @name: InitTokenConfig
 * @desc: 应用启动时初始化 Token
 **/
@Component
public class InitTokenConfig implements CommandLineRunner {

    @Autowired
    private RestTemplate restTemplateOne;
    @Autowired
    private AuthQuery authQuery;

    @Override
    public void run(String... args) throws Exception {

        String url = "http://localhost:9060/auth/token";

        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        paramMap.add("appId", this.authQuery.getAppId());
        paramMap.add("secretKey", this.authQuery.getSecretKey());
        ResultBean resultBean = restTemplateOne.postForObject(url, paramMap, ResultBean.class);
        LinkedHashMap<String,String> linkedHashMap = (LinkedHashMap<String, String>) resultBean.getDate();

        //设置到系统环境
        System.setProperty("jwtId", linkedHashMap.get("jwtId"));
        System.setProperty("token", linkedHashMap.get("token"));

    }
}

缓存 Token

请求获取认证的 Token 也可以缓存到 Redis 中,这样虽然减少了请求认证的次数,但会产生网络延时,所以建议存到服务环境变量中。

请求拦截器设置请求头

HTTP 远程调用通常会用到 HttpClient 或 RestTemplate,Spring Cloud 还可以使用 Feign,在调用前每次手动设置请求头则非常麻烦,而这三种 HTTP 客户端都支持添加拦截器来统一处理请求。

Feign 拦截器设置请求头

在 Spring Cloud 中通常会用 Feign 来调用接口,Feign 提供了请求拦截器 feign.RequestInterceptor 来支持对请求进行统一处理。

  1. Feign 请求拦截器实现 RequestInterceptor 接口

    /**
     * @name: FeignBasicAuthRequestInterceptor
     * @desc: Feign 请求拦截器
     **/
    public class FeignAuthRequestInterceptor implements RequestInterceptor {
        @Override
        public void apply(RequestTemplate requestTemplate) {
            requestTemplate.header("JwtId", System.getProperty("jwtId"));
            requestTemplate.header("Authorization", System.getProperty("token"));
        }
    }
    
  2. 将 FeignAuthRequestInterceptor 注册为 Bean

    /**
     * @name: FeignCustomConfig
     * @desc: TODO
     **/
    @Component
    public class FeignCustomConfig {
    
        @Bean
        public FeignAuthRequestInterceptor feignBasicAuthRequestInterceptor(){
            return new FeignAuthRequestInterceptor();
        }
    }
    
  3. 如果有多个 Feign 配置类,可通过 @FeignClient 注解时的 configuration 属性指定该配置类。

RestTemplate 拦截器设置请求头

如果使用 RestTemplate 发送请求,可以给 RestTemplate 添加拦截器来统一处理请求,需要实现 ClientHttpRequestInterceptor 接口。示例如下:

  1. RestTemplate 请求拦截器实现 ClientHttpRequestInterceptor 接口

    /**
     * @name: RestTemplateInterceptor
     * @desc: RestTemplate 请求拦截器
     **/
    public class RestTemplateRequestInterceptor implements ClientHttpRequestInterceptor {
        @Override
        public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    
            HttpHeaders headers = request.getHeaders();
            headers.add("JwtId", System.getProperty("jwtId"));
            headers.add("Authorization", System.getProperty("token"));
    
            return execution.execute(request, body);
        }
    }
    
  2. 创建 RestTemplate 实例时添加请求拦截器

    /**
     * @name: RestTemplateConfig
     * @desc: RestTemplate配置类
     **/
    @Configuration
    public class RestTemplateConfig {
    
        public RestTemplate restTemplate() {
            //设置超时时间,毫秒
            return new RestTemplateBuilder()
                    .setConnectTimeout(Duration.ofMillis(1000))
                    .setReadTimeout(Duration.ofMillis(1000))
                    .interceptors(new RestTemplateRequestInterceptor())
                    .build();
        }
    }
    

HttpClient 拦截器设置请求头

Apache Http Client 包(org.apache.http)下提供了 HttpRequestInterceptor 拦截器,可用于统一处理请求。

  1. HttpClient 请求拦截器实现 HttpRequestInterceptor 接口

    /**
     * @name: HttpClientRequestInterceptor
     * @desc: HttpClient 请求拦截器
     **/
    public class HttpClientRequestInterceptor implements HttpRequestInterceptor {
        @Override
        public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
            request.setHeader("JwtId", System.getProperty("JwtId"));
            request.setHeader("Authorization", System.getProperty("token"));
        }
    }
    
  2. 创建自定义的 httpClient 实例并添加请求拦截器

    /**
     * @name: HttpClientConfig
     * @desc: HttpClient 自定义配置
     **/
    @Component
    public class HttpClientConfig {
    
        @Bean
        public CloseableHttpClient closeableHttpClient() {
            CloseableHttpClient httpclient = HttpClients.custom()
                    .addInterceptorLast(new HttpClientRequestInterceptor())
                    .build();
            return httpclient;
        }
    }
    

生产者服务

生产者服务需要对消费接口请求进行身份认证,从请求头中取出 声明 和 Token,使用 JWT 进行验证。

可以使用 过滤器拦截器 来对请求的身份进行认证,以下是过滤器实现示例:

  1. 创建过滤器实现身份认证

    /**
    * @name: HttpTokenAuthFilter
    * @desc: 请求身份认证(Token)
    **/
    public class HttpTokenAuthFilter implements Filter {
    
       @Override
       public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
           HttpServletRequest request = (HttpServletRequest) servletRequest;
           HttpServletResponse response = (HttpServletResponse) servletResponse;
    
           response.setCharacterEncoding("UTF-8");
           response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    
           String token = request.getHeader("Authorization");
           String jwtId = request.getHeader("jwtId");
    
           if (StringUtils.isBlank(token) || StringUtils.isBlank(jwtId) || !JavaJwtUtil.verifyTokenByRSA512(token, jwtId)) {
               PrintWriter printWriter = response.getWriter();
               Map<String, String> resultMap = new HashMap<>();
               resultMap.put("state", "fail");
               resultMap.put("code", "400");
               resultMap.put("msg", "认证失败");
    
               String resultStr = JSON.toJSONString(resultMap);
               printWriter.write(resultStr);
           } else {
               filterChain.doFilter(request, response);
           }
    
       }
    }
    
  2. 注册过滤器为 Bean 来启用

    /**
     * @name: FilterConfig
     * @desc: 过滤器配置
     **/
    @Configuration
    public class FilterConfig {
    
        @Bean
        public FilterRegistrationBean filterRegistrationBean(){
            FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            registrationBean.setFilter(new HttpTokenAuthFilter());
    
            List<String> urlPatterns = new ArrayList<>(1);
            //针对所有请求
            urlPatterns.add("/*");
            registrationBean.setUrlPatterns(urlPatterns);
            return registrationBean;
        }
    }
    

网关统一身份认证

如内部微服务必须经过网关才能访问,则可以在网关统一执行身份认证。例如,Zuul 网关,可创建一个前置过滤器(pre filter),在过滤器执行统一认证,捕抓并抛出异常,阻断路由到下游服务。关于 Zuul 过滤器,可参考 Spring Cloud系列(九):API网关 Zuul 其它详细设置

  1. 创建 Token 认证前置过滤器

    /**
     * @name: TokenAuthPreFilter
     * @desc: 统一身份认证
     **/
    public class TokenAuthPreFilter extends ZuulFilter {
        @Override
        public String filterType() {
            return FilterConstants.PRE_TYPE;
        }
    
        @Override
        public int filterOrder() {
            return 5;
        }
    
        @Override
        public boolean shouldFilter() {
    
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            RequestContext context = RequestContext.getCurrentContext();
            HttpServletRequest request = context.getRequest();
            String jwtId = request.getHeader("JwtId");
            String authorization = request.getHeader("Authorization");
            System.out.println("JwtId : " + jwtId);
            System.out.println("Authorization : " + authorization);
            try {
                JavaJwtUtil.verifyTokenByRSA512(authorization, jwtId);
            } catch (Exception e) {
                //必须抛出或打印出错误,才不会路由到下游服务
    //            throw e;
                Throwable throwable = context.getThrowable();
                throwable.printStackTrace();
            }
            return null;
        }
    }
    
  2. 将认证过滤器注册为 Bean

    /**
     * @name: ZuulConfig
     * @desc: Zuul 网关配置
     **/
    
    @Configuration
    public class ZuulConfig {
    
        @Bean
        public TokenAuthPreFilter tokenAuthPreFilter(){
            return new TokenAuthPreFilter();
        }
    }
    

其它参考

  1. 微服务架构之访问安全
  2. 并发登录人数控制
  3. SpringBoot 并发登录人数控制
更多内容请访问:IT源点

相关文章推荐

全部评论: 0

    我有话说: