feign和springSecurity
一、拦截器
java
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String token = KmsPlatTokenUtil.genToken();
requestTemplate.header("Authorization", "Bearer " + token);
}
}
二、看工具类KmsPlatTokenUtil
依赖 配置类,实体类PlatAuth,token工具类
java
package com.skms.plat.api;
import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.symmetric.SM4;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.skms.common.exception.CustomException;
import com.skms.common.utils.SM3Utils;
import com.skms.common.utils.SecurityUtils;
import com.skms.plat.config.KmsPlatformConfig;
import lombok.extern.slf4j.Slf4j;
/**
* @author skms
*/
@Slf4j
public class KmsPlatTokenUtil {
private final static int EXPIRED_TIME = 5;
public static String genToken(PlatAuth platAuth, String key) throws Exception {
// 加密
String source = JSONUtil.toJsonStr(platAuth);
SM4 sm4 = SmUtil.sm4(key.getBytes());
return sm4.encryptHex(source);
}
public static String genToken() {
KmsPlatformConfig config = SpringUtil.getBean(KmsPlatformConfig.class);
PlatAuth auth = new PlatAuth(SecurityUtils.getUserId(), SecurityUtils.getUsername());
try {
return genToken(auth, config.getTokenEncodeKey());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static PlatAuth parseToken(String token, String key) throws Exception {
SM4 sm4 = SmUtil.sm4(key.getBytes());
String re = sm4.decryptStr(token);
PlatAuth platAuth = JSONUtil.toBean(re, PlatAuth.class);
// 校验Hash
boolean flag = SM3Utils.verify((platAuth.getUserId() + platAuth.getUsername() + platAuth.getTimeStr()).getBytes(), Base64Decoder.decode(platAuth.getHash()));
if (!flag) {
throw new CustomException("token校验失败");
}
long time = DateUtil.between(DateUtil.parse(platAuth.getTimeStr(), "yyyy-MM-dd HH:mm:ss"), DateUtil.date(), DateUnit.MINUTE, true);
if (time > EXPIRED_TIME) {
throw new CustomException("token过期");
}
return platAuth;
}
}
三、工具类的依赖
java
/**
* @author skms
*/
@Data
@Component
@ConfigurationProperties(prefix = "kms")
public class KmsPlatformConfig {
/**
* 中心配置
*/
private String center;
/**
* 密钥加密密钥
*/
private String kek;
private Integer centerId;
private Integer appId;
private String tokenEncodeKey;
private String uploadTempPath;
private String updateFilePath;
private String type;
private String grpcConfig;
private Integer grpcPort;
private Integer sdfPort;
}
java
/**
* @author skms
*/
@Data
@NoArgsConstructor
public class PlatAuth {
private Long userId;
private String username;
private String timeStr;
/**
* hash之后base64之后的值
*/
private String hash;
public PlatAuth(Long userId, String username) {
this.userId = userId;
this.username = username;
// 获取当前时间字符串
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
this.timeStr = LocalDateTime.now().format(formatter);
this.hash = Base64Encoder.encode(SM3Utils.hash((userId + username + timeStr).getBytes()));
}
}
java
/**
* 安全服务工具类
*
* @author skms
*/
public class SecurityUtils {
/**
* 用户ID
**/
public static Long getUserId() {
try {
return getLoginUser().getUserId();
} catch (Exception e) {
throw new ServiceE("获取用户ID异常", HttpStatus.UNAUTHORIZED);
}
}
/**
* 获取用户账户
**/
public static String getUsername() {
try {
return getLoginUser().getUsername();
} catch (Exception e) {
throw new ServiceE("获取用户账户异常", HttpStatus.UNAUTHORIZED);
}
}
/**
* 获取用户
**/
public static LoginUser getLoginUser() {
try {
return (LoginUser) getAuthentication().getPrincipal();
} catch (Exception e) {
throw new ServiceE("获取用户信息异常", HttpStatus.UNAUTHORIZED);
}
}
/**
* 获取Authentication
*/
public static Authentication getAuthentication() {
return SecurityContextHolder.getContext().getAuthentication();
}
/**
* 生成BCryptPasswordEncoder密码
*
* @param password 密码
* @return 加密字符串
*/
public static String encryptPassword(String password) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.encode(password);
}
/**
* 判断密码是否相同
*
* @param rawPassword 真实密码
* @param encodedPassword 加密后字符
* @return 结果
*/
public static boolean matchesPassword(String rawPassword, String encodedPassword) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(rawPassword, encodedPassword);
}
/**
* 是否为管理员
*
* @param userId 用户ID
* @return 结果
*/
public static boolean isAdmin(Long userId) {
return userId != null && 1L == userId;
}
}
四、feign请求加白名单
由于项目要抽取用户表单独放一个服务,所有请求都是 /system/user 开头,当时怎么都没想到请求不通是拦截器的原因,在Feign里面一步步断点才知道(项目奇怪的地方是项目A和项目B各自保留security,单独抽取的认证服务只有用户表,涉及用户操作都的远程请求,原来项目A和项目B涉及用户表操作都的重新修改)
我直接修改拦截器(简单粗暴)
java
/**
* @author skms
*/
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
String uri = requestTemplate.url();
if (uri != null && uri.contains("/system/user")) {
return;
}
String token = KmsPlatTokenUtil.genToken();
requestTemplate.header("Authorization", "Bearer " + token);
}
}
正确方式是:放配置文件
方式一:常用方式:在 RequestInterceptor 中通过 URL 判断白名单(最常用)
1. 创建白名单配置类
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Data
@Component
@ConfigurationProperties(prefix = "feign.whitelist")
public class FeignWhitelistProperties {
/**
* 白名单路径列表,支持通配符
*/
private List<String> urls = new ArrayList<>();
}
2. 配置文件添加白名单(application.yml)
java
feign:
whitelist:
urls:
- /api/public/**
- /health
- /actuator/**
- /system/user/login
3. 创建带白名单判断的拦截器
java
public class FeignRequestInterceptor implements RequestInterceptor {
private final FeignWhitelistProperties whitelistProperties;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取请求的 URL 路径(不包含域名和端口)
String url = template.url();
String path = extractPath(url);
// 判断是否在白名单中
if (isWhitelisted(path)) {
log.debug("白名单路径,跳过鉴权: {}", path);
return;
}
// 不在白名单的请求,添加鉴权 Header
log.debug("非白名单路径,添加鉴权信息: {}", path);
String token = KmsPlatTokenUtil.genToken();
requestTemplate.header("Authorization", "Bearer " + token);
}
/**
* 从完整 URL 中提取路径部分
*/
private String extractPath(String url) {
if (url == null) return "";
int queryIndex = url.indexOf('?');
if (queryIndex > 0) {
return url.substring(0, queryIndex);
}
return url;
}
/**
* 判断路径是否在白名单中
*/
private boolean isWhitelisted(String path) {
return whitelistProperties.getUrls().stream()
.anyMatch(pattern -> pathMatcher.match(pattern, path));
}
}
方式二:通过服务名判断白名单(微服务场景)
在微服务架构中,有时白名单是针对服务级别的,而非接口级别。可以通过判断调用的服务名来决定是否添加鉴权
1.配置文件:
yaml
feign:
whitelist:
services:
- public-service
- auth-service
- config-service
2... 创建带白名单判断的拦截器
java
public class FeignRequestInterceptor implements RequestInterceptor {
private final FeignWhitelistProperties whitelistProperties;
@Override
public void apply(RequestTemplate requestTemplate) {
// 获取目标服务名(需要自定义解析,可以通过 ThreadLocal 传递)
// 在调用 Feign 客户端之前,将目标服务名存入 ThreadLocal,然后在拦截器中取出来。因为 Feign 拦截器和业务代码运行在同一个线程中,所以可以通过 ThreadLocal 传递信息。
String targetService = getTargetServiceFromContext();
if (whitelistProperties.getServices().contains(targetService)) {
log.debug("服务 {} 在白名单中,跳过鉴权", targetService);
return;
}
// 不在白名单的请求,添加鉴权 Header
log.debug("非白名单路径,添加鉴权信息: {}", path);
String token = KmsPlatTokenUtil.genToken();
requestTemplate.header("Authorization", "Bearer " + token);
}
}