
1. 项目概述当企业级后台遇上移动端登录如何优雅“上车”做企业级后台开发的朋友对若依RuoYi这个开源项目应该都不陌生。它那套基于Spring Boot Vue的前后端分离架构配上丰富的权限控制和代码生成器让很多中后台项目的开发效率直接拉满。但不知道你有没有遇到过这样的需求客户说我们内部的管理系统能不能也搞个移动端让员工在外面用手机也能处理个审批、查个数据更具体一点客户希望这个移动端能直接集成到他们的企业微信公众号里员工点开公众号菜单就能用并且自动用微信身份登录省去记账号密码的麻烦。这个需求听起来很合理但真动手把RuoYi这套后台“搬”到移动端尤其是要对接微信公众号授权登录里面门道可不少。这不仅仅是把Vue页面用uni-app重写一遍那么简单它涉及到整个前端技术栈的切换、用户认证体系的改造、以及与微信生态的深度集成。今天我就结合最近刚做完的一个“RuoYi-App移动版uni-app微信公众号授权登录”项目来拆解一下这里面的核心思路、技术选型、实操步骤以及我踩过的那些“坑”。无论你是正在考虑为现有后台系统开发移动端还是单纯想了解uni-app与微信公众号登录的整合相信这篇都能给你带来一些直接的参考。2. 整体架构设计与思路拆解2.1 为什么是“uni-app 微信公众号”这个组合接到移动端需求时技术选型是第一个要面对的问题。原生开发iOS/Android成本高维护两套代码不现实纯H5体验和能力受限特别是调用微信JSSDK时。uni-app的出现几乎是为这个场景量身定做。首先uni-app基于Vue.js这对于原本使用Vue作为前端框架的RuoYi项目来说技术栈一致组件化思想和大部分语法可以平滑迁移极大降低了前端同学的学习成本和开发门槛。我们甚至可以将一些通用的工具函数、API请求封装直接复用。其次“一套代码多端发布”是uni-app的核心优势。我们编写一次代码不仅可以编译成H5用于微信公众号内的网页未来如果客户有需求还能快速发布成微信小程序、AppAndroid/iOS这种灵活性对于探索性项目或需求可能变化的业务非常友好。最后也是最重要的一点对微信生态的良好支持。uni-app提供了完善的 uni-login 和 uni-open-bridge 等插件封装了微信登录、分享、支付等复杂接口让我们能以更统一、简洁的方式调用微信能力避免了直接操作微信JSSDK时那些繁琐的配置和兼容性问题。所以选择uni-app来承载RuoYi的移动端并利用其能力对接微信公众号网页授权登录是一个在开发效率、体验一致性、未来扩展性上都非常平衡的方案。2.2 用户登录流程的重构从账号密码到OAuth2.0RuoYi原生的后台登录是基于“用户名/密码 验证码”的模式后端生成一个JWT或Session令牌来维持会话。但在微信公众号环境里这套流程行不通。用户期望的是“点击即登录”无需输入任何信息。这就需要我们将认证流程升级为基于OAuth2.0授权码模式的第三方登录。具体到微信就是其网页授权OAuth2.0流程。核心变化在于认证发起方从用户主动填写表单变为由我们的前端页面引导用户跳转到微信的授权页面。凭证交换从校验密码变为用微信返回的code去交换access_token和openid。用户标识从系统内部的user_id变为与微信用户唯一绑定的openid或unionid。我们的后端RuoYi需要新增一个专门处理微信OAuth回调的接口。前端uni-app的角色是引导流程、携带参数、并最终用获取到的微信用户信息如openid去调用我们自己的后端登录接口换取系统内部的登录态Token。2.3 技术栈与职责划分基于以上思路整个项目的技术栈和职责变得清晰前端 (uni-app):使用Vue 3 TypeScript Vite或Webpack作为开发基础保持技术先进性。利用uni.login或uni.getUserProfile需结合业务场景来简化微信登录流程的触发。负责构建所有移动端页面并通过uni.request与后端API交互。处理本地Token的存储、携带和过期刷新逻辑。后端 (RuoYi / Spring Boot):保持原有的用户、角色、权限管理体系不变这是业务核心。新增/auth/wechat相关接口包括生成前端跳转所需的微信授权URL包含appid,redirect_uri,state等参数。接收微信回调的code和state并用code、appid、secret去微信服务器换取access_token和openid。根据openid查询或创建本地用户并生成系统自身的JWT Token返回给前端。原有的权限拦截器(SecurityConfig)需要兼容两种登录方式传统的Authorization: Bearer token和从微信回调链路中建立的会话。微信公众平台:提供appid和secret。提供标准的OAuth2.0授权端点。验证我们的服务器并交换用户凭证。3. 核心细节解析与实操要点3.1 微信公众号配置那些容易忽略的“坑”在写代码之前80%的问题可能出在微信公众号的配置上。这里有几个关键点我敢说很多人都踩过坑。1. 网页授权域名必须是备案域名且精确匹配在公众号后台的“设置与开发” - “公众号设置” - “功能设置”里找到“网页授权域名”。这里填写的域名必须是已经完成ICP备案的并且不能带http://或https://协议头也不能带端口号默认80/443。例如你的前端H5页面最终部署在https://m.yourcompany.com那么这里就填m.yourcompany.com。注意这里配置的是前端页面所在域名而不是后端接口域名。微信在重定向回你的页面时会校验当前页面URL的根域名是否在此白名单内。2. 安全域名、业务域名与JS接口安全域名这几个概念容易混淆网页授权域名如上所述用于OAuth跳转授权。JS接口安全域名如果你想在页面中使用微信JSSDK进行分享、拍照等操作需要在这里配置。它可以是子域名但同样需要备案。业务域名仅小程序相关本项目是H5不涉及。 对于我们的登录场景主要确保“网页授权域名”配置正确即可。但如果后续页面需要调用更多微信JSAPI则“JS接口安全域名”也必须配置。3. AppID和AppSecret妥善保管AppID是公开的会写在前端代码里虽然理论上可以后端动态下发但通常直接写死问题不大。但AppSecret是绝密绝对不能出现在前端代码、客户端配置或版本库中。它必须只存在于后端服务器的环境变量或配置中心。泄露AppSecret意味着攻击者可以冒充你的公众号调用所有API。3.2 前端(uni-app)登录流程实现细节在uni-app中我们有两种主要方式触发微信登录选择哪种取决于业务对用户信息的需求。方案一静默登录scope为snsapi_base这种方式只获取用户的openid无需用户点击确认授权按钮无感完成。适合只需要标识用户身份不需要昵称、头像等信息的场景。// 在需要登录的页面如首页的 onLoad 或 mounted 中 uni.login({ provider: weixin, onlyAuthorize: true, // 仅做授权不获取用户信息 success: (loginRes) { // loginRes.code 就是微信的临时授权码 console.log(微信授权码:, loginRes.code); // 将这个 code 发送给自己的后端服务 this.exchangeCodeForToken(loginRes.code); }, fail: (err) { console.error(微信登录失败:, err); uni.showToast({ title: 登录失败请重试, icon: none }); } });方案二手动授权登录scope为snsapi_userinfo这种方式会弹出一个授权窗口请求获取用户的昵称、头像、地区等信息。用户可能会拒绝。// 通常由一个按钮触发 async handleWechatLogin() { try { // 1. 获取用户信息会弹窗 const userInfoRes await uni.getUserProfile({ provider: weixin, desc: 用于完善会员信息 // 必填声明用途 }); // 2. 获取登录凭证 const loginRes await uni.login({ provider: weixin }); // 此时我们拥有了 code 和 userInfo const params { code: loginRes.code, rawData: userInfoRes.rawData, // 原始数据可用于后端验证签名 signature: userInfoRes.signature, iv: userInfoRes.iv, // 如果加密了需要传 encryptedData: userInfoRes.encryptedData }; // 3. 将 code 和用户信息一起发送给后端 this.exchangeCodeForToken(params); } catch (error) { console.error(授权或登录失败:, error); } }实操心得除非业务强依赖用户昵称头像如社区、社交否则优先使用静默登录snsapi_base。用户体验更好转化率更高。用户信息可以在登录后通过openid去后台查询或者引导用户在个人中心手动完善。3.3 后端(Spring Boot)接口设计与安全考量后端需要新增两个核心接口接口1获取微信授权URL这个接口的目的是让前端能够动态获取跳转地址。虽然前端也可以拼接但后端控制更灵活可以方便地添加state参数防CSRF攻击。RestController RequestMapping(/auth/wechat) public class WeChatAuthController { Value(${wechat.app-id}) private String appId; Value(${wechat.redirect-uri}) private String redirectUri; // 编码后的回调地址 GetMapping(/auth-url) public AjaxResult getAuthUrl(RequestParam String frontendRedirect) { // 生成一个随机的state并存入缓存如Redis键可以为 state_xxx String state UUID.randomUUID().toString(); redisTemplate.opsForValue().set(wechat:state: state, frontendRedirect, 5, TimeUnit.MINUTES); // 构建微信授权URL (snsapi_base 静默授权) String url String.format( https://open.weixin.qq.com/connect/oauth2/authorize?appid%sredirect_uri%sresponse_typecodescopesnsapi_basestate%s#wechat_redirect, appId, URLEncoder.encode(redirectUri, StandardCharsets.UTF_8), state ); return AjaxResult.success(success, url); } }为什么需要state和frontendRedirectstate微信授权后会原样带回。后端在回调接口中校验此state是否有效存在于Redis中可以有效防止CSRF攻击。frontendRedirect前端传入一个登录成功后的跳转地址如/pages/home/index。后端将其与state绑定。在回调接口处理完微信登录后知道该将用户重定向回哪个具体的H5页面而不是固定的首页体验更佳。接口2处理微信回调并登录这个接口地址就是在微信公众号配置的“网页授权域名”下的一个具体路径例如https://m.yourcompany.com/api/auth/wechat/callback。GetMapping(/callback) public void callback(RequestParam String code, RequestParam String state, HttpServletResponse response) throws IOException { // 1. 校验state防止CSRF String frontendRedirect (String) redisTemplate.opsForValue().get(wechat:state: state); if (StringUtils.isEmpty(frontendRedirect)) { response.sendRedirect(/#/pages/error/404); // 跳转到错误页 return; } redisTemplate.delete(wechat:state: state); // 使用后立即删除 // 2. 用code换取access_token和openid String url String.format( https://api.weixin.qq.com/sns/oauth2/access_token?appid%ssecret%scode%sgrant_typeauthorization_code, appId, appSecret, code ); // 使用RestTemplate或HttpClient请求微信接口 WechatAccessTokenResp tokenResp restTemplate.getForObject(url, WechatAccessTokenResp.class); if (tokenResp null || tokenResp.getErrcode() ! null) { // 换取token失败记录日志跳转错误页 log.error(微信换取access_token失败: {}, tokenResp); response.sendRedirect(/#/pages/error/auth-failed); return; } String openid tokenResp.getOpenid(); String accessToken tokenResp.getAccessToken(); // 3. 根据openid查找或创建本地用户 SysUser user userService.findOrCreateByWechatOpenid(openid, accessToken); // 这个方法需要自己实现 // 4. 为用户生成系统内部的JWT Token String jwtToken tokenService.createToken(user); // 5. 将Token传递给前端H5页面。这里有两种常见方式 // 方式A重定向到前端页面Token放在URL Fragment或Query中有泄露风险需HTTPS且短期有效 // 方式B推荐重定向到前端页面同时设置一个CookieHttpOnly, Secure, SameSiteStrict // 这里演示方式B简化版实际需考虑跨域、域名等 Cookie tokenCookie new Cookie(Admin-Token, jwtToken); tokenCookie.setPath(/); tokenCookie.setHttpOnly(true); // 防止XSS读取 tokenCookie.setSecure(true); // 仅HTTPS传输 tokenCookie.setMaxAge(7 * 24 * 60 * 60); // 7天与JWT过期时间对齐 response.addCookie(tokenCookie); // 6. 重定向回前端最初想去的页面 response.sendRedirect(/# frontendRedirect); }安全警告将Token通过URL传递是下策容易被浏览器历史记录、Referer头泄露。通过HttpOnly的Cookie携带是最安全的方式之一但需要确保前后端域名一致或正确配置CORS和Cookie作用域。4. 实操过程与核心环节实现4.1 环境搭建与项目初始化假设我们已经有一个标准的RuoYi后端项目在运行。前端部分我们使用HBuilderX或命令行创建uni-app项目。# 使用Vue3/Vite版模板创建 npx degit dcloudio/uni-preset-vue#vite my-ruoyi-app cd my-ruoyi-app npm install创建好后项目结构大致如下my-ruoyi-app/ ├── src/ │ ├── pages/ # 页面文件对应RuoYi的views │ │ ├── index/ │ │ │ └── index.vue # 首页 │ │ ├── login/ │ │ │ └── login.vue # 登录页可能简化或作为备用入口 │ │ └── ... # 其他业务页面 │ ├── static/ # 静态资源 │ ├── uni.scss # 全局样式 │ ├── App.vue # 应用配置 │ └── main.js # 入口文件 ├── manifest.json # 应用配置包括各平台设置 ├── pages.json # 页面路由与样式设置 └── ...其他配置文件关键配置1manifest.json中的微信配置在manifest.json的mp-weixin节点下需要配置微信小程序的AppID注意这里是小程序的AppID如果你同时要发布小程序的话。对于纯H5公众号的AppID是在后端配置的。但更重要的是我们需要在h5节点下配置运行的基础路径因为最终H5是部署在服务器子目录或独立域名下的。{ h5: { router: { base: ./ // 如果部署在根目录则为./如果部署在/h5下则为/h5/ }, publicPath: ./, template: index.h5.html, devServer: { port: 8080, proxy: { // 开发环境代理解决跨域 /prod-api: { target: http://localhost:8080, // 你的RuoYi后端地址 changeOrigin: true, secure: false } } } } }关键配置2封装全局请求工具在src下创建utils/request.js封装uni.request统一处理基地址、Token、响应拦截。import { getToken } from /utils/auth; // 假设有从Cookie/LocalStorage读取Token的工具 const service uni.request; const request (options) { // 1. 设置默认基地址根据运行环境切换 if (!options.url.startsWith(http)) { options.url (process.env.NODE_ENV development ? /prod-api : https://api.yourdomain.com) options.url; } // 2. 携带Token const token getToken(); if (token) { options.header { ...options.header, Authorization: Bearer token // 与后端RuoYi的JWT过滤器保持一致 }; } // 3. 发起请求并返回Promise return new Promise((resolve, reject) { service({ ...options, success: (res) { const { data, statusCode } res; if (statusCode 200) { // 这里根据RuoYi后端统一的响应格式处理例如 { code: 200, msg: success, data: {} } if (data.code 200) { resolve(data.data); } else { // 业务错误例如 token过期(401) if (data.code 401) { // 触发重新登录逻辑 uni.navigateTo({ url: /pages/login/login }); } uni.showToast({ title: data.msg || 请求失败, icon: none }); reject(new Error(data.msg)); } } else { reject(new Error(网络请求错误 ${statusCode})); } }, fail: (err) { console.error(请求失败:, err); uni.showToast({ title: 网络连接失败, icon: none }); reject(err); } }); }); }; export default request;4.2 登录页与登录状态管理在/pages/login/login.vue中我们的逻辑变得比传统登录页简单。template view classlogin-container view classlogoRuoYi移动端/view view classtip企业微信授权登录/view !-- 可能保留一个备用入口如手机号验证码登录 -- button classwechat-btn typeprimary taphandleWechatLogin :loadingloading image src/static/wechat-logo.png modewidthFix classbtn-icon/image 微信一键登录 /button /view /template script setup import { ref } from vue; import { onLoad } from dcloudio/uni-app; import request from /utils/request; const loading ref(false); // 页面加载时检查URL中是否有微信回调带来的code如果是静默授权跳转回来的 onLoad((query) { if (query.code query.state) { // 说明是从微信授权回调跳转回来的直接处理登录 handleWechatCallback(query.code, query.state); } else { // 否则检查本地是否已有Token有则直接跳转首页 const token uni.getStorageSync(token); if (token) { uni.switchTab({ url: /pages/index/index }); // 假设首页是tab页 } } }); const handleWechatLogin async () { loading.value true; try { // 1. 先向后端请求微信授权URL const { data: authUrl } await request({ url: /auth/wechat/auth-url, method: GET, params: { frontendRedirect: /pages/index/index // 登录成功后要去的页面 } }); // 2. 跳转到微信授权页面 // 注意在H5环境中直接使用 window.location.href 跳转 // uni-app 的 uni.navigateTo 等API不适用于跨域跳转 window.location.href authUrl; } catch (error) { console.error(获取授权URL失败:, error); loading.value false; } // 注意跳转后当前页面逻辑中断控制权交给微信和我们的回调接口 }; // 这个函数可能不会被调用因为通常回调是由后端处理并重定向。 // 但如果后端采用“前端处理code”的模式则会用到。 const handleWechatCallback async (code, state) { try { const token await request({ url: /auth/wechat/login, method: POST, data: { code, state } }); // 存储Token (示例用Storage实际生产环境建议用更安全的方式或依赖后端Set-Cookie) uni.setStorageSync(token, token); uni.showToast({ title: 登录成功, icon: success }); // 跳转到首页或state中指定的页面 uni.switchTab({ url: /pages/index/index }); } catch (error) { uni.showToast({ title: 登录失败, icon: none }); } }; /script登录状态持久化与校验 Token的存储需要权衡。localStorage易受XSS攻击但方便。HttpOnly Cookie最安全但需要前后端域名一致且注意跨域。在我们的架构中由于后端回调接口可以直接设置HttpOnly Cookie前端可以不必再显式管理Token的存储和携带request拦截器直接从Cookie中读取浏览器会自动携带。但需要确保uni.request在H5环境下开启了withCredentials默认是开启的。4.3 后端用户绑定与JWT签发这是后端最核心的业务逻辑userService.findOrCreateByWechatOpenid。Service public class SysUserServiceImpl implements ISysUserService { Autowired private SysUserMapper userMapper; Override Transactional(rollbackFor Exception.class) public SysUser findOrCreateByWechatOpenid(String openid, String wechatAccessToken) { // 1. 根据openid查询现有用户 SysUser user userMapper.selectUserByWechatOpenid(openid); if (user ! null) { // 用户已存在更新可能的微信信息如access_token user.setWechatAccessToken(wechatAccessToken); user.setUpdateTime(new Date()); userMapper.updateUser(user); return user; } // 2. 用户不存在创建新用户 user new SysUser(); user.setUserName(wx_ openid.substring(0, 8)); // 生成一个默认用户名 user.setNickName(微信用户); // 默认昵称 user.setWechatOpenid(openid); user.setWechatAccessToken(wechatAccessToken); user.setStatus(0); // 正常状态 user.setCreateTime(new Date()); // 3. 可以尝试用access_token获取更多信息snsapi_userinfo时 if (StringUtils.isNotEmpty(wechatAccessToken)) { try { String userInfoUrl String.format( https://api.weixin.qq.com/sns/userinfo?access_token%sopenid%slangzh_CN, wechatAccessToken, openid ); WechatUserInfoResp userInfo restTemplate.getForObject(userInfoUrl, WechatUserInfoResp.class); if (userInfo ! null userInfo.getErrcode() null) { user.setNickName(userInfo.getNickname()); user.setAvatar(userInfo.getHeadimgurl()); // 其他字段如性别、城市等可按需存入扩展字段 } } catch (Exception e) { log.warn(获取微信用户详情失败不影响主流程, e); } } // 4. 插入用户并分配默认角色例如一个“移动端用户”角色 userMapper.insertUser(user); Long userId user.getUserId(); // 分配默认角色ID需要先在角色表中创建好 SysUserRole userRole new SysUserRole(); userRole.setUserId(userId); userRole.setRoleId(3L); // 假设3是“移动端用户”的角色ID userRoleMapper.batchUserRole(new Long[]{userId}, new Long[]{3L}); return user; } }注意事项为新创建的微信用户分配一个恰当的默认角色至关重要。这个角色应该只拥有移动端必要的、最低限度的权限例如只能查看部分数据、提交审批等遵循权限最小化原则。切勿直接赋予管理员权限。5. 常见问题与排查技巧实录在实际开发和上线过程中我遇到了不少问题这里总结几个最有代表性的。5.1 授权回调页面“白屏”或报错“redirect_uri参数错误”这是最高频的问题几乎100%与配置有关。检查点1网页授权域名登录公众号后台确认“网页授权域名”填写正确没有http://没有端口且已经备案。域名必须精确匹配如果你配置的是m.domain.com那么跳转的redirect_uri也必须是https://m.domain.com/xxx不能是www.domain.com或其他子域名。检查点2redirect_uri编码在后台拼接授权URL时redirect_uri参数必须进行URLEncode。且编码后的地址必须完全匹配你配置的域名下的一个合法路径。检查点3回调地址的协议必须使用HTTPS除非是本地开发调试localhost或127.0.0.1。检查点4端口号线上环境不能带端口号默认443。开发环境如果用了非80/443端口需要在内网测试时使用ip地址或配置域名映射并确保公众号测试号配置了带端口的域名测试号支持。排查工具直接在浏览器地址栏输入你拼接的微信授权URL观察微信返回的错误信息。错误信息通常很明确如“redirect_uri域名与后台配置不一致”。5.2 获取到的openid为null或无效原因1code已被使用过。微信的code是一次性的且有效期很短约5分钟。如果同一个code被重复请求兑换access_token第二次就会失败。原因2appid和secret不对。检查后端配置的appid和secret是否与当前公众号一致。特别是secret如果重置过需要更新后端配置。原因3网络超时或微信接口异常。在兑换access_token的HTTP请求处增加重试机制和详细的错误日志。原因4code与appid不匹配。确保前端发起授权请求的appid和后端兑换code的appid是同一个公众号。5.3 跨域与Cookie问题在开发环境前端localhost:8080访问后端localhost:8080或其他端口会遇到跨域。虽然uni-app的devServer.proxy解决了API请求的代理但微信回调是直接跳转到后端接口的这个跳转无法被前端开发服务器代理。解决方案开发阶段让后端接口允许跨域通过CrossOrigin注解或全局配置并且redirect_uri配置为前端开发服务器的地址如http://localhost:8080然后在前端页面如/pages/login/callback.vue的onLoad中接收code和state再由前端调用后端登录接口。这样整个流程都在浏览器端完成避免了回调跨域。但这不是标准的生产模式。生产环境前后端部署在同一个主域名下。例如前端H5在https://m.xxx.com后端API在https://api.xxx.com。通过Nginx配置代理将/api路径转发到后端。这样微信回调地址可以设置为https://m.xxx.com/api/auth/wechat/callback由Nginx转发给后端前后端域名一致Cookie可以顺利设置和携带。同时后端需要配置Cookie的domain为.xxx.com并设置SameSiteNone; Secure如果前后端是严格同级域名。5.4 移动端样式与交互适配RuoYi原后台是为桌面端设计的直接照搬到移动端会惨不忍睹。组件库选择放弃使用Element UI选择专为移动端设计的uni-app UI组件库如uView UI、uni-ui。它们提供了更符合移动端交互习惯的组件如底部导航、下拉刷新、上拉加载、单元格等。布局重构采用Flex布局为主使用rpx单位确保各屏幕适配。将复杂的表格拆分为卡片列表将多步骤表单拆分为分步页面。权限控制适配RuoYi的权限按钮v-hasPermi在移动端可能需要转化为页面级或区块级的显示/隐藏。可以封装一个移动端专用的权限判断指令或方法。API适配部分后台API返回的数据结构可能过于庞大包含大量桌面端才需要的字段。可以考虑为移动端专门开发一套精简的API或者使用GraphQL让前端按需查询。5.5 上线部署与HTTPS微信公众号要求授权页面必须使用HTTPS。这意味着你的前端H5站点和后端接口都必须支持HTTPS。前端部署将uni-app编译出的dist/build/h5目录内容部署到支持HTTPS的Web服务器如Nginx上。后端部署Spring Boot应用使用JDK自带的keytool生成证书或使用Let‘s Encrypt申请免费证书配置在application.yml或通过Nginx反向代理配置HTTPS。Nginx配置示例 (前端)server { listen 443 ssl; server_name m.yourdomain.com; ssl_certificate /path/to/your/fullchain.pem; ssl_certificate_key /path/to/your/privkey.pem; root /path/to/your/h5/dist; index index.html; location / { try_files $uri $uri/ /index.html; # 支持Vue/React等SPA路由 } # 代理后端API请求 location /prod-api/ { proxy_pass https://api.yourdomain.com/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }这样配置后前端访问/prod-api/auth/wechat/...会被代理到后端服务器同时保证了前后端在同域名m.yourdomain.com下解决了Cookie和跨域问题。