前端:鸿蒙开发ArkTs语言
后端:spring boot mybatis-plus框架
后端代码
@PostMapping("/sendMsg")public R<String> sendMsg(@RequestBody User user, HttpServletRequest request, HttpServletResponse response) {// 从User对象中提取用户手机号String phone = user.getPhone();// 验证手机号是否不为空if (StringUtils.isNotEmpty(phone)) {// 生成4位随机验证码String code = ValidateCodeUtils.generateValidateCode(4).toString();// 获取或创建Session,并获取Session IDHttpSession session = request.getSession(true);String sessionId = session.getId();// 发送短信验证码,使用第三方服务或APISMSUtils.sendMessage("鸿蒙开发", "SMS_475965454", phone, code);// 将验证码存储在Session中,以便后续验证session.setAttribute("SMS_CODE_" + phone, code);// 设置Session的超时时间为300秒(5分钟)session.setMaxInactiveInterval(300);// 记录日志,包含手机号、验证码和Session IDlog.info("Stored code in session - Phone: {}, Code: {}, SessionId: {}",phone, code, sessionId);// 创建JSESSIONID Cookie,用于维持会话状态Cookie cookie = new Cookie("JSESSIONID", sessionId);// 设置Cookie的路径,使得整个应用都可以访问该Cookiecookie.setPath("/");// 设置Cookie的HttpOnly属性为true,增加安全性cookie.setHttpOnly(true);// 将Cookie添加到响应中response.addCookie(cookie);// 返回成功的响应,表示短信验证码已发送return R.success("手机验证码短信发送成功");}// 如果手机号为空,返回失败的响应return R.error("发送失败");}/*** 处理手机号验证码登录的请求。* 该方法处理HTTP POST请求到"/loginByPhone"路径,接收登录参数和HTTP会话,返回登录结果。** @param map 包含登录参数的Map对象,其中应包含手机号和验证码。* @param session HTTP会话对象,用于验证验证码和存储用户信息。* @return R<User> 封装了登录结果的响应对象,包含登录成功或失败的信息。*/@PostMapping("/loginByPhone")public R<User> loginByPhone(@RequestBody Map<String, String> map, HttpSession session) {// 从请求体中提取手机号和验证码String phone = map.get("phone");String code = map.get("code");// 记录尝试登录的日志信息log.info("Attempting phone login - Phone: {}, Input Code: {}", phone, code);// 1. 验证手机号和验证码非空// 检查手机号和验证码是否为空,如果为空返回错误信息if (StringUtils.isEmpty(phone) || StringUtils.isEmpty(code)) {return R.error("手机号或验证码不能为空");}// 2. 验证码校验// 从会话中获取与手机号关联的验证码Object codeInSession = session.getAttribute("SMS_CODE_" + phone);// 记录验证码校验的日志信息log.info("Phone: {}, Input Code: {}, Stored Code: {}", phone, code, codeInSession);// 检查会话中是否存在验证码,如果不存在返回验证码过期的错误信息if (codeInSession == null) {log.warn("No verification code found in session for phone: {}", phone);return R.error("验证码已过期,请重新获取");}// 比较输入的验证码和会话中存储的验证码,如果不一致返回验证码错误信息if (!codeInSession.toString().equals(code)) {log.warn("Invalid verification code. Expected: {}, Got: {}", codeInSession, code);return R.error("验证码错误");}// 3. 查询用户是否存在// 创建查询包装器,设置查询条件为手机号等于请求中的手机号LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.eq(User::getPhone, phone);// 根据查询条件获取用户信息User user = userService.getOne(queryWrapper);// 4. 不存在则创建新用户// 如果用户不存在,创建新用户对象,并设置手机号和随机生成的用户名if (user == null) {user = new User();user.setPhone(phone);// 生成随机用户名(手机号后4位+随机4位数字)String randomUsername = "user_" + phone.substring(7) +String.format("%04d", new Random().nextInt(10000));user.setUsername(randomUsername);// 使用默认密码123456,并加密String defaultPassword = "123456";user.setPassword(CustomPasswordEncoder.encode(defaultPassword)); // 确保使用加密user.setStatus(1); // 设置用户状态为启用// 保存用户信息userService.save(user);// 记录新用户创建的日志信息log.info("New user created - Phone: {}, Username: {}, Default password set", phone, randomUsername);// 设置credentials信息,包含用户名和默认密码Map<String, String> credentials = new HashMap<>();credentials.put("username", randomUsername);credentials.put("password", defaultPassword); // 返回未加密的默认密码user.setCredentials(credentials);}// 5. 清除验证码// 从会话中移除验证码,确保验证码只能使用一次session.removeAttribute("SMS_CODE_" + phone);// 6. 将用户ID存入Session// 将用户ID存储在会话中,以便后续请求可以识别用户session.setAttribute("user", user.getId());// 记录用户登录成功的日志信息log.info("User logged in successfully - Phone: {}, UserId: {}", phone, user.getId());// 返回成功的响应,包含用户信息return R.success(user);}
前端model代码
/*** 使用手机号和验证码进行登录的方法。* 该方法向服务器发送POST请求,包含手机号和验证码,以验证用户身份。** @param phone 手机号* @param code 验证码* @return Promise<UserInfo> 一个Promise对象,包含登录结果,成功时解析为用户信息,失败时拒绝。*/loginByPhone(phone: string, code: string): Promise<UserInfo> {return new Promise((resolve, reject) => {// 创建HTTP请求实例let httpRequest = http.createHttp();// 从存储中获取保存的cookieconst sessionCookie = AppStorage.Get<string>('sessionCookie');// 调用getRequestOptions方法来设置请求选项,并附加登录数据const requestOptions = this.getRequestOptions(http.RequestMethod.POST, {phone: phone,code: code});// 如果存在cookie,则添加到请求头中if (sessionCookie) {requestOptions.header['Cookie'] = sessionCookie;}// 打印请求选项的日志信息console.log('Login request options:', requestOptions);// 发起HTTP请求到后端的/loginByPhone端点httpRequest.request(`${this.baseURL}/loginByPhone`,requestOptions).then(resp => {// 打印响应的日志信息console.log('Login response:', resp);// 如果响应代码是200,表示请求成功if (resp.responseCode === 200) {// 解析响应体中的JSON数据const responseData = JSON.parse(resp.result.toString());// 如果响应码是1,表示操作成功if (responseData.code === 1) {// 保存登录状态到本地存储AppStorage.SetOrCreate('isLoggedIn', true);AppStorage.SetOrCreate('userInfo', JSON.stringify(responseData.data));// 解决Promise并返回用户信息resolve(responseData.data);} else {// 如果响应码不是1,拒绝Promise并返回错误信息reject(responseData.msg || '登录失败');}} else {// 如果响应代码不是200,拒绝Promise并返回登录失败信息reject('登录失败');}}).catch(error => {// 如果请求过程中出现错误,打印错误日志并拒绝Promiseconsole.error('Login error:', error);reject(error);}).finally(() => {// 不管请求成功与否,最终都要销毁HTTP请求实例,释放资源httpRequest.destroy();});});}/*** 发送验证码的方法。* 该方法向服务器发送POST请求,请求发送验证码到指定的手机号。** @param phone 接收验证码的手机号* @return Promise<void> 一个Promise对象,表示发送验证码操作的异步结果。*/sendVerificationCode(phone: string): Promise<void> {return new Promise((resolve, reject) => {// 创建HTTP请求实例let httpRequest = http.createHttp();// 打印日志,显示正在向哪个手机号发送验证码console.log('Sending code to phone:', phone);// 向服务器的/sendMsg端点发送POST请求,请求发送验证码httpRequest.request(`${this.baseURL}/sendMsg`,// 使用之前定义的getRequestOptions方法来设置请求选项,并附加手机号信息this.getRequestOptions(http.RequestMethod.POST, { phone: phone })).then(resp => {// 打印日志,显示发送验证码的响应结果console.log('Send code response:', resp);// 从响应头中获取Set-Cookie字段,保存登录会话的cookieconst cookies = resp.header['set-cookie'];if (cookies) {console.log('Received cookies:', cookies);// 将cookie存储到应用存储中,以便后续请求使用AppStorage.SetOrCreate('sessionCookie', cookies[0]);}// 如果响应代码是200,表示请求成功if (resp.responseCode === 200) {// 解析响应体中的JSON数据const responseData = JSON.parse(resp.result.toString());// 如果响应码是1,表示操作成功if (responseData.code === 1) {// 解决Promiseresolve();} else {// 如果响应码不是1,拒绝Promise并返回错误信息reject(responseData.msg || '发送验证码失败');}} else {// 如果响应代码不是200,拒绝Promise并返回发送验证码失败信息reject('发送验证码失败');}}).catch(error => {// 如果请求过程中出现错误,打印错误日志并拒绝Promiseconsole.error('Send code error:', error);reject(error);}).finally(() => {// 不管请求成功与否,最终都要销毁HTTP请求实例,释放资源httpRequest.destroy();});});}
发送验证码流程详解
前端发送验证码请求 (客户端)
- 用户输入手机号
- 调用 sendVerificationCode(phone) 方法
- 构造 POST 请求到 /sendMsg 端点
- 携带手机号信息
后端处理验证码发送 (服务端) - 接收到前端请求后,@PostMapping(“/sendMsg”) 方法被触发
- 验证手机号是否非空
- 生成4位随机验证码
- 关键操作 - 会话管理:
○ 创建 HttpSession
○ 获取 sessionId
○ 将验证码存储在 Session 中,键为 “SMS_CODE_” + phone
○ 设置 Session 超时时间为 5 分钟
会话安全机制
● 使用 HttpSession 存储验证码
● 生成唯一的 sessionId
● 创建 JSESSIONID Cookie
● Cookie 设置为 HttpOnly,防止跨站脚本攻击
● Cookie 路径设置为 “/”,整个应用可访问
发送短信
● 调用 SMSUtils.sendMessage() 发送验证码
● 使用第三方短信服务
● 记录发送日志
手机号登录流程详解
前端登录请求
1.用户输入手机号和收到的验证码 - 调用 loginByPhone(phone, code) 方法
- 构造 POST 请求到 /loginByPhone 端点
- 携带手机号和验证码
- 附带之前获得的 sessionCookie
后端登录验证 (服务端) - 接收到前端请求
- 验证码校验流程:
○ 检查手机号和验证码是否为空
○ 从 Session 中获取存储的验证码
○ 比较输入的验证码与存储的验证码
○ 验证通过后,立即删除 Session 中的验证码(一次性) - 用户处理:
○ 根据手机号查询用户是否存在
○ 不存在则自动创建新用户
■ 生成随机用户名
■ 设置默认密码
■ 保存用户信息 - 登录成功后:
○ 将用户 ID 存入 Session
○ 返回用户信息
Session ID 一致性的关键机制
会话同步关键步骤
- 发送验证码时:
HttpSession session = request.getSession(true);
String sessionId = session.getId();
○ 创建/获取 Session
○ 生成唯一 sessionId
2. 设置 Cookie:
Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
○ 将 sessionId 作为 Cookie 值
○ 设置 Cookie 路径为全局
○ 设置 HttpOnly 增加安全性
3. 前端保存 Cookie:
const cookies = resp.header['set-cookie'];
if (cookies) {AppStorage.SetOrCreate('sessionCookie', cookies[0]);
}
○ 保存服务端返回的 Cookie
○ 后续请求携带此 Cookie
4. 登录时附带 Cookie:
const sessionCookie = AppStorage.Get<string>('sessionCookie');
if (sessionCookie) {requestOptions.header['Cookie'] = sessionCookie;
}
○ 登录请求携带原始 Cookie
○ 确保 sessionId 一致
在这个手机验证登录流程中,Cookie(特别是JSESSIONID Cookie)扮演了非常重要的会话管理和状态维持的角色。
- Cookie的基本目的 在这个应用程序中,Cookie主要用于在客户端和服务器之间维持会话状态。具体来说,JSESSIONID Cookie负责在不同的HTTP请求之间保持用户的会话标识。
- 在服务端发送Cookie的过程 在sendMsg方法中,服务器明确创建并发送了一个Cookie:
// 创建JSESSIONID Cookie,用于维持会话状态
Cookie cookie = new Cookie("JSESSIONID", sessionId);
// 设置Cookie的路径,使得整个应用都可以访问该Cookie
cookie.setPath("/");
// 设置Cookie的HttpOnly属性为true,增加安全性
cookie.setHttpOnly(true);
// 将Cookie添加到响应中
response.addCookie(cookie);
这段代码做了几个关键的事情:
● 创建了一个名为"JSESSIONID"的Cookie
● 设置Cookie的值为当前会话的Session ID
● 将Cookie的路径设置为"/",意味着整个应用都可以访问这个Cookie
● 设置了HttpOnly标志,防止客户端脚本访问Cookie,提高安全性
- 客户端处理Cookie的方式 在客户端的sendVerificationCode方法中,可以看到对Cookie的处理:
// 从响应头中获取Set-Cookie字段,保存登录会话的cookie
const cookies = resp.header['set-cookie'];
if (cookies) {console.log('Received cookies:', cookies);// 将cookie存储到应用存储中,以便后续请求使用AppStorage.SetOrCreate('sessionCookie', cookies[0]);
}
这里的处理逻辑是:
● 从响应头中提取Cookie
● 将Cookie保存在应用存储中
● 在后续的请求(如登录)中,会将这个Cookie发送回服务器
- 确保Session ID一致的机制 通过这种方式,客户端和服务器可以保持Session ID的一致性:
● 服务器在sendMsg方法中生成一个唯一的Session ID
● 将这个Session ID作为Cookie的值发送给客户端
● 客户端在后续请求(如loginByPhone)中携带这个Cookie
● 服务器可以通过Cookie中的Session ID识别和恢复之前的会话上下文 - 安全和会话管理的附加措施
● 服务器设置了Session的超时时间:session.setMaxInactiveInterval(300),即5分钟
● 验证码使用后会立即从Session中移除:session.removeAttribute(“SMS_CODE_” + phone)
● 使用HttpOnly和设置路径等方式增加Cookie的安全性
●
1. 实际流程示例 完整的流程大致是: a. 用户请求发送验证码 b. 服务器生成验证码并创建Session c. 服务器在响应中发送包含Session ID的Cookie d. 客户端保存这个Cookie e. 用户输入验证码进行登录 f. 客户端在登录请求中携带之前保存的Cookie g. 服务器通过Cookie中的Session ID验证会话和验证码
通过这种精心设计的机制,应用程序能够在无状态的HTTP协议上实现有状态的会话管理,确保验证码的安全性和登录流程的完整性。
// 从会话中获取与手机号关联的验证码
Object codeInSession = session.getAttribute("SMS_CODE_" + phone);
这里的关键点是:
- session 对象是直接从 HttpSession 获取的
- 服务器能获取到 session 对象,意味着 Session ID 已经被正确识别
- 通过 getAttribute() 方法,从会话中获取之前存储的验证码
具体验证流程:
● 服务器使用请求中的 Session ID(通过 Cookie 传递)找到对应的 Session
● 从 Session 中检索特定手机号的验证码
● 将请求中的验证码与 Session 中存储的验证码进行比较
换句话说,服务器通过 Cookie 中的 Session ID 自动定位到正确的会话,这个过程是框架(如 Spring)和 Servlet 容器自动处理的,开发者不需要手动编写额外的 Session ID 验证代码。
Cookie在这个过程中就像是一个安全的、临时的身份标记,帮助服务器在多个请求中识别和管理同一个用户会话。
在这个程序中,Cookies对于确保Session ID一致性起到了至关重要的作用。:
- Session ID的本质 Session ID是一个唯一的标识符,用于在服务器端识别和跟踪特定的用户会话。在这个程序中,Session ID用于关联发送验证码和登录的一系列操作。
- Cookies在Session ID一致性中的具体作用 在发送验证码的服务器端代码中,有这样一段关键逻辑:
// 获取或创建Session,并获取Session ID
HttpSession session = request.getSession(true);
String sessionId = session.getId();// 创建JSESSIONID Cookie,用于维持会话状态
Cookie cookie = new Cookie("JSESSIONID", sessionId);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
这段代码的作用是:
● 创建一个会话(Session)
● 获取这个会话的唯一标识符(Session ID)
● 将Session ID作为Cookie的值发送到客户端
- 会话一致性的具体机制 当客户端在后续的登录请求中携带这个Cookie时:
// 从存储中获取保存的cookie
const sessionCookie = AppStorage.Get<string>('sessionCookie');
const requestOptions = this.getRequestOptions(http.RequestMethod.POST, {phone: phone,code: code
});// 如果存在cookie,则添加到请求头中
if (sessionCookie) {requestOptions.header['Cookie'] = sessionCookie;
}
服务器端会通过这个Cookie来识别和恢复之前的会话状态。
- 为什么需要这种机制? 在无状态的HTTP协议中,每个请求都是独立的。Cookies提供了一种在多个请求之间维持状态的方法。具体来说:
● 发送验证码时,创建一个Session并生成Session ID
● 将Session ID通过Cookie发送到客户端
● 登录时,客户端将这个Cookie发送回服务器
● 服务器通过Cookie中的Session ID找到对应的Session
● 验证之前存储在Session中的验证码 - 一致性保证的关键步骤
● 服务器为每个会话生成唯一的Session ID
● 通过Cookie将Session ID传递给客户端
● 客户端在后续请求中携带这个Cookie
● 服务器根据Cookie中的Session ID恢复会话 - 安全性考虑
● HttpOnly标志防止客户端脚本访问Cookie
● 设置Cookie路径为"/",使其在整个应用可用
● Session设置5分钟超时
● 验证码使用后立即失效
通过这种机制,Cookie确保了:
● 发送验证码和登录请求属于同一个会话
● 验证码只能使用一次
● 会话状态在多个请求间保持一致
想象一下,Cookie就像是一个特殊的身份标签,在用户的整个验证流程中始终跟随并识别用户,确保从发送验证码到最终登录的每一步都能准确地关联起来。
解决Session不一致问题的方法总结:
1.Cookie管理
前端统一管理Cookie存储
每次请求都携带相同的Cookie
在发送验证码时保存Cookie
后续请求复用相同Cookie
2.Session配置
合理配置Session超时时间
设置正确的Cookie属性
统一Session存储方式
配置跨域时允许携带凭证
3.请求处理
使用统一的请求拦截器
确保请求头包含必要信息
正确处理响应头中的Cookie
维护请求的一致性
4.日志记录
记录Session创建和销毁
记录Cookie的变化
记录验证码存储和验证过程
便于问题定位和调试
5.错误处理
统一的Session过期处理
友好的错误提示
完善的异常捕获
合理的重试机制
6. 最佳实践
避免频繁创建新Session
及时清理过期Session
合理使用Session存储
定期检查Session状态
通过以上方法的组合使用,可以有效解决Session不一致的问题。