news 2026/9/4 8:11:10

JWT登录+Vuex状态管理+验证码解锁:SPA项目认证实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JWT登录+Vuex状态管理+验证码解锁:SPA项目认证实战

做八股文网站这类项目时,总绕不开一个核心模块:登录认证。我在自己折腾“面试刷题站”的过程中,把验证码解锁、JWT登录、Vuex状态管理这一整套流程从零撸了一遍,踩了不少坑,也把底层原理摸透了。这篇就把完整的技术方案、实现细节和那些文档里不会写的经验教训一次性讲清楚,给正在做SPA项目、或者面试前想搞懂认证机制的朋友做个参考。

1. 项目整体设计与思路拆解

1.1 为什么选“验证码+JWT”这套组合

八股文网站这类内容型SPA应用,最核心的用户体系诉求其实很朴素:让用户注册、登录,然后根据登录状态提供差异化功能——比如收藏题目、记录刷题进度、提交答案等。而安全层面的痛点也很明确:登录接口如果裸奔,很容易被脚本暴力撞库,或者被恶意刷验证码接口。

我当时定方案的时候,第一反应是传统的Session-Cookie模式。但仔细一琢磨,这个方案在后端需要维护会话状态,如果将来要把服务拆分成多个实例做负载均衡,还得额外引入共享Session存储(比如Redis),复杂度直接上来。而JWT(JSON Web Token)是自包含的、无状态的,服务端只需验证签名,天然适合SPA项目前后端分离的场景,也方便以后做多端复用。

至于验证码,核心目的不是“防机器人”(真正的强验证请上行为验证码),而是提高暴力破解的成本。配合登录失败次数限制,可以非常有效地挡住大部分无脑扫描和撞库脚本。我用的是图形验证码方案:后端生成图片、把答案存Redis、给前端返回一个UUID作为凭证,前端提交登录时带上这个UUID和用户输入的验证码,后端比对。

这套组合的逻辑很简单:验证码负责“你是不是人”,JWT负责“你是谁、权限是什么”,Vuex负责“前端怎么管理你的登录态”。

1.2 SPA项目里Token安全面临的现实问题

在设计这套方案之前,必须先看清楚SPA项目里Token面临的几个现实问题,不然设计出来的方案就是空中楼阁。

首先是存储问题。JWT拿到手放哪儿?localStorage方便但XSS一打就丢;内存里安全但刷新就没了。其次是过期与续签。JWT是无状态的,一旦签发,服务端在它自然过期前没法主动作废,这就是无状态特性的双刃剑。第三是并发请求时的401处理。如果Token在多个请求同时发出时过期了,前端如果每个请求各自去刷新Token,会产生竞态条件,后端会被刷爆。

这些问题不是理论推演,是我实际开发中真真切切遇到的问题。所以最终的方案不是简单“发个Token就完事”,而是要做一整套配套机制:

  • 前端用Vuex管理Token,同时持久化到localStorage(后面细说为什么不能只存一边)
  • 后端采用双Token机制:AccessToken短时效,RefreshToken长时效,刷新接口负责续期
  • 前端用Axios响应拦截器统一处理401,配合单例刷新机制避免并发竞态

1.3 整体架构分模块拆解

整个项目从模块角度看,大致分了三块:

模块核心职责关键技术点
验证码服务生成、存储、校验Java的BufferedImage绘制图片,Redis存储答案
JWT认证服务签发、校验、刷新jjwt库,HMAC-SHA256签名,双Token机制
前端登录流程调用接口、管理状态、路由控制Vue2 + Vuex + Axios + vue-router

这个划分不是随便分的,每个模块边界清晰,独立演进。比如验证码服务以后想升级成滑块验证码,只需要替换这一个服务,对外接口不变,前端不用动。这就是模块化设计带来的好处。

2. JWT登录机制核心原理拆解

2.1 一段代码看懂JWT的结构

JWT本质上就是一个经过签名处理的JSON字符串,由三部分组成,用点号分隔:

Header.Payload.Signature

第一段Header,声明令牌类型和签名算法:

{ "alg": "HS256", "typ": "JWT" }

第二段Payload,存放实际的数据,比如用户ID、用户名、过期时间。注意这一段是Base64Url编码的,不是加密的,任何人拿到都能直接解码看到内容。所以千万不要把密码等敏感信息塞进去。

{ "sub": "1234567890", "name": "zhangsan", "iat": 1516239022, "exp": 1516242622 }

第三段Signature,是把前两段的编码结果用密钥做HMAC-SHA256签名后得到的。这段存在的意义,就是保证整个Token在传输过程中没有被篡改过。

用代码来演示一下签名生成过程:

// 示意代码,实际开发用jjwt库 String header = Base64UrlEncoder.encode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}"); String payload = Base64UrlEncoder.encode("{\"sub\":\"123\",\"exp\":1516242622}"); String signature = HmacSHA256(header + "." + payload, secretKey); String jwt = header + "." + payload + "." + signature;

写代码的时候不建议自己撸这些底层,用库就行。Java后端我用的是jjwt(io.jsonwebtoken),前端在线调试的时候用jwt.io这个工具就能肉眼验证Token内容。

提示:签名用的密钥一旦泄露,等于所有Token都可以被伪造。这个密钥一定要放到服务端环境变量或配置中心,绝对不要写死在代码里,更不要传到前端仓库。

2.2 签发与校验的完整时序

登录成功的后续流程是这样的:

  1. 前端把用户名、密码、验证码、验证码UUID一起POST到/api/auth/login
  2. 后端先校验验证码(从Redis按UUID取出来比对,比对完立刻删除,防止重放)
  3. 验证码通过后再校验用户名密码(密码要用BCrypt做哈希存储)
  4. 校验通过后,用密钥签发AccessToken(有效期2小时)和RefreshToken(有效期7天)
  5. 把两个Token返回给前端,前端交给Vuex管理并持久化
  6. 后续请求在请求头带上Authorization: Bearer <AccessToken>
  7. 后端写一个过滤器,对所有需要认证的接口校验Token签名、过期时间
  8. 校验通过后把用户信息放入请求上下文,供Controller直接取用

校验的时候,核心逻辑就两句话:签名对不对、过期没有。所以JWT的校验是O(1)的,不需要查数据库,这也是它天生适合高并发场景的原因。

2.3 为什么用JWT而不是Session

这个问题面试被问烂了,但实际做项目的时候才有体感。

Session方案:用户登录后,服务端生成一个SessionId,存一份Session数据在服务端(可能放内存也可能放Redis),把这个SessionId种到Cookie里发给浏览器。后续请求浏览器自动带上Cookie,服务端比对SessionId找到对应的Session数据。

JWT方案:用户登录后,服务端签发一个自包含的Token,客户端存着,后续请求手动放在请求头里。服务端只校验签名和过期时间,完全不关心客户端是谁、Token是发给谁的。

对比一下:

维度SessionJWT
服务端存储需要,占内存/Redis不需要,无状态
水平扩展需要共享存储天然支持
跨域要处理CORS带Cookie请求头携带,无压力
移动端适配一般友好
主动失效方便,直接删Session麻烦,只能等过期
安全风险CSRF(因Cookie自动携带)XSS(因放localStorage)

最直观的体感是部署的时候:Session方案多起一个服务实例,如果不能共享Session,用户的登录态就到处漂移;JWT方案随便起多少实例,只要密钥一致,任何实例都能验证Token。

2.4 Token续签与主动失效的实现方案

纯JWT有个很烦人的点:Token一旦签发,在过期前是没法主动作废的。用户修改密码后,旧的Token依然有效,这显然有安全风险。这就是我引入双Token机制的原因。

AccessToken设置短时效(我设2小时),负责日常请求认证;RefreshToken设置长时效(7天),只用来调刷新接口换新的AccessToken。用户每次操作时,如果AccessToken过期了,前端用RefreshToken去换新的AccessToken,用户无感知。

刷新接口的核心代码逻辑:

@PostMapping("/refresh") public Result refresh(@RequestBody RefreshRequest request) { // 1. 校验RefreshToken是否有效(签名+过期时间) // 2. 拿RefreshToken里的userId,查Redis里存的refreshToken进行比对 // 3. 不一致说明RefreshToken可能被重放,直接拒绝 // 4. 一致则签发新的AccessToken,同时把新的RefreshToken也签发并存储 // 5. 返回两个新Token }

这个方案解决了一个容易被忽略的问题:RefreshToken轮换。每次刷新都换发新的RefreshToken,正常情况下一个RefreshToken只使用一次。如果攻击者盗取了一个RefreshToken,但用户还在正常用,那么用户下一次刷新时就能发现新的RefreshToken被提前使用了,后端可以立刻判定有异常,把这对Token全部作废。

注意:RefreshToken需要服务端存储(我是放Redis的)。这看起来和“无状态”矛盾,但这个“有状态”只发生在刷新接口这个窄入口,而且失效能快速止损,这个复杂度是值得的。

3. 验证码解锁模块设计与实现

3.1 验证码在后端怎么生成和存储

八股文网站的登录页,验证码模块我分了三步实现。

第一步是生成验证码图片。用Java的BufferedImage直接在内存里画,掺入干扰线和噪点。核心代码:

// 生成4位随机数字 String code = String.valueOf((int)((Math.random() * 9 + 1) * 1000)); // 创建画布 BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = image.createGraphics(); // 绘制背景色 g.setColor(Color.WHITE); g.fillRect(0, 0, width, height); // 绘制干扰线(3条) for (int i = 0; i < 3; i++) { g.setColor(new Color(180 + (int)(Math.random() * 70), 180 + (int)(Math.random() * 70), 180)); g.drawLine((int)(Math.random() * width), (int)(Math.random() * height), (int)(Math.random() * width), (int)(Math.random() * height)); } // 绘制字符,每个字符用随机颜色、随机旋转 for (int i = 0; i < code.length(); i++) { g.setColor(new Color(30 + (int)(Math.random() * 100), 30 + (int)(Math.random() * 100), 30 + (int)(Math.random() * 100))); g.setFont(new Font("SansSerif", Font.BOLD, 32)); g.drawString(String.valueOf(code.charAt(i)), i * 20 + 8, 28); }

上面这块代码里,最容易忽略的细节是字符留白。如果字画得太满,或者字间距不均匀,用户肉眼看不清,体验直接崩。我当时调了好几次画布尺寸(最终用了120*40),找到字体大小和间距的平衡点。

第二步是存储答案。生成图片的同时,把验证码答案存到Redis,key用UUID,value是验证码内容,设置5分钟过期。用户登录时必须传两个东西:验证码UUID和用户输入的验证码内容,这样才能对应上。

第三步是返回给前端接口。后端把图片转成Base64字符串,和UUID一起返回给前端:

// 用ImageIO把内存中的图片写为PNG ByteArrayOutputStream baos = new ByteArrayOutputStream(); ImageIO.write(image, "png", baos); String base64Img = Base64.getEncoder().encodeToString(baos.toByteArray()); // 响应体结构 return new Result<>(new CaptchaVO(uuid, "data:image/png;base64," + base64Img));

3.2 前端如何展示和刷新验证码

前端这块很直接,因为验证码就是一张Base64的图片,用Vue的绑定就完事:

<template> <div class="captcha-wrapper"> <img :src="captchaImg" @click="refreshCaptcha" title="点击刷新" alt="验证码" /> </div> </template> <script> export default { data() { return { captchaUuid: '', captchaImg: '' } }, methods: { async refreshCaptcha() { const res = await this.$axios.get('/api/auth/captcha') this.captchaUuid = res.data.uuid this.captchaImg = res.data.img } } } </script>

关于点击刷新这个细节,我想多说一句。很多用户第一次看验证码图片没看清,或者输错了,最自然的操作就是点一下图片刷新。这个交互成本比“找刷新按钮”低得多,一定要做上。

另一个容易漏的细节是:用户输入验证码错误后,前端一定要自动刷新验证码。如果不刷新,用户这次输错了,下次看到的还是同一个验证码,等于把“重试一次”的窗口无限放大,暴力破解的成本就下来了——不对,是上去了。但更重要的问题在后端:验证码校验通过后必须立刻删除,防止同一个验证码反复使用。

3.3 登录接口串起整个链路

登录接口是验证码和JWT交汇的地方。逻辑顺序很关键:先验验证码,再验密码。

@PostMapping("/login") public Result login(@RequestBody LoginRequest request) { // 1. 先校验验证码(不通过直接返回,不查数据库) String savedCode = redisTemplate.opsForValue().get("captcha:" + request.getUuid()); if (savedCode == null || !savedCode.equalsIgnoreCase(request.getCode())) { return Result.error("验证码错误或已过期"); } // 验证码一次性,用完即焚 redisTemplate.delete("captcha:" + request.getUuid()); // 2. 再校验用户名密码 User user = userMapper.selectByUsername(request.getUsername()); if (user == null || !BCrypt.checkpw(request.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } // 3. 签发Token String accessToken = jwtUtil.generateAccessToken(user.getId(), user.getUsername()); String refreshToken = jwtUtil.generateRefreshToken(user.getId()); redisTemplate.opsForValue().set("refresh:" + user.getId(), refreshToken, 7, TimeUnit.DAYS); return Result.success(new TokenVO(accessToken, refreshToken)); }

注意这里有个性能层面的小技巧:验证码校验失败时直接返回,完全不用查数据库,把大部分恶意请求挡在了最外层。如果你把密码校验放在验证码前面,数据库就会被无效请求打穿,这是我在压测时发现的。

4. 前端Vuex登录态管理与实现剖析

4.1 为什么用Vuex而不是直接操作localStorage

这个问题我在设计初期纠结了很久。理论上,登录态管理就是“存Token + 取Token”,localStorage加一个工具函数完全够用。但真做过项目就会发现,登录态一旦复杂起来,localStorage方案会让组件间通信变得非常别扭。

举一个具体场景:用户登录成功后,顶栏要显示用户名,侧边栏要刷新菜单权限,页面路由要动态添加。如果我直接用localStorage存Token,Token是存进去了,但怎么通知所有组件“登录成功了”?你得在登录成功的函数里手动调用各个组件的更新方法,耦合度极高。

Vuex的答案很优雅:登录成功后commit一个mutation,把用户信息放进store;所有组件通过mapGetters或mapState读取用户信息,状态一变,界面自动更新。这就是单向数据流的好处。

另一个容易被忽略的点是:Vuex帮我们把“登录中”这个状态也管理了起来。登录请求发出到响应回来的这段时间,按钮要显示loading、防止重复提交,这个isLoginLoading状态放在全局store里,比放在组件data里靠谱得多。

4.2 Vuex Store结构设计

我最终采用的store结构如下:

// store/index.js import Vue from 'vue' import Vuex from 'vuex' import createPersistedState from 'vuex-persistedstate' import auth from './modules/auth' Vue.use(Vuex) export default new Vuex.Store({ modules: { auth }, plugins: [ createPersistedState({ key: 'exam-app', paths: ['auth.token', 'auth.refreshToken'] }) ] })
// store/modules/auth.js export default { namespaced: true, state: () => ({ token: '', refreshToken: '', userInfo: null, isLoginLoading: false }), getters: { isLoggedIn: state => !!state.token, username: state => state.userInfo ? state.userInfo.username : '' }, mutations: { SET_TOKEN(state, token) { state.token = token }, SET_REFRESH_TOKEN(state, token) { state.refreshToken = token }, SET_USER_INFO(state, userInfo) { state.userInfo = userInfo }, SET_LOGIN_LOADING(state, loading) { state.isLoginLoading = loading }, CLEAR_AUTH(state) { state.token = '' state.refreshToken = '' state.userInfo = null } }, actions: { async login({ commit }, payload) { commit('SET_LOGIN_LOADING', true) try { const res = await axios.post('/api/auth/login', payload) commit('SET_TOKEN', res.data.accessToken) commit('SET_REFRESH_TOKEN', res.data.refreshToken) // 登录成功后立刻拉取用户信息 const userRes = await axios.get('/api/user/info') commit('SET_USER_INFO', userRes.data) return { success: true } } catch (e) { return { success: false, message: e.message } } finally { commit('SET_LOGIN_LOADING', false) } }, async logout({ commit }) { try { await axios.post('/api/auth/logout') } finally { commit('CLEAR_AUTH') } } } }

这个结构有几个值得注意的设计决策。

第一,为什么要持久化到localStorage?Vuex的state是内存态,刷新页面就没了。不持久化的话,用户刷新一下就被踢出登录,体验非常糟糕。vuex-persistedstate插件按配置把指定path同步到localStorage,页面加载时自动恢复。

第二,为什么只持久化token和refreshToken,不持久化userInfo?因为userInfo是“用户资料”,可能被用户修改(比如改头像、改昵称),localStorage里存的可能是旧数据。更好的做法是页面刷新后,用token去调一次/api/user/info拿最新数据。token是凭证,userInfo是可变的业务数据,两者的持久化策略应该不同。

第三,登录时只存token,等Token拿到手后再去请求用户信息。一开始我图省事,把用户名塞在JWT的Payload里,登录后直接解码Payload拿用户名。后来发现用户改昵称后,Token里还是旧昵称,除非重新登录,否则永远显示不对。改成“Token只负责认证,用户信息一律通过接口获取”之后,这个问题就根治了。

4.3 Axios拦截器如何无缝接入Token

光有store还不够,每个请求都要自动带上Token,这是Axios拦截器干的活。我的实现分两层:请求拦截器加Token,响应拦截器处理401。

// utils/request.js import axios from 'axios' import store from '@/store' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 是否正在刷新token的标志 let isRefreshing = false // 刷新token期间,暂存的请求队列 let pendingQueue = [] service.interceptors.request.use(config => { const token = store.getters['auth/token'] // 注意getter写法 if (token) { config.headers['Authorization'] = `Bearer ${token}` } return config }, error => { return Promise.reject(error) }) service.interceptors.response.use( response => { // 后端返回结构:{ code: 200, data: ..., message: '...' } const res = response.data if (res.code !== 200) { return Promise.reject(new Error(res.message)) } return res.data }, async error => { const { response } = error if (response && response.status === 401) { // 尝试用refreshToken换新的accessToken const refreshToken = store.getters['auth/refreshToken'] if (!refreshToken) { // 连refreshToken都没有,直接踢回登录页 store.commit('auth/CLEAR_AUTH') router.push('/login') return Promise.reject(error) } if (!isRefreshing) { isRefreshing = true try { const res = await axios.post('/api/auth/refresh', { refreshToken }) store.commit('auth/SET_TOKEN', res.data.accessToken) store.commit('auth/SET_REFRESH_TOKEN', res.data.refreshToken) // 重放暂存的请求 pendingQueue.forEach(cb => cb(res.data.accessToken)) pendingQueue = [] return service(error.config) // 重试当前请求 } catch (refreshError) { // 刷新失败,回到登录页 store.commit('auth/CLEAR_AUTH') router.push('/login') return Promise.reject(refreshError) } finally { isRefreshing = false } } else { // 正在刷新中,把请求放进队列 return new Promise(resolve => { pendingQueue.push(newToken => { error.config.headers['Authorization'] = `Bearer ${newToken}` resolve(service(error.config)) }) }) } } return Promise.reject(error) } )

上面这段代码里,最核心也最容易被忽略的是请求重放队列。假设用户打开页面后同时发出5个请求,Token恰好过期了,5个请求全部返回401。如果没有队列机制,5个请求各自去调刷新接口,后端收到5个几乎同时的刷新请求,其中4个会失败(RefreshToken已被第一个请求轮换或重放拦截)。有了队列,只有第一个401触发刷新,其他4个排队等新Token到位后重放。

这里还有一个坑:刷新接口本身不能被401拦截器拦截,否则会死循环。我的处理办法是刷新请求直接用裸axios.post,不走service实例。

4.4 路由守卫与登录态联动

最后一步,路由守卫。这一步解决的问题是:用户没登录就访问需要登录的页面,直接踢回登录页。

// router/index.js router.beforeEach(async (to, from, next) => { const isLoggedIn = store.getters['auth/isLoggedIn'] if (to.meta.requiresAuth) { if (!isLoggedIn) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } } else { // 已登录还访问登录页,直接回首页 if (to.path === '/login' && isLoggedIn) { next('/') } else { next() } } })

这里有个细节值得提一下:登录成功后要跳回用户原本想访问的页面。所以登录页在拦截跳转时,要把目标路由放到query里(/login?redirect=/profile),登录成功的action里读一下这个参数再决定跳哪。这个小体验,不做其实不致命,但做了会显得很专业。

5. 常见问题与排查技巧实录

5.1 验证码错一次就失效,用户吐槽体验差

这个问题在我上线初期被反馈得最狠。我原本的设计是“验证码校验失败即删除”,理论上一个验证码只能用一次,安全性拉满,但用户的真实行为是:输错一次,重新输之前先在脑子里回忆一下自己刚才到底看的是什么字,然后换一个验证码——结果他刚刚看清的那个数字代码已经没法用了。

后面我调整了策略:验证码校验失败时不立即删除,而是允许同一个验证码在5分钟有效期内尝试3次,3次都失败才作废。同时每次失败都让前端自动刷新验证码。

这里其实有一个平衡点:允许重试太多次,暴力破解成本就降下去了;允许太少的重试,用户体验又不行。3次是我实测下来比较平衡的数字。前端配合自动刷新,即使用户没看清,点了刷新也就一秒钟的事。

5.2 部署到服务器后,验证码图片不显示

这个坑我印象太深刻了,本地开发一切正常,部署到服务器后验证码图片就加载不出来。排查了半天,最后发现问题出在Java的字体库上。

服务器是精简版Linux镜像,没装中文字体。new Font("SansSerif", Font.BOLD, 32)在开发机上有字体可渲染,到了服务器上直接用了缺省字体,甚至可能导致图形渲染异常。

解决办法是在服务器上安装字体包:

# Ubuntu/Debian apt-get install -y fonts-dejavu-core

然后代码里指定具体字体名:

g.setFont(new Font("DejaVu Sans", Font.BOLD, 30));

提示:如果你部署到Docker容器里,一定要在Dockerfile里加字体安装步骤。这种问题和业务代码无关,纯粹是环境差异,属于那种“线上事故级”的隐藏坑,排查起来非常花时间。

5.3 Vuex刷新后登录态丢失

这个是Vuex新手必踩的坑。原因我在4.2里已经说了:Vuex的state只存在内存里。解决方式是引入vuex-persistedstate做持久化。

但持久化之后又有一个新坑:用户手动改localStorage里的token,或者在浏览器控制台执行store.commit('auth/SET_TOKEN', 'xxxx'),前端就认为自己是登录状态了。这种“伪登录”其实是伪造了前端状态,但后端校验不通过,请求还是会401。

所以不要把前端的“isLoggedIn”当作安全边界,它只是改善体验的。真正的安全校验永远在后端。前端顶多做到:访问需要登录的接口时,如果401了,不管前端状态是什么,一律清空并跳回登录页。

5.4 多标签页同时操作,Token刷新互相踢下线

这个问题的场景是:用户在两个标签页打开同一个系统,两边同时操作,A页面的Token过期去刷新了RefreshToken,B页面的RefreshToken还是旧的,刷新时后端比对Redis发现不一致,判定为重放,把两个标签页都踢下线了。

这个场景处理方式比较粗暴但也实用:如果两个标签页同时操作同一个账号,后刷新的会把先刷新的踢下线,这个行为是可以接受的。现实使用中,一个用户在两三个标签页同时高频操作的概率不高,真遇到了,重新登录一遍的成本也不高。

如果确实要做,更优雅的方案是让同一个浏览器下的多个标签页共享同一个Storage事件监听:localStorage变化时同步刷新页面里的Token。但这个方案的复杂度会上升不少,而我评估这个场景的收益不值得,就放弃了。这里面的取舍逻辑是:不要为了极端场景把主流程搞复杂

5.5 JWT伪造与密钥泄露的防御

前面提到了,JWT的Signature本来就是为了防伪造。但有一个真实存在的攻击面是:后端代码仓库泄露了密钥,或者密钥写死在发给前端的静态资源里。

我在项目里做了这几件事:

  • 密钥放在环境变量里,每个环境(dev/test/prod)各一份
  • 定期轮换密钥,轮换时Redis里存一个旧的密钥列表,让旧的Token在轮换过渡期还能通过校验
  • 不把任何密钥相关的东西写进前端代码

另外还有一个容易忽视的点:JWT的Payload是Base64编码的,不是加密的。虽然它不会把用户密码带进去,但如果你在Payload里放了手机号、邮箱这些用户个人信息,别人只要拿到Token就能解码看到。所以我在Payload里只放userId和username,其他敏感信息一律不放。

6. 这套方案的后续扩展方向

做完整个登录认证流程后,我明显感觉对SPA项目的理解上了一个台阶。后面如果继续迭代,有几个方向是我想深入的:

一是集成OAuth2.0,支持第三方登录(Github、微信扫码),核心是理解Authorization Code模式的回调流程,以及怎么把第三方用户的身份映射到自己系统的用户体系上。

二是做更丰富的验证码方案,接入滑块验证码或者点选验证码。行为式验证码的防机器能力比图形验证码强很多,主要是通过收集用户的鼠标轨迹、停留时长等行为特征来判断是人还是机器。

三是把权限模型做厚。现在只是“登录/未登录”二态,后面可以引入RBAC(基于角色的权限控制),在JWT里加入角色信息,前端通过Vuex管理权限路由,动态生成菜单和路由表。

我在实际开发中最大的体会是:登录认证这套东西,看文档觉得不难,真正动手做才会发现难点全在边界情况和异常路径上——并发刷新、重放攻击、多标签页、XSS防护、过期时间设计、刷新轮换策略,每一处都是坑。把这些坑都趟平一遍,你对“前端状态管理”和“后端无状态认证”这两件事的理解,比看十篇面试八股文都有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 21:13:41

国产AI芯片制程落后为何能效反超?技术路径与选型指南

最近在关注AI芯片领域的朋友&#xff0c;可能都注意到了这样一个现象&#xff1a;一些国产AI芯片&#xff0c;即便在半导体制造工艺&#xff08;制程&#xff09;上相比国际巨头如英伟达的旗舰产品&#xff08;例如B300&#xff09;落后一到两代&#xff0c;但在某些关键性能指…

作者头像 李华
网站建设 2026/9/4 8:12:10

STM32出租车计价器毕设实战:从硬件选型到软件架构全解析

简介&#xff1a;本资源是一套完整的基于STM32单片机的出租车计价器毕业设计工程资料&#xff0c;面向电子信息、自动化及嵌入式方向的本科生与初学者&#xff0c;解决课程设计、毕设开发中软硬件协同实现计费逻辑、传感器信号采集、LED/LCD显示及实时时钟管理等典型问题。压缩…

作者头像 李华
网站建设 2026/9/4 9:11:47

2026年7月银川市新房价格深度分析报告

一、报告背景与数据说明本报告基于2026年7月银川市新房实际成交案例&#xff0c;结合市场公开数据&#xff0c;对银川市新房价格走势、区域分化、产品结构及未来趋势进行深度分析。报告数据来源包括银川市住房和城乡建设局网签备案数据、主要房企成交台账及第三方机构监测样本&…

作者头像 李华
网站建设 2026/9/4 13:01:50

国产SiC二极管替换硅基快恢复/肖特基二极管的工程实践指南

这类国产碳化硅&#xff08;SiC&#xff09;二极管&#xff0c;特别是贴片封装、650V/4A这个规格的&#xff0c;最值得关注的点不是“国产”这个标签&#xff0c;而是它到底能不能在实际电路里&#xff0c;稳定、可靠地替代你原来用的快恢复二极管&#xff08;FRD&#xff09;或…

作者头像 李华
网站建设 2026/9/4 8:55:10

基于SSM+微信小程序的实验室管理系统:从技术选型到实战部署

简介&#xff1a;这是一套面向计算机专业本科生的高分毕业设计级实验室管理系统&#xff0c;融合Java后端&#xff08;SSM框架&#xff09;、MySQL数据库与微信小程序前端&#xff0c;解决高校实验室预约、设备管理、实验数据归档与权限协同等实际管理痛点&#xff0c;亦适用于…

作者头像 李华
网站建设 2026/9/4 12:55:39

URF-R330开发包DLL报错排查与身份证阅读器二次开发部署指南

简介&#xff1a;URF-R330开发包面向需基于明华URF-R330远距离无线通信模块进行产品开发的嵌入式与物联网工程师&#xff0c;整合硬件接口说明、通信协议文档、API函数参考、VC6与C#双语言示例、DEMO程序及调试指南&#xff0c;可帮助读者快速掌握UART/SPI/I2C接口集成、MODBUS…

作者头像 李华