此文章主要介绍客户端token与网关API的结合的鉴权实现方式。
这种方式要求每个请求的请求头或者参数里面必须携带token,所有请求必须经过网关,有效地隐藏了微服务。
目前比较常用的生成token的方式,一种是通过MD5的方式生成token,一种是JWT:base64编码信息+签名。
如选择MD5的方式生成token,借助redis作为缓存做判断,将token作为key,登录信息作为value,设置key失效时间做登录时效。
选择JWT,设置过期时间以及登录信息进行鉴权。
先来看第一种:
MD5的方式生成tokenpublic 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"; public static final long EXPIRE_TIME = 12 * 60 * 60 * 1000; public static final long EXPIRE = 5 * 60 * 1000; public static String generateToken(String uuid, Long expire) { Map网关鉴权claims = new HashMap<>(3); claims.put(USERNAME, USERNAME_VALUE); claims.put(CREATED, new Date()); // 这里可以放一些鉴权用的信息 claims.put(AUTHORITIES, uuid); return generateToken(claims, expire); } private static Claims getClaimsFromToken(String token) { Claims claims; try { claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } catch (Exception e) { claims = null; } return claims; } 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("你的令牌无效或者过期"); } } 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("你的令牌无效或者过期"); } } private static String generateToken(Map 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 SetwhiteUrls; @Override public Mono 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); } public Mono 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()); } private Mono 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)); } @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做数据隔离。
烦请各路大神吗,指正点评一下这种方式的弊端或者需要改进的地方!
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)