前后端分离架构下的Token身份验证实现:原理与实战

一、整体流程图

简述流程如下:

  1. 用户登录 → 后端验证用户名密码;
  2. 验证通过 → 生成Token返回前端;
  3. 前端存储Token → 后续请求携带Token;
  4. 后端通过拦截器/过滤器校验Token。

二、后端实现(以 Spring Boot 为例)

1. 依赖引入(如 jjwt)

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

2. 生成Token工具类

package com.admin.utils;
import io.jsonwebtoken.*;
import java.util.Date;

public class JwtUtils {
    private static final String SECRET_KEY = "mySecretKey123456";
    private static final long EXPIRATION = 1000 * 60 * 60 * 24; // 1天

    public static String generateToken(Long userId) {
        return Jwts.builder()
                .setSubject(userId.toString()) // 把用户ID放在sub字段里
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public static Long getUserIdFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }
}

3.将用户id存储

将用户id和用户类型存储在SecurityContextHolder.getContext()中,方便后续获取。(这里我是根本我自己项目的需求存储的,因为未设置单独的角色表,所以把用户类型也存储了,方便后续业务的处理)

package com.admin.filter;

import com.admin.businessuser.constant.Type;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements Serializable {
    private Long userId;
    private Type type; // MEMBER / MERCHANT / ADMINISTRATOR
}
package com.admin.filter;

import com.admin.businessuser.domain.UserDO;
import com.admin.businessuser.mapper.UserMapper;
import com.admin.utils.JwtUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.Collections;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final UserMapper userMapper;

    public JwtAuthenticationFilter(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {

        String token = request.getHeader("Authorization");

        if (token != null && !token.isEmpty()) {
            try {
                Long userId = JwtUtils.getUserIdFromToken(token);

                //  查数据库获取用户信息(你可以用 UserService 或 Mapper)
                UserDO user = userMapper.selectById(userId); // 你自己项目里的 user 表

                if (user != null) {
                    LoginUser loginUser = new LoginUser(userId, user.getType());

                    UsernamePasswordAuthenticationToken authentication =
                            new UsernamePasswordAuthenticationToken(
                                    loginUser,
                                    null,
                                    Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
                            );

                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }

            } catch (Exception e) {
                // token 无效,继续走过滤链或返回401
            }
        }

        filterChain.doFilter(request, response);
    }

}
/**
后续获取代码:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser= (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUserId();
/

4. 登录业务逻辑

   @Override
    public ResultVO<Map<String, Object>> login(UserLoginDTO userLoginDTO) {

        String username = userLoginDTO.getUsername();
        String password = userLoginDTO.getPassword();
        LambdaQueryWrapper<UserDO> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(UserDO::getUsername, username)
                .eq(UserDO::getPassword, SecureUtil.md5(password));
        // 查询用户
        UserDO user = userMapper.selectOne(wrapper);
        if (user == null) {
            return ResultVO.fail("用户名或密码错误");
        }

        if (Type.MEMBER.equals(user.getType())) {
            return ResultVO.fail("非管理员及商家账号无法登录");
        }

        // 生成 JWT Token
        String token = JwtUtils.generateToken(user.getId());

        // 构建响应数据(不直接返回完整 UserDO)
        Map<String, Object> data = new HashMap<>();
        data.put("token", token);
        Map<String, Object> userMap = new HashMap<>();
        userMap.put("id", user.getId());
        userMap.put("username", user.getUsername());
        userMap.put("password", user.getPassword());
        userMap.put("nickname", user.getNickname());
        userMap.put("sex", user.getSex());
        userMap.put("telephone", user.getTelephone());
        userMap.put("avatar", user.getAvatar());
        userMap.put("type", user.getType());

        data.put("user", userMap);

        return ResultVO.success(data);
    }
@PostMapping("/login")
    ResultVO<Map<String, Object>> login(@RequestBody UserLoginDTO userLoginDTO) {
        return userService.login(userLoginDTO);
    }

三、前端实现(以 Vue + Axios 为例)

1.前端登录后,通过将用户信息放在store中,使用pinia进行管理。

登录接口联调,登入成功获取token和用户信息:

const onSubmit = async ({
  validateResult,
  firstError,
}: {
  validateResult: boolean | undefined;
  firstError: string | undefined;
}) => {
  if (validateResult !== true) {
    MessagePlugin.warning(firstError || "未知错误");
    return;
  }

  try {
    const res = await login(formData);
    if (res.code === 0) {
      const token = res.data.token;
      const user = res.data.user;

      localStorage.setItem("token", token);
      localStorage.setItem("user", JSON.stringify(user));
      userStore.setUserInfo(token, user);

      // 默认保存用户名和密码
      localStorage.setItem("adminSavedUsername", formData.username);
      localStorage.setItem("adminSavedPassword", formData.password);

      MessagePlugin.success("登录成功");
      router.push("order");
    } else {
      MessagePlugin.warning(res.message || "登录失败");
    }
  } catch (e) {
    console.error("请求失败:", e);
    MessagePlugin.warning("网络请求失败");
  }
};

store文件夹下的user.ts文件。后续统一从这里获取用户信息。场景:例如用户个人信息模块。

import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
  state: () => ({
    token: "",
    user: null as any,
  }),
  actions: {
    setUserInfo(token: string, user: any) {
      this.token = token;
      this.user = user;
    },
    clearUserInfo() {
      this.token = "";
      this.user = null;
    },
  },
  persist: true, // 使用 pinia-plugin-persist 持久化
});

2.请求拦截器设置请求头

目的:只有登录成功,或token未过期的情况下才能访问相应接口。token过去则需要重新登录才能访问。

import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import { MessagePlugin } from "tdesign-vue-next";

const service = axios.create({
  // TODO
  baseURL: import.meta.env.VITE_API_BASE, //基础访问路径,我是引用过来的

  timeout: 10000,
  withCredentials: true,
});

//请求拦截器
service.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem("token");
    if (token) {
      config.headers["Authorization"] = token; // 使用 token 设置请求头
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

说明:本文只展示主要的流程,模糊处各位码友可以交流提供很好的方案,至于前端依赖的安装大家自己安装即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值