☰
前后端分离登录实战:SpringBoot+Redis+VueX 从假登录到上线
2026/10/4 2:41:44 网站建设 项目流程

很多朋友做前后端分离项目时,登录功能都是“假登录”——前端写死一个 token,后端用过滤器拦一下就算完事。等到要接真实数据库、做会话共享、存用户状态的时候,才发现里面全是坑。这期咱们就老老实实把登录功能从“玩具”升级成“能上线”的版本:后端接 MySQL 做账号校验,用 Redis 存会话,前端用 VueX 管理登录态,顺便把 axios 拦截器、路由守卫这些配套逻辑一并理顺。适合正在做毕设、或者在公司维护老 Vue2 项目的同学参考。

1. 整体思路:登录功能为什么非要拉上 Redis 和 VueX

先别急着写代码,想清楚“登录”这件事到底在解决什么问题。用户输入账号密码,后端校验通过后,怎么让系统记住“这个人已经登录了”?传统单体项目用 Session 就行,但前后端分离架构下,前端部署在一台服务器,后端在另一台,Session 默认存在内存里,跨域请求带不上 Cookie 不说,后端一重启用户就得重新登录。所以业界通用做法是:后端生成一个凭证(token),前端拿着这个凭证访问受保护的接口,后端校验通过就放行。

那 Redis 在这里扮演什么角色?一句话:Session 的“外置仓库”。把登录状态从后端内存里挪到 Redis 中,多台后端实例都能共享同一份会话数据,重启也不会丢。你可能会问:为什么不用 JWT?JWT 确实能无状态化,但有个硬伤:无法服务端主动失效。用户修改密码、管理员封禁账号、检测到异地登录强制下线,JWT 在过期前都拦不住。用 Redis 存 token 就能随时删掉,这也是很多内部管理系统选择 Redis 方案的原因。

VueX 的作用则是解决“前端怎么知道用户是谁”。登录成功后,后端返回用户信息,总不能让每个页面都重新请求一遍吧?存到 VueX 里,所有组件都能直接读取。但 VueX 有个特点:刷新页面状态就没了,所以还得配合 localStorage 持久化,刷新后重新把状态塞回 VueX。这套流程跑通后,整个登录闭环才算是完整的。

2. 技术选型与准备工作

想跑通这套流程,先把环境搭好。我这里用的是最稳妥的“老项目全家桶”组合,版本搭配经实测不打架。

2.1 后端环境清单

JDK 1.8、Maven 3.6+、SpringBoot 2.7(别用 3.x,javax 命名空间和 springfox 兼容性会折腾到你怀疑人生)、MySQL 5.7+(8.0 也 OK,注意驱动依赖)、Redis 任意稳定版(5.0 以上就行)。

pom.xml 里核心依赖加这几个:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

Redis 别用 lettuce 客户端,坑多,换成 jedis 更顺手。排除 lettuce、引入 jedis:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>

2.2 前端环境清单

Node 14 或 16(更高版本跑老 Vue2 项目的 node-sass 会崩)、Vue 2.6、Vue Router 3.x、VueX 3.x、axios 0.27(最新版在部分老项目里有兼容警告,不过不影响使用)。HBuilderX 跑 Vue2 项目我个人试过,启动速度还行,但依赖安装和调试还是命令行更顺手,看个人习惯。

2.3 数据库准备

建个 user 表,字段设计直接给你参考:

CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `status` tinyint(1) DEFAULT '1' COMMENT '1启用 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

密码字段必须用 BCrypt 加密存储,千万别用 MD5。MD5 撞库太容易了,这个后面展开讲。

3. 后端实现:从数据库校验到 Redis 会话

3.1 登录接口逻辑拆解

登录接口的核心逻辑分四步:接收参数、校验账号密码、生成 token 写入 Redis、返回结果。先写个统一的返回体 Result,让前端好统一处理:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }

Service 层别把业务写在 Controller 里,不利于复用和维护。登录核心逻辑参考:

@Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private StringRedisTemplate stringRedisTemplate; public Result<LoginVO> login(LoginDTO dto) { // 1. 查数据库 User user = userMapper.selectByUsername(dto.getUsername()); // 2. 判断用户是否存在 if (user == null) { return Result.error("用户名不存在"); } if (user.getStatus() != 1) { return Result.error("账号已被禁用"); } // 3. 校验密码 if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error("密码错误"); } // 4. 生成token,存入Redis String token = UUID.randomUUID().toString().replace("-", ""); // key设计:login:token:{uuid} stringRedisTemplate.opsForValue().set( "login:token:" + token, String.valueOf(user.getId()), 60 * 30, TimeUnit.SECONDS ); // 5. 返回前端需要的信息 LoginVO vo = new LoginVO(); vo.setToken(token); vo.setNickname(user.getNickname()); vo.setAvatar(user.getAvatar()); return Result.success(vo); } }

这里有几个细节值得说说。

用户名不存在和密码错误为什么要分开提示?从安全角度说,分开提示方便攻击者探测“哪些用户名是注册过的”,统一提示会更安全。但作为内部系统或练手项目,分开提示在用户体验上更友好,怎么取舍看你场景。

Redis key 设计我用了login:token:{uuid}这种带前缀的格式,好处有两个:一是避免和业务缓存混在一起,排查问题时用keys login:token:*一下就能筛出来;二是加过期时间后 Redis 自动清理,不需要写定时任务。

3.2 BCrypt:为什么非它不可

很多人图省事,密码存成 MD5,觉得反正内部系统无所谓。但 MD5 有个特性:同样的明文,永远产生同样的密文。这意味着两个用户密码相同,数据库中密文也相同,再加上彩虹表的存在,弱密码几乎等于裸奔。BCrypt 每次加密都会混入随机盐,同样的明文每次密文都不同,而且内部有轮次计算,暴力破解成本高得多。Spring Security 提供了现成的 BCrypt 实现,单独引个spring-security-crypto依赖就能用,不用把整个安全框架引进来。

<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency>

生成加密密码就一行:

String encoded = BCrypt.hashpw("123456", BCrypt.gensalt()); System.out.println(encoded);

校验时用刚才代码中的BCrypt.checkpw(明文, 密文),返回 true 就通过了。注册功能里新用户创建密码时同样用BCrypt.hashpw加密后落库。

注意:不要把 gensalt 每次手动传入同样的盐,BCrypt.gensalt()默认带随机盐,直接调用就好。

3.3 Redis 序列化策略:怎么存、存什么

实际开发中最容易出问题的就是 Redis key 和 value 乱码。我见过不少同事用 RedisTemplate 默认配置,存进去的 key 变成\xac\xed\x00\x05t\x00...,看着像乱码,其实是 JDK 序列化器搞的鬼。JDK 序列化对象会带一堆类型信息,存字符串时白白占用存储空间,还会让可视化工具看起来非常难受。

解决办法:用 StringRedisTemplate 操作所有缓存数据,key 和 value 都当字符串处理。如果非要存对象,就手动把对象转成 JSON 字符串再存。上面代码里写法就是典型的字符串方式,token 作为 key,用户 ID 作为 value,不涉及序列化器问题。取的时候:

String userIdStr = stringRedisTemplate.opsForValue().get("login:token:" + token); if (userIdStr == null) { // token不存在或已过期 }

关于 token 存什么,有人存用户 ID,有人存用户对象 JSON,还有人只存个"1"表示有效。我建议存用户 ID 字符串。因为后续任何接口需要知道“当前登录用户是谁”,用 userId 查一次数据库就完事了。如果存对象 JSON,一旦用户昵称、头像变了,Redis 里的旧数据还留着旧信息,反而产生脏数据。

3.4 注册功能顺手搞定

没有注册功能的登录是没法闭环的。趁热打铁,把注册接口也加上。逻辑不复杂,但要注意:先查用户名是否存在,再加密密码,最后插入。别让 Service 吞掉重复键异常,得主动查询判断再插入,给用户一个明确的“用户名已被占用”提示。

public Result<String> register(RegisterDTO dto) { User exist = userMapper.selectByUsername(dto.getUsername()); if (exist != null) { return Result.error("用户名已被注册"); } User user = new User(); user.setUsername(dto.getUsername()); user.setPassword(BCrypt.hashpw(dto.getPassword(), BCrypt.gensalt())); user.setNickname(dto.getNickname() == null ? dto.getUsername() : dto.getNickname()); userMapper.insert(user); return Result.success("注册成功"); }

电商系统一般还有手机号验证码注册、扫码登录这些玩法,今天先把基础账号密码版搞定,后续想加手机号登录,核心想法不变:验证码校验通过后,同样方式生成 token 进 Redis。

4. 前端实现:VueX 管理登录态 + 封装请求拦截

后端接口有了,前端怎么把登录态串起来?核心思路是:登录接口拿 token,token 交给 VueX,VueX 配上 localStorage,再由 axios 拦截器统一把 token 拼到请求头。这样任何请求都自动携带凭证,路由守卫负责检查“没登录别进来”。

4.1 VueX store 模块设计

VueX 按模块管理,逻辑更清晰。store 根目录下建 modules/user.js:

const state = { token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}') } const mutations = { SET_TOKEN(state, token) { state.token = token localStorage.setItem('token', token) }, SET_USER_INFO(state, userInfo) { state.userInfo = userInfo localStorage.setItem('userInfo', JSON.stringify(userInfo)) }, CLEAR_STATE(state) { state.token = '' state.userInfo = {} localStorage.removeItem('token') localStorage.removeItem('userInfo') } } const actions = { async login({ commit }, loginForm) { const { data } = await loginApi(loginForm) if (data.code === 200) { commit('SET_TOKEN', data.data.token) commit('SET_USER_INFO', { nickname: data.data.nickname, avatar: data.data.avatar }) } return data }, logout({ commit }) { // 退出要调后端接口让token失效 return logoutApi().finally(() => { commit('CLEAR_STATE') }) } } export default { namespaced: true, state, mutations, actions }

为什么要模块化?因为商城项目后面还要维护购物车状态、订单状态、商品浏览记录,全堆在根 store 里,几百行代码看得头大。拆成模块后,一个 store 文件只维护一份职责,配合 namespaced: true,组件里调用this.$store.dispatch('user/login'),语义清晰。

刷新页面时,VueX 数据没了,但 localStorage 还在。模块初始化 state 时直接从 localStorage 取,这就是“刷新不掉登录态”的核心:前端拿 localStorage 的 token 继续访问后端,后端从 Redis 校验 token 仍有效,整个会话就衔接上了。

4.2 axios 拦截器与 token 传递

请求封装的核心代码:

// utils/request.js import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 请求拦截器:自动带上token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = 'Bearer ' + token } return config }, error => { return Promise.reject(error) }) // 响应拦截器:统一处理错误状态 service.interceptors.response.use(response => { const res = response.data // 业务错误 if (res.code !== 200) { Message.error(res.message || '请求失败') if (res.code === 401) { // token过期 localStorage.clear() router.push('/login') } return Promise.reject(new Error(res.message)) } return res }, error => { if (error.response && error.response.status === 401) { Message.error('登录状态已过期,请重新登录') localStorage.clear() router.push('/login') } else { Message.error('网络异常,请稍后重试') } return Promise.reject(error) }) export default service

请求拦截器解决“token 怎么带”的问题,响应拦截器解决“token 失效怎么办”的问题。后端如果发现 token 无效,返回 401 状态码,前端拦截到后清空本地信息跳转登录页,这个 UX 顺滑度很关键,否则用户会看到一个红条报错,而不知道要重新登录。

4.3 登录页逻辑

登录页模板就不赘述了,核心是提交逻辑:

async handleLogin() { this.$refs.loginForm.validate(async valid => { if (!valid) return this.loading = true try { const res = await this.$store.dispatch('user/login', this.loginForm) if (res.code === 200) { this.$message.success('登录成功') // 跳转到重定向的页面,没有则回首页 const redirect = this.$route.query.redirect || '/' this.$router.push(redirect) } } finally { this.loading = false } }) }

登录成功后跳哪?很多新手直接写router.push('/')固定跳首页,但用户是从某个页面被拦到登录页的,登录完应该回到他原本想去的地方。Vue Router 提供了路由守卫里的 redirect 参数,登录成功判断一下,体验高下立判。

无验证码的登录页在商城项目里其实不太安全。加验证码的思路:后端生成图片 base64 + 一个 uuid 标识,Redis 存验证码值,前端提交时带上 uuid 和填写的验证码。这期先不铺开,后面单独写一期“验证码接入 Redis 的正确姿势”。

4.4 路由守卫:别让未登录用户乱闯

路由守卫是登录态的最后一道防线,写在 router/index.js:

const whiteList = ['/login', '/register', '/home', '/goods'] router.beforeEach((to, from, next) => { // 已登录状态 if (localStorage.getItem('token')) { if (to.path === '/login') { next({ path: from.path || '/' }) } else { next() } } else { // 白名单直接放行 if (whiteList.indexOf(to.path) !== -1) { next() } else { next(`/login?redirect=${to.fullPath}`) } } })

注意守卫里判断是否登录,依据的是 localStorage 还是 VueX?都能用,但推荐用 localStorage 做判断。原因:刷新后 VueX 初始数据本来就是从 localStorage 读的,直接读 localStorage 更保险,还避免了一个潜在时序问题——VueX 还没初始化完毕就去读,读到空值误判。

whiteList 很值得一说。商城首页、商品列表这些 B 端页面不需要登录也能看,但购物车、结算、个人中心必须登录。哪些接口要登录?不只是路由层面拦截,后端也要做鉴权。有个常见的“前端防了后端没防”的情况——用户直接用 Postman 调接口,后端没校验,直接返回数据。所以后端必须每条接口做 token 校验,前端路由守卫只是用户体验层面的优化,真正的安全边界在后端。

5. 后端登录拦截器:让受保护接口强制校验 token

大家常犯的一个错误是只在 Controller 里手动判断“头部有没有 token”,这又陷到“每条接口写一遍”的重复劳动里。正确姿势是做一个拦截器(Interceptor)统一处理。SpringBoot 里注册一个 HandlerInterceptor,配置到 WebMvcConfigurer 的拦截器注册表里。

5.1 登录拦截器代码设计

@Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } String authHeader = request.getHeader("Authorization"); if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); String userId = stringRedisTemplate.opsForValue().get("login:token:" + token); if (userId != null) { // 用户ID存进 request 上下文,后续接口直接用 request.setAttribute("userId", userId); return true; } } // 校验失败,返回401 response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"未登录或登录已过期\"}"); return false; } }

关键点:

  • OPTIONS 预检请求必须放行。前后端分离项目,跨域请求会先发一个 OPTIONS 请求,浏览器预检服务器允不允许跨域。如果不放行,前端所有非简单请求都会被 CORS 拦死,表现就是“明明后端在跑,前端却一直报跨域错误”。
  • 校验通过后把 userId 塞到 request attribute,后续 Controller 直接取:
@RestController @RequestMapping("/user") public class UserController { @GetMapping("/info") public Result<UserInfoVO> info(HttpServletRequest request) { String userId = (String) request.getAttribute("userId"); User user = userService.getById(userId); // ... return Result.success(vo); } }

不用每次都在接口方法里写“获取 token 再解析”,拦截器把脏活干完了,业务代码只关注“当前是谁”。

5.2 注册拦截器与放行白名单

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Resource private LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**") .excludePathPatterns( "/user/login", "/user/register", "/user/captcha", "/goods/**", "/home/**" ); } }

.excludePathPatterns的作用和前端 whiteList 异曲同工:登录注册这些端点不需要 token 就能访问;商品查询、首页聚合数据这种展示型接口,不登录也放行。如果你做的是纯后台管理系统,所有接口都要求登录,那就把.addPathPatterns("/**")留着不放行任何必登录白名单外的东西。

注意:excludePathPatterns 里写的是路径规则,支持通配符/goods/**,别配成/goods/*,前者只匹配一层,后者匹配多层。这两个写错的人不在少数。

5.3 全局 CORS 解决跨域

配置拦截器好办,跨域配置也顺手做了。写一个 CorsFilter Bean 或实现 WebMvcConfigurer 的 addCorsMappings。我用的是 WebMvcConfigurer 实现类里加这段:

@Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 前端地址 .allowedOrigins("http://localhost:8080") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); }

allowedOrigins 要注意端口必须写对。写http://localhost:8080,但前端跑在http://localhost:8081就跨域失败。生产环境建议用配置项读取,这里不写死。

6. 前后端联调:登个录要跨几道坎

代码全写完了,前后端联调才是真正考验耐心的时候。把最常见的几个问题列个“排雷手册”,对照排查能省一大半时间。

6.1 VueX 数据持久化遗漏问题

现象:登录成功后刷新页面,用户信息全没了,路由守卫还把人踢回登录页。

排查思路:先看 localStorage 里到底有没有 token。有 token 但 VueX 状态是空的,多半是 state 初始化没从 localStorage 读;连 token 都没了,说明登录成功后没写 localStorage。我见过有人把 token 存 VueX 就完事了,忘了 localStorage 同步,刷新必掉线。老话重提:localStorage是 VueX 的持久化层,两者必须同步。

6.2 Redis key 乱码与过期时间误区

现象:Redis Desktop Manager 里看到\xac\xed\x00开头的 key,数据也读不出来。

排查思路:检查代码里是用了 RedisTemplate 的默认序列化器还是 StringRedisTemplate。默认的 JdkSerializationRedisSerializer 就会这样,换成 StringRedisTemplate 即可。另一个高频误区:Redis 过期时间单位是秒,很多人写成60*30忘了单位导致 30 毫秒过期,用户刚登录完,第二次请求就被弹出去。也别用set(key, value)不带过期时间,那 token 永远不过期,清不掉的“永久登录”隐患很大。

6.3 401 状态码误判与路由死循环

现象:登录页登录成功,跳转首页,路由守卫又给踢回登录页,死循环。

排查思路:路由守卫里写“已登录用户访问 login 就跳首页”,但判断逻辑有漏洞——比如 token 有值但过期了,用户又手动访问 /login,守卫看到 token 存在就跳首页,首页请求接口返回 401,响应拦截器调用 router.push('/login'),又触发守卫死循环。根治办法:登录成功后清理旧路由记录,响应拦截器做 401 处理时,判断当前路由不在 /login 再跳转。细节代码参考:

if (error.response.status === 401 && router.currentRoute.path !== '/login') { localStorage.clear() router.push('/login') }

6.4 接口提示“未登录”但前端明明带了 token

排查顺序:前端浏览器 Network 面板看请求头 Authorization 是否带上 → 后端日志看拦截器有没有进去 → Redis 里手动get login:token:xxx看 key 是否存在。这三个点逐个排查,一般都能定位。最常见原因是登录接口被路由守卫拦截了,压根没送出去;其次就是 CORS 预检请求没放行,非预检测试接口都跨域失败。

7. 会话过期、登出与“记住我”差异化设计

登录不只有“进”,还得有“出”和“过期处理”。这块做不好,用户会骂产品“登录状态总掉”“退出登录怎么没反应”。

7.1 登出功能双端清理

退出登录不能只清前端 localStorage。如果 token 还在 Redis 里存活,任何人拿到这个 token 还能继续调用接口,账号处于“假退出”状态。登出接口逻辑:

@PostMapping("/logout") public Result<String> logout(HttpServletRequest request) { String authHeader = request.getHeader("Authorization"); if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); // 删除Redis中的会话 stringRedisTemplate.delete("login:token:" + token); } return Result.success("退出成功"); }

前端登出先调接口再清本地状态:

async logout() { try { await logoutApi() } finally { this.$store.commit('user/CLEAR_STATE') this.$router.push('/login') } }

顺序上,即使后端 delete 失败,前端也要清掉本地 token,保证用户侧状态一致性。

7.2 记住我功能的实现技巧

登录页勾选“记住我”,本质是调整 token 有效期。记住我时 7 天过期,不记时 30 分钟过期。后端登录接口接收一个 rememberMe 字段:

long expire = dto.getRememberMe() != null && dto.getRememberMe() ? 60 * 60 * 24 * 7 : 60 * 30;

存 Redis 时用变量 expire 替代固定的 1800 秒即可。注意:过期时间太长确实方便,但安全风险也高。如果这是电商后台,涉及资金操作,建议默认短过期、敏感操作二次验证。

7.3 登录状态过期前端怎么优雅处理

token 过期后,用户点某个按钮,接口返回 401,响应拦截器直接弹“登录过期”踢到登录页,这种体验其实挺生硬的。优雅一点的方案:拦截器里遇到 401,弹一个确认框“登录已过期,是否重新登录?”,点击确认再跳登录页。移动端可以弹 toast + 跳转。另外,页面停着不动、token 在后台悄悄过期,用户回来发现明明停在自己看过的页面上,一点击却被弹走,那体验约等于“白干了”。局部校验、静默刷新这类功能属于进阶话题,跟网关、权限模型放以后单独聊。

8. 最终代码结构与后续扩展思路

完整跑一遍后,项目的目录结构大概是这个形态,给你对照检查别漏文件:

后端: ├── config │ ├── WebMvcConfig.java │ └── RedisConfig.java ├── interceptor │ └── LoginInterceptor.java ├── controller │ ├── UserController.java │ └── GoodsController.java ├── service │ └── UserService.java ├── mapper │ ├── UserMapper.java │ └── UserMapper.xml └── entity / dto / vo ├── User.java ├── LoginDTO.java ├── RegisterDTO.java └── LoginVO.java 前端: ├── src │ ├── api │ │ ├── login.js │ │ └── user.js │ ├── store │ │ └── modules │ │ └── user.js │ ├── router │ │ └── index.js │ ├── utils │ │ └── request.js │ └── views │ ├── login │ │ └── index.vue │ └── profile │ └── index.vue

写代码容易,写好边界很难。很多人做到“登录功能能跑通”就转去做购物车、商品列表了,但登录是后续所有“需要知道当前用户”功能的地基——购物车归属哪个用户、订单用谁的地址、优惠券谁领的、操作日志记录谁做的。这期把数据库、Redis、VueX、拦截器、路由守卫一条链路串下来的方案,正好支撑这些后续功能无缝复用。想快速验收成果,建议从“注册新用户 → 登录 → 刷新页面 → 停 30 分钟 → 再访问受保护接口被踢出 → 重新登录”这条完整链路测一遍,把所有断点记下来挨个排查。

最后再留一个小彩蛋:现在登录只用账号密码,商城项目真正上线时大概率要接第三方登录。你可以在现有框架上扩展一个 OAuth 登录入口,用户在第三方回调后,用同样的逻辑生成 token 写入 Redis,前端走同一套 VueX + 路由守卫,业务代码零改动。这就是把登录逻辑收敛得干净、统一的好处。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询