本文共 4880 字,大约阅读时间需要 16 分钟。
在商场停车场景中,除了极少数功能不需要用户登录外(如可用车位数),其他功能均需要用户在会话状态下才能正常使用。为实现统一的认证操作,本文将介绍在网关层增加一个公共鉴权功能,采用轻量级解决方案JWT(JSON Web Token)进行认证。
JSON Web Token(JWT)是一种流行的轻量级跨域认证解决方案。Tomcat的Session方式在分布式环境和多实例多应用场景下并不适用。JWT按一定规则生成并解析,无需存储,只需验证即可,相比于Session的存储方式,JWT的优势凸显得多。此外,JWT生成后只要不过期就可正常使用,但这也带来了一个潜在问题:一旦生成,token无法更改,需要借助第三方手段配置token验证,防止被别有用意的人员利用。JWT的无状态性特点使得服务端实例能够更好地扩展,避免了状态维护带来的额外开销。
会话主动退出
需要结合第三方解决方案(如Redis)来完成。会话主动退出时,将token写入缓存中,后续请求在网关层验证时,先判定缓存中是否存在,若存在则证明token无效,提示用户重新登录。用户持续在线但JWT失效
假设JWT有效期为30分钟,如果用户持续在线,直接在30分钟后强制用户登录会影响用户体验。依照Session方式,只要用户活跃,有效期就要延长。然而JWT本身无法更改,这时需要刷新JWT来保证体验流畅性。方案如下:当检测到即将过期或已经过期但用户仍在活跃状态时,生成新token返回给前端,使用新的token进行请求,直到主动退出或失效退出。io.jsonwebtoken jjwt 0.9.1
@Slf4jpublic class JWTUtils { private static final SecretKey SECRET_KEY = new SecretKeySpec(Base64.decodeBase64("your-key-here"), "AES"); public static String createJWT(String id, String subject, long ttlMillis, String key) throws Exception { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); JwtBuilder builder = Jwts.builder() .setIssuer("") .setId(id) .setIssuedAt(now) .setSubject(subject) .signWith(signatureAlgorithm, SECRET_KEY); if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); } return builder.compact(); } public static Claims parseJWT(String jwt, String key) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException { SecretKey key = new SecretKeySpec(Base64.decodeBase64(key), "AES"); Claims claims = Jwts.parser() .setSigningKey(key) .parseClaimsJws(jwt) .getBody(); return claims; } public static boolean isTokenExpire(String jwt, String key) { Claims aClaims = parseJWT(jwt, key); if (LocalDateTime.now().isAfter( LocalDateTime.now() .with(aClaims.getExpiration().toInstant().atOffset(ZoneOffset.ofHours(8)).toLocalDateTime()))) { return true; } else { return false; } }} @Component@Order(-200)public class JWTFilter implements GlobalFilter { @Autowired private JWTData jwtData; private ObjectMapper objectMapper = new ObjectMapper(); @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String url = exchange.getRequest().getURI().getPath(); if (jwtData.getSkipUrls() != null && Arrays.asList(jwtData.getSkipUrls()).contains(url)) { return chain.filter(exchange); } String token = exchange.getRequest().getHeaders().getFirst("token"); ServerHttpResponse resp = exchange.getResponse(); if (StringUtils.isEmpty(token)) { return authError(resp, "请先登录!"); } else { try { JWTUtils.parseJWT(token, jwtData.getTokenKey()); log.info("验证通过"); return chain.filter(exchange); } catch (ExpiredJwtException e) { log.error("token过期", e); return authError(resp, "token过期"); } catch (Exception e) { log.error("认证失败", e); return authError(resp, "认证失败"); } } } private Mono authError(ServerHttpResponse resp, String message) { resp.setStatusCode(HttpStatus.UNAUTHORIZED); resp.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); CommonResult returnData = new CommonResult<>(HttpStatus.SC_UNAUTHORIZED); returnData.setRespMsg(message); String returnStr = ""; try { returnStr = objectMapper.writeValueAsString(returnData.getRespMsg()); } catch (JsonProcessingException e) { log.error("JSON转换错误", e); } DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8)); return resp.writeWith(Flux.just(buffer)); } @Override public int getOrder() { return -200; }} jwt: token-key: your-key-here skip-urls: - /member-service/member/bindMobile - /member-service/member/logout@Data@ConfigurationProperties(prefix = "jwt")public class JWTData { public String tokenKey; private String[] skipUrls;} 本次测试主要验证token的可用性。通过Postman工具,使用生成的正常token进行“商场用户日常签到功能请求”,验证请求是否成功。
以上方案提供了一个轻量级的网关鉴权解决方案,虽然简单但实用。在应对复杂场景时,还需结合其他组件或功能来加固服务安全性。例如,鉴权通过后,确定哪些功能有权操作,这在管理系统中很常见,但本案例中未体现。可以尝试增加角色权限配置来验证,加深对JWT的理解。
转载地址:http://jmhfk.baihongyu.com/