cookie跨域共享踩的坑
微服务在进行登录验证时,通常有这样的需求:在登录时生成一个令牌,该令牌有一定的有效期。仅在该令牌有效的情况下可以访问登录成功后的界面。如没有令牌或令牌失效,则要将网页重定向到登录界面。这种业务需求需要配置cookie跨域共享。
本篇博客需要用到spring cloud gateway,如不知道如何配置,可以看一下这篇博客:spring cloud gateway配置。
一、配置文件
查询pom.xml里是否有以下依赖文件:
<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version> <!-- 或其他旧版本 --><scope>provided</scope>
</dependency>
如果有,看一下你的java版本是否大于等于9,从Java9开始要替换成以下配置文件:
<dependency><groupId>jakarta.servlet</groupId><artifactId>jakarta.servlet-api</artifactId><scope>provided</scope></dependency>
添加配置文件后,在gateway微服务的application.yaml添加gateway配置:
spring:cloud:gateway:globalcors:cors-configurations:'[/**]':allowed-origins:- "http://localhost:8120"allowed-methods:- GET- POST- PUT- OPTIONSallowed-headers: "*"allow-credentials: trueexposedHeaders: "Set-Cookie"
二、前端代码
前端使用的是vue.js,网页把登录请求发送到gateway微服务,由gateway转发到相应微服务进行验证:
const response = await fetch('http://localhost:8110/api/login', {method: 'POST',headers: {'Content-Type': 'application/json',},credentials: 'include',body: JSON.stringify(this.formData)
});if (!response.ok) {throw new Error(`HTTP错误!状态码:${response.status}`);
}const result = await response.json();
alert('登录成功!');
// 跳转到签到页面
window.location.href = '/signIn';
登录成功后跳转到相关界面,该界面首先要进行令牌验证,相关代码如下:
async checkTokenValidity() {try {const res = await fetch('http://localhost:8110/api/validateToken', {method: 'POST',credentials: "include",headers: {'Content-Type': 'application/json'}});this.isTokenValid = res.ok;if (res.ok==false)window.location.href = '/login';} catch (err) {console.error('token验证失败:', err);this.redirectToLogin();}}
注:必须包含credentials: “include”
三、后端代码
后端需要生成jwt token,相关生成代码如下:
@Component
public class JwtConfig {private static final String SECRET_KEY = "nmPhPo7Ju/xgis3fXUyerWKVh4TrdFszIbuSnCiz4Vg="; //openssl rand -base64 32命令行生成private static final long EXPIRATION_TIME = 86400000; // 24小时(单位:毫秒)// 生成Tokenpublic String generateToken(String userId) {return Jwts.builder().setSubject(userId) // 用户唯一标识(如用户ID).setIssuedAt(new Date()) // 签发时间.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) // 过期时间.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256) // 签名算法.compact();}// 验证Tokenpublic boolean validateToken(String token) {try {Jwts.parserBuilder().setSigningKey(SECRET_KEY.getBytes()).build().parseClaimsJws(token);return true;} catch (Exception e) {return false;}}}
登录成功后,调用相关函数生成token,并存入cookie:
public ResponseEntity<Map<String, Object>> userLogin(HttpServletResponse response, @RequestBody Worker worker) {Map<String, Object> res = new HashMap<>();Worker ret = loginService.userLogin(worker.getName(), worker.getPassword());if (ret != null) {// 登录成功String userId = ret.getPid() + "";String token = jwt.generateToken(userId); // 设置HttpOnly CookieResponseCookie cookie = ResponseCookie.from("userClientToken", token).httpOnly(true).secure(false) // 开发环境允许.path("/") // 全局路径有效.maxAge(8640).build();response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());res.put("code", 200);res.put("message", "登录成功");res.put("token", token);res.put("data", Map.of("userId", ret.getPid(), "username", ret.getName()));} else {res.put("code", 401);res.put("message", "用户名或密码错误");return ResponseEntity.status(401).body(res);}return ResponseEntity.ok(res);
token验证相关代码如下:
@PostMapping("/api/validateToken")public ResponseEntity<Boolean> validateToken(HttpServletRequest request){Cookie[] cookies = request.getCookies();if (cookies == null) {System.out.print("没有cookie\n");return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();}String token = Arrays.stream(cookies).filter(c -> "userClientToken".equals(c.getName())).map(Cookie::getValue).findFirst().orElse(null);System.out.print("cookie验证\n");if (token == null || !jwt.validateToken(token)) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();}System.out.print("token有效\n");return ResponseEntity.ok().build();}