微服务之间的相互调用,需要一套认证机制来确认调用是安全的。这不同于在 API 网关的统一认证,主要是防止在微服务暴露在外网的情况下,内部接口被外部恶意调用。
如果微服务是在内网,对外暴露的只有 API 网关,则可以不用做认证。本篇以 JWT 技术来实现安全认证。更多关于 JWT ,可参考分布式应用系列(一):详细理解 JWT(Json Web Token)。
微服务架构中,通常会将认证功能独立成一个微服务,即创建一个专门处理认证、授权、解析、核验
的认证服务
,可叫认证中心
。
其实 API 调用安全认证与 OSS 单点登录认证,在总体流程是上是相似的的,消费者首先请求认证服务,认证服务创建签发令牌(Token)返回给客户端,消费者带着令牌发请求到生产者,接下来就是对令牌的验证,验证通过就返回到业务层。
令牌验证三种方案:
一、令牌是基于 JWT 创建的,此令牌支持自验证,可以直接在生产者端对 JWT 令牌进行解析验证。
二、认证服务在生成令牌时,存到缓存服务器,生产者从缓存取出消费者令牌,与消费者携带的令牌进行比较验证。
三、生产者拿到消费者的令牌,请求认证服务,由认证服务对签发的令牌进行验证,把验证结果返回生产者。
认证服务
认证服务主要提供创建令牌、签发令牌、返回令牌给客户端、解析验证令牌。
创建认证服务
创建 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_id 和 secret_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 工具类 和 加密算法工具类,便于复用。
生成和验证 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(); } }
相关实体类
- AuthQuery:实体类,查询数据库的条件,包含 appId 和 secretKey 两个属性。
- AppInfo:实体类,微服务应用信息,属性与数据库表 app_info 中的字段对应。
- JwtToken:实体类,封装生成 JWT Token 的必要信息,示例中包含基本地 jwtId 和 token 两个属性。
- 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 密钥时,必须重新设置 MODULUS 、PRIVATE_EXPONENT 和 PUBLIC_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 的过期时间,建议使用此方式。
应用访问认证服务必须提供 appId 和 secretKey 两种参数,用于从数据库查询该应用的合法性。可以定义实体类注入配置文件中的属性值,或从环境变量( 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 来支持对请求进行统一处理。
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")); } }
将 FeignAuthRequestInterceptor 注册为 Bean
/** * @name: FeignCustomConfig * @desc: TODO **/ @Component public class FeignCustomConfig { @Bean public FeignAuthRequestInterceptor feignBasicAuthRequestInterceptor(){ return new FeignAuthRequestInterceptor(); } }
如果有多个 Feign 配置类,可通过 @FeignClient 注解时的 configuration 属性指定该配置类。
RestTemplate 拦截器设置请求头
如果使用 RestTemplate 发送请求,可以给 RestTemplate 添加拦截器来统一处理请求,需要实现 ClientHttpRequestInterceptor 接口。示例如下:
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); } }
创建 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 拦截器,可用于统一处理请求。
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")); } }
创建自定义的 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 进行验证。
可以使用 过滤器 或 拦截器 来对请求的身份进行认证,以下是过滤器实现示例:
创建过滤器实现身份认证
/** * @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); } } }
注册过滤器为 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 其它详细设置。
创建 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; } }
将认证过滤器注册为 Bean
/** * @name: ZuulConfig * @desc: Zuul 网关配置 **/ @Configuration public class ZuulConfig { @Bean public TokenAuthPreFilter tokenAuthPreFilter(){ return new TokenAuthPreFilter(); } }
其它参考
注意:本文归作者所有,未经作者允许,不得转载