此文章主要介绍客户端token与网关API的结合的鉴权实现方式。
这种方式要求每个请求的请求头或者参数里面必须携带token,所有请求必须经过网关,有效地隐藏了微服务。
目前比较常用的生成token的方式,一种是通过MD5的方式生成token,一种是JWT:base64编码信息+签名。
如选择MD5的方式生成token,借助redis作为缓存做判断,将token作为key,登录信息作为value,设置key失效时间做登录时效。
选择JWT,设置过期时间以及登录信息进行鉴权。
先来看第一种:
MD5的方式生成token
public class TokenGeneratorUtils {
public static String generateValue() {
return generateValue(UUID.randomUUID().toString());
}
private static final char[] HEX_CODE = "0123456789abcdef".toCharArray();
public static String toHexString(byte[] data) {
if (data == null) {
return null;
}
StringBuilder r = new StringBuilder(data.length * 2);
for (byte b : data) {
r.append(HEX_CODE[(b >> 4) & 0xF]);
r.append(HEX_CODE[(b & 0xF)]);
}
return r.toString();
}
public static String generateValue(String param) {
try {
MessageDigest algorithm = MessageDigest.getInstance("MD5");
algorithm.reset();
algorithm.update(param.getBytes());
byte[] messageDigest = algorithm.digest();
return toHexString(messageDigest);
} catch (Exception e) {
throw new RenException("token invalid", e);
}
}
}
登录的时候保存在redis
private String refreshToken(SmsSysUserDTO userDTO) {
//用户token
String token;
//当前时间
Date now = new Date();
//过期时间
Date expireTime = new Date(now.getTime() + EXPIRE * 1000);
//判断token是否过期
if (userDTO.getExpireTime() == null || userDTO.getExpireTime().getTime() < System.currentTimeMillis()) {
//token过期,重新生成token
token = TokenGeneratorUtils.generateValue();
} else {
token = userDTO.getToken();
}
userDTO.setToken(token);
userDTO.setExpireTime(expireTime);
userDTO.setLastLoginTime(new Date());
smsSysUserService.saveOrUpdateDTO(userDTO);
UserShareDTO userShareDTO = new UserShareDTO(userDTO.getId(), userDTO.getUsername(), token, SmsConstant.SmsSystem.SERVER.getValue());
// 缓存token
redisUtils.set(token, userShareDTO, EXPIRE);
return token;
}
JWT工具类
我的工具类主要是为了验证授权场景。
具体的业务场景:第三方系统跳转到我的系统,并且要将信息在我的系统首页展示出来,
具体的实现方案:
1、与第三方约定了一个密钥,利用AES对称加密先把信息和一个uuid通过post请求我的接口
2、我这边进行解密,解密成功则把信息存入数据库,并将uuid去封装JWT返回一个signId
3、第三方将signId和uuid代入url,前端解析url并请求我的鉴权接口,鉴权通过则拉取数据库中的信息进行展示
具体的JWT工具如下:
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名称
*/
private static final String USERNAME = Claims.SUBJECT;
public static final String USERNAME_VALUE = "ABCabc-username";
/**
* 密钥
*/
private static final String SECRET = "TESTs-jwt";
/**
* 创建时间
*/
private static final String CREATED = "created";
/**
* 权限列表
*/
private static final String AUTHORITIES = "authorities";
/**
* 有效期12小时
*/
public static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;
/**
* 有效期5分钟
*/
public static final long EXPIRE = 5 * 60 * 1000;
/**
* 生成令牌
*
* @param uuid
* @param expire
* @return
*/
public static String generateToken(String uuid, Long expire) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, USERNAME_VALUE);
claims.put(CREATED, new Date());
// 这里可以放一些鉴权用的信息
claims.put(AUTHORITIES, uuid);
return generateToken(claims, expire);
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
throw new RenException("你的令牌无效或者过期");
}
}
/**
* 根据uuid验证token
*
* @param token
* @param uuid
*/
public static void verifyToken(String token, String uuid) {
Claims claims = getClaimsFromToken(token);
if (claims == null) {
throw new RenException("你的令牌无效或者过期");
}
if (isTokenExpired(token)) {
throw new RenException("你的令牌无效或者过期");
}
String username = claims.getSubject();
if (username == null) {
throw new RenException("你的令牌无效或者过期");
}
Object authors = claims.get(AUTHORITIES);
if (authors instanceof String) {
String str = authors.toString();
if (!str.equals(uuid)) {
throw new RenException("你的令牌无效或者过期");
}
} else {
throw new RenException("你的令牌无效或者过期");
}
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @param expire 过期时间
* @return
*/
private static String generateToken(Map<String, Object> claims, long expire) {
Date expirationDate = new Date(System.currentTimeMillis() + expire);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
}
网关鉴权
以MD5生成token存redis为例:
具体步骤:
1、过滤白名单
2、从请求头中拿token作为key,请求redis
3、封装参数
4、AES加密下发请求
5、封装当前登录用户对象,通过解析请求头中的参数来获取当前登录用户对象
@Component
@RefreshScope
public class GatewayFilter implements GlobalFilter, Ordered {
@Autowired
private RedisUtils redisUtils;
/**
* 白名单
*/
@Value(value = "${white.urls}")
private Set<String> whiteUrls;
/**
* 拦截器
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
// 过滤白名单
if (null != whiteUrls.stream().filter(path::contains).findAny().orElse(null)) {
return chain.filter(exchange);
}
// 从header里取token
String token = exchange.getRequest().getHeaders().getFirst("token");
// 没有token 就阻止访问 抛未授权
if (StringUtils.isBlank(token)) {
return TokenException(exchange);
}
// 缓存token
Map tokenMap = (Map) redisUtils.get(token);
if (MapUtils.isEmpty(tokenMap)) {
return TokenException(exchange);
}
if (MapUtils.isNotEmpty(tokenMap)) {
// 封装 user
UserShareDTO user = new UserShareDTO();
user.setId(MapUtils.getLong(tokenMap, "id"));
user.setToken(MapUtils.getString(tokenMap, "token"));
user.setSystem(MapUtils.getString(tokenMap, "system"));
user.setUsername(MapUtils.getString(tokenMap, "username"));
if (StringUtils.isBlank(user.getSystem()) || StringUtils.isBlank(user.getToken())
|| StringUtils.isBlank(user.getUsername())) {
return TokenException(exchange);
}
return buildRequest(exchange, chain, JSON.toJSONString(user));
}
return chain.filter(exchange);
}
/**
* 认证信息用AES加密 下发请求
*
* @param exchange
* @param chain
* @param auth
* @return
*/
public Mono<Void> buildRequest(ServerWebExchange exchange, GatewayFilterChain chain, String auth) {
String userAESStr = AESUtils.AESencode("abcdssdfsdfsafq", auth);
ServerHttpRequest request = exchange.getRequest().mutate()
.header("userInfo", userAESStr)
.build();
return chain.filter(exchange.mutate().request(request).build());
}
/**
* token失效或者过期
*
* @param exchange
* @return
*/
private Mono<Void> TokenException(ServerWebExchange exchange) {
//1.获取响应对象
ServerHttpResponse response = exchange.getResponse();
//2.设置响应头类型
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(HttpStatus.OK);
JSONObject jsonObject = new JSONObject();
jsonObject.put("msg", "你的令牌无效或者过期");
jsonObject.put("code", HttpStatus.UNAUTHORIZED.value());
jsonObject.put("data", false);
DataBuffer wrap = response.bufferFactory().wrap(jsonObject.toJSONString().getBytes());
return response.writeWith(Flux.just(wrap));
}
/**
* 拦截器的顺序
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
当前登录用户对象工具
@Slf4j
public class ShareUtils {
public static UserShareDTO user;
public static UserShareDTO getUser() {
updateInfo();
return user;
}
public static synchronized void updateInfo() {
try {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String userInfoStr = request.getHeader("userInfo");
String userInfo = AESUtils.AESdecode("abcdssdfsdfsafq", userInfoStr);
if (StringUtils.isNotBlank(userInfo)) {
user = JSON.parseObject(userInfo, UserShareDTO.class);
} else {
user = new UserShareDTO();
}
log.info("ShareUtils UserShareDTO ", user);
} catch (Exception e) {
log.error("ShareUtils HttpServletRequest request is null: " + e.getMessage());
user = new UserShareDTO();
}
}
}
如果用这种鉴权方式,在feign调用的时候需要重写feign拦截器,会出现请求头丢失的情况
具体的解决方案可以参考我的这篇文章
我个人觉得这种鉴权方式灵活性较大,可以在网关redis鉴权后,通过参数的形式区分客户端、服务端或者内部请求、外部请求等等。同时可以结合mybaitis-plus做数据隔离。
烦请各路大神吗,指正点评一下这种方式的弊端或者需要改进的地方!