2.认证与授权升级方案及使用
目录
1.方案提出背景
传统Spring Security的缺点
Spring Security+JWT的优点:
适用场景
2.在Srping Security基础上扩展JWT(后端)
(1).引入依赖(基于Maven)
(2).编写 JWT 工具类
(3).JWT中的secret
[1].secret的核心作用
[2].secret的生成规则
[3].生产环境的安全配置方案
(4).配置 Spring Security 过滤器
(5).登录成功时成功JWT
[1].修改JwtUtil
[2].修改JwtAuthenticationFilter
[3].修改 MyLoginSuccessHandler.java
[4].修改SecurityConfig
3.在Srping Security基础上扩展JWT(前端)
(1).安装必要依赖
(2).JWT 工具函数 (api/auth.js)
(3).修改请求拦截器
(4).用户状态管理 (stores/user.js)加到auth.js中
(5).整合 JWT 认证的路由守卫
(6).修改登录页面
1.方案提出背景
传统Spring Security的缺点
依赖会话管理:基于Session的认证机制,需要服务器存储会话信息,在分布式系统中可能面临会话同步问题。
扩展性受限:在微服务架构中,传统的Cookie-Session模式难以跨服务共享认证状态,需要额外解决方案如Session复制或集中存储。
CSRF防护负担:需要为每个状态变更请求配置CSRF令牌,增加开发复杂度,尤其在前后端分离架构中显得冗余。
移动端适配差:移动应用通常难以有效处理和管理Cookie,使得基于Session的认证机制适配性较差。
Spring Security+JWT的优点:
整合性与安全性
Spring Security与JWT结合提供了更灵活的认证和授权机制。传统Spring Security基于会话(Session)管理,依赖服务器存储用户状态,而JWT是无状态的,将用户信息加密存储在客户端令牌中,减轻服务器压力。
JWT的标准化结构(Header、Payload、Signature)确保数据完整性,签名机制防止篡改。Spring Security的过滤器链可无缝集成JWT验证逻辑,实现跨服务认证。
跨域与扩展性
JWT适合分布式系统和微服务架构,令牌可跨域传递,无需依赖集中式会话存储。传统Spring Security的会话管理在扩展时需考虑会话复制或共享存储(如Redis),增加复杂度。
JWT的Payload可自定义包含用户角色和权限,减少频繁查询数据库。Spring Security的GrantedAuthority
可直接解析JWT中的权限声明,简化授权流程。
性能与无状态
JWT减少了服务器端的会话存储开销,每次请求只需验证令牌签名。Spring Security的会话管理需要维护会话生命周期,可能引发性能瓶颈。
无状态特性更适合RESTful API,避免CSRF攻击。Spring Security需额外配置CSRF防护,而JWT通过签名验证天然规避此类问题。
适用场景
- Spring Security会话管理:适合单体应用或需严格会话控制的场景。
- Spring Security+JWT:适合微服务、前后端分离或需高扩展性的系统。
2.在Srping Security基础上扩展JWT(后端)
(1).引入依赖(基于Maven)
在 pom.xml
中添加 JWT 相关依赖,常用 jjwt
(Java JSON Web Token):
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.2</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.2</version><scope>runtime</scope>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.11.2</version><scope>runtime</scope>
</dependency>
也可根据需求选择其他 JWT 工具库,如 nimbus-jose-jwt
等。
(2).编写 JWT 工具类
用于生成、解析 JWT 令牌,示例:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Component
public class JwtUtil {// 密钥,实际应配置在 application.yml 等配置文件中,这里为演示写死private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // token 过期时间,单位毫秒,这里设为 1 小时,可根据需求调整private static final long EXPIRATION_TIME = 3600 * 1000; // 生成 JWT 令牌public String generateToken(String username) {Date now = new Date();Date expiration = new Date(now.getTime() + EXPIRATION_TIME);Map<String, Object> claims = new HashMap<>();claims.put("sub", username); // subject,一般存用户名等标识claims.put("iat", now); // issued atclaims.put("exp", expiration); // expirationreturn Jwts.builder().setClaims(claims).signWith(key, SignatureAlgorithm.HS256).compact();}// 解析 JWT 令牌,获取 claimspublic Claims parseToken(String token) {return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();}// 从令牌中获取用户名public String getUsernameFromToken(String token) {Claims claims = parseToken(token);return claims.getSubject();}// 验证令牌是否过期public boolean isTokenExpired(String token) {Claims claims = parseToken(token);Date expiration = claims.getExpiration();return expiration.before(new Date());}
}
实际项目中,密钥、过期时间等应配置在 application.yml
里,通过 @Value
注入,比如:
jwt:secret: your_secret_key_here # 实际要足够复杂、安全,可通过加密方式配置expiration: 3600000 # 过期时间,毫秒
(3).JWT中的secret
在 JWT 配置中,secret
是非常关键的安全参数,它用于对 JWT 进行签名和验证,必须保证足够安全。以下是关于secret
的详细说明及配置建议:
[1].secret
的核心作用
- 签名与验证:JWT 的第三部分(签名)通过
secret
对前两部分(头部、载荷)进行加密签名,确保令牌未被篡改。 - 安全基石:如果
secret
泄露,攻击者可伪造合法 JWT,直接绕过认证机制。
[2].secret
的生成规则
长度与复杂度要求
- 推荐长度:至少 256 位(32 字节),例如使用 64 位随机字符串(如
base64
编码的随机字节序列)。 - 字符要求:包含大小写字母、数字、特殊字符(如
!@#$%^&*()_+-=[]{}|;':",.<>/?
),避免纯字母或数字。
安全生成方式
工具生成:
- 使用 Java 的
SecureRandom
生成随机字节序列:
import java.security.SecureRandom;
import java.util.Base64;SecureRandom random = new SecureRandom();
byte[] bytes = new byte[32]; // 32字节=256位
random.nextBytes(bytes);
String secret = Base64.getEncoder().encodeToString(bytes);
System.out.println(secret);
- 使用命令行工具(如 Linux/macOS):
# 生成32字节随机字符串(base64编码)
openssl rand -base64 32
-
禁止行为:
- 避免使用硬编码的简单字符串(如
"123456"
、"jwtsecret"
)。 - 避免使用与项目相关的可猜测信息(如项目名、公司名、日期)。
- 避免使用硬编码的简单字符串(如
[3].生产环境的安全配置方案
1. 避免明文存储
- 环境变量:将
secret
存入服务器环境变量(如JWT_SECRET_KEY
),代码中通过System.getenv("JWT_SECRET_KEY")
获取。 - 配置中心:使用配置中心(如 Spring Cloud Config、Apollo)加密存储,避免直接写入配置文件。
2.Spring Boot 项目示例(以 application.yml 为例)
# 原始明文配置(仅用于开发环境,禁止生产使用)
jwt:secret: ${JWT_SECRET_KEY:your_secure_random_key_here} # 优先读取环境变量,默认值需为强密码expiration: 3600000# 生产环境推荐方案:通过环境变量注入
# 启动命令示例:java -DJWT_SECRET_KEY=your_actual_secret -jar app.jar
3.代码中读取 secret 的示例(Spring Security + JWT)
@Configuration
public class JwtConfig {private final String secret;public JwtConfig(@Value("${jwt.secret}") String secret) {this.secret = secret;}// JWT令牌生成器public String generateToken(Authentication authentication) {// 使用secret进行签名return Jwts.builder().setSubject(authentication.getName()).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(SignatureAlgorithm.HS256, secret).compact();}
}
(4).修改工具类,通过 @Value
注入:
import org.springframework.beans.factory.annotation.Value;
// 其他依赖...@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private long expiration;private Key key;@PostConstructpublic void init() {key = Keys.hmacShaKeyFor(secret.getBytes());}// 生成 JWT 令牌public String generateToken(String username) {//... 逻辑类似,只是用注入的 expiration 等Date now = new Date();Date expirationDate = new Date(now.getTime() + expiration);//...}// 其他方法...
}
改好之后的:
package com.jac.screen.util;import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private long expiration;private SecretKey key;// 新增:初始化密钥(如果没其他初始化逻辑,也可直接在构造器调用)public void initKey() {this.key = Keys.hmacShaKeyFor(secret.getBytes());}// 生成 JWT 令牌public String generateToken(String username) {Date now = new Date();Date expirationDate = new Date(now.getTime() + expiration);return Jwts.builder().setSubject(username).setIssuedAt(now).setExpiration(expirationDate).signWith(key, SignatureAlgorithm.HS256).compact();}// 新增:解析为 Jws<Claims>(供过滤器使用)public Jws<Claims> parseJws(String token) {return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);}// 原有方法保持不变(如果有的话)public Claims parseToken(String token) {return parseJws(token).getBody();}public boolean isTokenExpired(String token) {Date expiration = parseJws(token).getBody().getExpiration();return expiration.before(new Date());}public String getUsernameFromToken(String token) {return parseJws(token).getBody().getSubject();}// 获取密钥(如果需要的话)public SecretKey getKey() {return key;}
}
(4).配置 Spring Security 过滤器
[1].创建 JWT 认证过滤器,用于解析请求中的 JWT 令牌,进行身份验证:
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);private static final List<String> WHITE_LIST = Arrays.asList("/public/api","/auth/login");private final JwtUtil jwtUtil;private final UserDetailsService userDetailsService;public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {this.jwtUtil = jwtUtil;this.userDetailsService = userDetailsService;}@Overrideprotected boolean shouldNotFilter(HttpServletRequest request) {String path = request.getRequestURI();return WHITE_LIST.stream().anyMatch(path::startsWith);}private String extractToken(HttpServletRequest request) {String header = request.getHeader("Authorization");if (header != null && header.startsWith("Bearer ")) {return header.substring(7);}return null;}@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {// 跳过白名单路径if (shouldNotFilter(request)) {filterChain.doFilter(request, response);return;}String token = extractToken(request);if (token == null) {filterChain.doFilter(request, response);return;}try {// 关键修改:调用 parseJws 而非 parseTokenJws<Claims> jws = jwtUtil.parseJws(token);Claims claims = jws.getBody();if (jwtUtil.isTokenExpired(token)) {// 传入 JwsHeader 和 Claimsthrow new ExpiredJwtException(jws.getHeader(), claims, "Token已过期");}String username = claims.getSubject();UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (userDetails == null) {throw new UsernameNotFoundException("用户不存在: " + username);}UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authentication);} catch (ExpiredJwtException e) {logger.warn("JWT过期: {}", e.getMessage());SecurityContextHolder.clearContext();response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "令牌已过期");return;} catch (JwtException | UsernameNotFoundException e) {logger.error("JWT验证失败: {}", e.getMessage());SecurityContextHolder.clearContext();response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效令牌");return;}filterChain.doFilter(request, response);}
}
[2].在 Spring Security 配置类(继承 WebSecurityConfigurerAdapter
或使用新的配置方式 )中,配置该过滤器
@Beanprotected SecurityFilterChain filterChain(HttpSecurity http) throws Exception{// 自定义表单登录http.formLogin(form->{form.usernameParameter("username").passwordParameter("password").loginProcessingUrl("/admin/login").successHandler(new MyLoginSuccessHandler()).failureHandler(new MyLoginFailureHandler());});// 权限拦截配置http.authorizeHttpRequests(resp->{///login","/admin/loginresp.requestMatchers("/*").permitAll(); // 登录请求不需要认证resp.anyRequest().authenticated(); // 其余请求都需要认证});// 退出登录配置http.logout(logout->{logout.logoutUrl("/admin/logout") // 注销的路径.logoutSuccessHandler(new MyLogoutSuccessHandler()) // 登出成功处理器.clearAuthentication(true) // 清除认证数据.invalidateHttpSession(true); // 清除session});// 异常处理http.exceptionHandling(exception->{exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未登录处理器.accessDeniedHandler(new MyAccessDeniedHandler()); // 权限不足处理器});// 跨域访问http.cors(cors -> cors.configurationSource(request -> {CorsConfiguration config = new CorsConfiguration();config.setAllowedOrigins(List.of("http://localhost:6021")); // 允许的域名config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的 HTTP 方法config.setAllowedHeaders(List.of("*")); // 允许的请求头config.setAllowCredentials(true); // 允许携带凭证(如 cookies)config.setMaxAge(3600L); // 预检请求的缓存时间(秒)return config;}));// 添加 JWT 认证过滤器,在 UsernamePasswordAuthenticationFilter 之前执行http.addFilterBefore(new JwtAuthenticationFilter(new JwtUtil(), new MyUserDatailService()), UsernamePasswordAuthenticationFilter.class);// 关闭csrf防护http.csrf(csrf-> csrf.disable());return http.build();}
(5).登录成功时成功JWT
[1].修改JwtUtil
@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private long expiration;private SecretKey key;// 新增:初始化密钥(如果没其他初始化逻辑,也可直接在构造器调用)// 添加 @PostConstruct 注解确保密钥自动初始化@PostConstructpublic void initKey() {this.key = Keys.hmacShaKeyFor(secret.getBytes());}// 生成 JWT 令牌public String generateToken(String username) {Date now = new Date();Date expirationDate = new Date(now.getTime() + expiration);return Jwts.builder().setSubject(username).setIssuedAt(now).setExpiration(expirationDate).signWith(key, SignatureAlgorithm.HS256).compact();}// 新增:解析为 Jws<Claims>(供过滤器使用)public Jws<Claims> parseJws(String token) {return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);}// 原有方法保持不变(如果有的话)public Claims parseToken(String token) {return parseJws(token).getBody();}public boolean isTokenExpired(String token) {Date expiration = parseJws(token).getBody().getExpiration();return expiration.before(new Date());}public String getUsernameFromToken(String token) {return parseJws(token).getBody().getSubject();}// 获取密钥(如果需要的话)public SecretKey getKey() {return key;}// 新增获取过期时间的方法,供外部获取jwt的过期时间配置值public long getExpiration() {return expiration;}
}
[2].修改JwtAuthenticationFilter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);private static final List<String> WHITE_LIST = Arrays.asList("/public/api","/auth/login");private final JwtUtil jwtUtil;private final UserDetailsService userDetailsService;public JwtAuthenticationFilter(JwtUtil jwtUtil, UserDetailsService userDetailsService) {this.jwtUtil = jwtUtil;this.userDetailsService = userDetailsService;}@Overrideprotected boolean shouldNotFilter(HttpServletRequest request) {String path = request.getRequestURI();return WHITE_LIST.stream().anyMatch(path::startsWith);}private String extractToken(HttpServletRequest request) {String header = request.getHeader("Authorization");if (header != null && header.startsWith("Bearer ")) {return header.substring(7);}return null;}@Overrideprotected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, IOException {// 跳过白名单路径if (shouldNotFilter(request)) {filterChain.doFilter(request, response);return;}String token = extractToken(request);if (token == null) {filterChain.doFilter(request, response);return;}try {// 关键修改:调用 parseJws 而非 parseTokenJws<Claims> jws = jwtUtil.parseJws(token);Claims claims = jws.getBody();if (jwtUtil.isTokenExpired(token)) {// 传入 JwsHeader 和 Claimsthrow new ExpiredJwtException(jws.getHeader(), claims, "Token已过期");}String username = claims.getSubject();UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (userDetails == null) {throw new UsernameNotFoundException("用户不存在: " + username);}UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());SecurityContextHolder.getContext().setAuthentication(authentication);} catch (ExpiredJwtException e) {logger.warn("JWT过期: {}", e.getMessage());SecurityContextHolder.clearContext();response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "令牌已过期");return;} catch (JwtException | UsernameNotFoundException e) {logger.error("JWT验证失败: {}", e.getMessage());SecurityContextHolder.clearContext();response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效令牌");return;}filterChain.doFilter(request, response);}
}
[3].修改 MyLoginSuccessHandler.java
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {private final JwtUtil jwtUtil;private final Gson gson = new Gson();// 注入 JwtUtilpublic MyLoginSuccessHandler(JwtUtil jwtUtil) {this.jwtUtil = jwtUtil;// 初始化 JWT 密钥(如果没在其他地方初始化)this.jwtUtil.initKey();}@Overridepublic void onAuthenticationSuccess(HttpServletRequest request,HttpServletResponse response,Authentication authentication) throws IOException, ServletException {// 从 Authentication 中获取用户信息User user = (User) authentication.getPrincipal();String username = user.getUsername();// 生成 JWT 令牌String token = jwtUtil.generateToken(username);// 构造响应数据(包含 JWT)Map<String, Object> data = new HashMap<>();data.put("token", token);data.put("username", username);data.put("roles", user.getAuthorities());data.put("expiration", new Date(System.currentTimeMillis() + jwtUtil.getExpiration()));Result result = new Result();result.setCode(200);result.setMessage("登录成功");result.setData(data); // 将 JWT 放入响应数据response.setContentType("application/json;charset=utf-8");response.getWriter().write(gson.toJson(result));}
}
[4].修改SecurityConfig
/*** Security配置类*/
@Configuration
@EnableMethodSecurity
public class SecurityConfig {// SpringSecurity配置private final JwtUtil jwtUtil; // 注入 JwtUtilprivate final MyUserDetailsService userDetailsService; // 注意类名正确:MyUserDetailsService// 构造器注入public SecurityConfig(JwtUtil jwtUtil, MyUserDetailsService userDetailsService1) {this.jwtUtil = jwtUtil;this.userDetailsService = userDetailsService1;}@Beanprotected SecurityFilterChain filterChain(HttpSecurity http) throws Exception{// 自定义表单登录http.formLogin(form->{form.usernameParameter("username").passwordParameter("password").loginProcessingUrl("/admin/login").successHandler(new MyLoginSuccessHandler(jwtUtil)).failureHandler(new MyLoginFailureHandler());});// 权限拦截配置http.authorizeHttpRequests(resp->{///login","/admin/loginresp.requestMatchers("/*").permitAll(); // 登录请求不需要认证resp.anyRequest().authenticated(); // 其余请求都需要认证});// 退出登录配置http.logout(logout->{logout.logoutUrl("/admin/logout") // 注销的路径.logoutSuccessHandler(new MyLogoutSuccessHandler()) // 登出成功处理器.clearAuthentication(true) // 清除认证数据.invalidateHttpSession(true); // 清除session});// 异常处理http.exceptionHandling(exception->{exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()) // 未登录处理器.accessDeniedHandler(new MyAccessDeniedHandler()); // 权限不足处理器});// 跨域访问http.cors(cors -> cors.configurationSource(request -> {CorsConfiguration config = new CorsConfiguration();config.setAllowedOrigins(List.of("http://localhost:6021")); // 允许的域名config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 允许的 HTTP 方法config.setAllowedHeaders(List.of("*")); // 允许的请求头config.setAllowCredentials(true); // 允许携带凭证(如 cookies)config.setMaxAge(3600L); // 预检请求的缓存时间(秒)return config;}));// 正确:使用构造器注入的 Beanhttp.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userDetailsService), UsernamePasswordAuthenticationFilter.class);// 关闭csrf防护http.csrf(csrf-> csrf.disable());return http.build();}// 加密工具@Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
}
3.在Srping Security基础上扩展JWT(前端)
以下是前端集成 JWT 认证的完整步骤,以 Vue 3 + Axios 为例(其他框架如 React、Angular 逻辑类似):
(1).安装必要依赖
npm install axios vue-router pinia # 或 yarn add axios vue-router pinia
(2).JWT 工具函数 (api/auth.js
)
// utils/auth.js
export function getToken() {return localStorage.getItem('token');
}export function setToken(token) {localStorage.setItem('token', token);
}export function removeToken() {localStorage.removeItem('token');
}export function getTokenExpirationDate(token) {if (!token) return null;try {const decoded = JSON.parse(atob(token.split('.')[1]));if (decoded.exp) {return new Date(decoded.exp * 1000);}return null;} catch (error) {return null;}
}export function isTokenExpired(token) {const expiration = getTokenExpirationDate(token);return expiration < new Date();
}
(3).修改请求拦截器
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'; // 引入路由实例// 创建 axios 实例
const service = axios.create({baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // 从环境变量获取 API 基础路径timeout: 15000, // 请求超时时间headers: {'Content-Type': 'application/json'}
})// 请求拦截器
service.interceptors.request.use(config => {// 在这里可以添加 token 等认证信息const token = localStorage.getItem('token');if (token) {config.headers['Authorization'] = `Bearer ${token}`;}return config},error => {console.error('请求错误:', error)return Promise.reject(error)}
)// 增强型响应拦截器 - 关键修改点
service.interceptors.response.use(response => {// 1. 基础响应结构校验if (!response || !response.data) {ElMessage.error('响应数据格式异常');return Promise.reject(new Error('响应数据格式异常'));}const res = response.data;// 2. 业务状态码校验if (res.code === 200 || res.code === 0) {// 确保res.data存在才返回,避免null/undefinedreturn res.data || res;} else {// 处理业务错误ElMessage({message: res.message || '请求失败',type: 'error',duration: 3000});// 根据业务状态码进行不同处理switch (res.code) {case 401: // 未认证(如 token 过期)localStorage.removeItem('token');router.push({path: '/login',query: { redirect: router.currentRoute.value.fullPath }});break;case 403: // 权限不足router.push('/403');break;case 500: // 服务器内部错误ElMessage.error('服务器内部错误,请稍后重试');break;default:break;}return Promise.reject(new Error(res.message || '请求失败'));}},error => {console.error('响应错误:', error);// 处理网络错误if (!error.response) {ElMessage.error('网络连接异常,请检查网络设置');return Promise.reject(new Error('网络连接异常'));}const { status, data } = error.response;ElMessage({message: data.message || `请求失败 (${status})`,type: 'error',duration: 3000});return Promise.reject(error);}
)export default service
(4).用户状态管理 (stores/user.js
)加到auth.js中
import request from '@/utils/request';
import { getToken, setToken, removeToken, isTokenExpired } from '../utils/auth';// 用户状态(替代 Pinia 存储)
const userState = {token: getToken(),name: '',avatar: '',roles: []
};/*** 登录API* @param {Object} data - 登录参数 { username, password }* @returns {Promise<boolean>} - 登录是否成功*/
export function login(data) {return new Promise((resolve, reject) => {request({url: '/admin/login',method: 'post',data: data}).then(response => {const { token } = response.data.data;setToken(token);userState.token = token;resolve(true);}).catch(error => {console.error('登录失败:', error);reject(false);});});
}/*** 登出API* @returns {Promise<boolean>} - 登出是否成功*/
export function logout() {return new Promise((resolve, reject) => {request({url: '/admin/logout',method: 'post'}).then(() => {removeToken();userState.token = '';userState.name = '';userState.roles = [];resolve(true);}).catch(error => {console.error('登出失败:', error);removeToken();userState.token = '';resolve(false);});});
}/*** 获取用户信息API* @returns {Promise<Object>} - 用户信息*/
export function getInfo() {return new Promise((resolve, reject) => {if (!userState.token || isTokenExpired(userState.token)) {reject(new Error('未登录或令牌已过期'));return;}request({url: '/api/user/info',method: 'get'}).then(response => {const { username, roles, avatar } = response.data.data;userState.name = username;userState.roles = roles;userState.avatar = avatar;resolve(response.data);}).catch(error => {console.error('获取用户信息失败:', error);reject(error);});});
}/*** 获取当前用户状态* @returns {Object} - 用户状态 { token, name, avatar, roles }*/
export function getUserState() {return { ...userState };
}/*** 检查是否已登录* @returns {boolean} - 是否已登录*/
export function isLoggedIn() {return !!userState.token && !isTokenExpired(userState.token);
}/*** 检查是否有权限* @param {string} permission - 权限标识* @returns {boolean} - 是否有权限*/
export function hasPermission(permission) {return userState.roles.includes(permission);
}
(5).整合 JWT 认证的路由守卫
import { createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/layout/index.vue'
import config from '@/config'
import { getUserState, isLoggedIn, login, getInfo } from '@/api/auth' // 导入认证工具
import { ElMessage } from 'element-plus'// 前置路由守卫
router.beforeEach(async (to, from, next) => {// 1. 检查是否需要认证if (to.meta.requiresAuth) {const user = getUserState();// 2. 检查登录状态if (!isLoggedIn()) {ElMessage.warning('请先登录');return next({path: '/login',query: { redirect: to.fullPath }});}// 3. 检查用户信息是否已加载if (!user.roles || user.roles.length === 0) {try {await getInfo(); // 加载用户信息和权限next();} catch (error) {ElMessage.error('获取用户信息失败,请重新登录');next({ path: '/login' });}return;}// 4. 检查角色权限(如果定义了 roles)if (to.meta.roles && to.meta.roles.length > 0) {const hasRole = user.roles.some(role => to.meta.roles.includes(role));if (!hasRole) {return next({ name: '403' });}}next(); // 权限验证通过} else {// 不需要认证的路由直接通过next();}
});
(6).修改登录页面
[1].首先需要在登录成功后获取 JWT 令牌并存储到本地:
<script setup>const form = ref(null);
const router = useRouter()
const loading = ref(false); // 登录加载状态
const modelForm = reactive({username: '', password: ''});
const rules = reactive({username: [{required: true, message: '用户名不能为空'}],password: [{required: true, message: '密码不能为空'}],
});const onSubmit = async () => {form.value.validate(async (valid) => {if (valid) {loading.value = true;try {// 调用登录APIconst response = await login({username: modelForm.username,password: modelForm.password});// 假设响应中包含token字段const {token} = response.data.data;// 存储JWT令牌setToken(token);ElMessage.success('登录成功');// 跳转到首页router.push('/user/admin');} catch (error) {console.error('登录失败', error);ElMessage.error('用户名或密码错误');} finally {loading.value = false;}}})
}
</script>