1. 项目背景与技术选型
最近在重构一个后台管理系统,需要实现一套完整的用户认证体系。考虑到现代前后端分离架构的特点,最终选择了JWT(JSON Web Token)作为认证方案,配合Vue3前端和Spring Boot 3后端实现。这个组合在当前的Web开发中非常流行,但实际集成过程中发现不少细节问题需要特别注意。
为什么选择这个技术栈?首先,JWT的无状态特性非常适合RESTful API,避免了服务端存储session的开销。其次,Vue3的Composition API比Options API更灵活,配合TypeScript能大幅提升开发效率。后端选择MyBatis-Plus则是看中了它的强大CRUD能力和代码生成功能,可以节省大量重复工作。
提示:虽然JWT有很多优点,但在实际项目中要特别注意token过期时间和刷新机制的设计,否则会带来安全隐患。
2. 后端JWT实现细节
2.1 依赖配置与基础类创建
首先需要在pom.xml中添加必要的依赖:
xml复制<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
然后创建JWT工具类,封装token的生成和解析方法:
java复制public class JwtUtil {
private static final String SECRET_KEY = "your-256-bit-secret";
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256)
.compact();
}
public static String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY.getBytes())
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
2.2 Spring Security配置
配置Spring Security来保护API端点:
java复制@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
创建自定义的JWT认证过滤器:
java复制public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && JwtUtil.validateToken(jwt)) {
String username = JwtUtil.extractUsername(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
2.3 MyBatis-Plus集成与用户服务
使用MyBatis-Plus实现用户数据操作:
java复制@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public User register(UserDTO userDTO) {
if (lambdaQuery().eq(User::getUsername, userDTO.getUsername()).exists()) {
throw new RuntimeException("用户名已存在");
}
User user = new User();
user.setUsername(userDTO.getUsername());
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
user.setRoles("ROLE_USER");
save(user);
return user;
}
@Override
public String login(String username, String password) {
User user = lambdaQuery().eq(User::getUsername, username).one();
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
return JwtUtil.generateToken(user);
}
}
3. 前端Vue3实现
3.1 项目初始化与依赖安装
使用Vite创建Vue3项目:
bash复制npm create vite@latest my-project --template vue-ts
cd my-project
npm install axios vue-router pinia element-plus
配置axios拦截器处理JWT:
typescript复制// src/utils/http.ts
import axios from 'axios'
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 5000
})
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) => {
return response.data
},
(error) => {
if (error.response.status === 401) {
// token过期处理
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default service
3.2 登录页面实现
使用Element Plus构建登录表单:
vue复制<template>
<div class="login-container">
<el-form :model="form" :rules="rules" ref="loginForm">
<el-form-item prop="username">
<el-input v-model="form.username" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" type="password" placeholder="密码"></el-input>
</el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import http from '@/utils/http'
const router = useRouter()
const form = ref({
username: '',
password: ''
})
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const handleLogin = async () => {
try {
const res = await http.post('/api/auth/login', form.value)
localStorage.setItem('token', res.token)
router.push('/')
} catch (error) {
ElMessage.error('登录失败')
}
}
</script>
3.3 路由守卫与权限控制
配置路由守卫检查登录状态:
typescript复制// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/',
component: () => import('@/views/Home.vue'),
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else {
next()
}
})
export default router
4. 常见问题与优化方案
4.1 JWT安全问题
在实际项目中,JWT的安全问题需要特别注意:
-
Token存储:前端不要使用localStorage存储敏感信息,可以考虑使用httpOnly的cookie,但要注意CSRF防护。
-
Token过期:设置合理的过期时间(通常15-30分钟),并实现token刷新机制。
-
密钥管理:生产环境不要使用硬编码的密钥,应该从环境变量或配置中心获取。
4.2 性能优化
- MyBatis-Plus二级缓存:对于频繁查询但不常变更的数据,可以启用二级缓存:
java复制@Configuration
@MapperScan("com.example.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
interceptor.addInnerInterceptor(new CachingInnerInterceptor());
return interceptor;
}
}
- 前端请求节流:使用lodash的throttle函数防止重复提交:
typescript复制import { throttle } from 'lodash-es'
const handleLogin = throttle(async () => {
// 登录逻辑
}, 1000)
4.3 跨域问题解决方案
虽然开发环境可以通过Vite代理解决跨域,但生产环境需要后端配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.maxAge(3600);
}
}
4.4 接口文档生成
使用Swagger生成API文档:
java复制@Configuration
@EnableOpenApi
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.OAS_30)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.controller"))
.paths(PathSelectors.any())
.build()
.securitySchemes(List.of(apiKey()))
.securityContexts(List.of(securityContext()));
}
private ApiKey apiKey() {
return new ApiKey("Authorization", "Authorization", "header");
}
private SecurityContext securityContext() {
return SecurityContext.builder()
.securityReferences(List.of(new SecurityReference("Authorization",
new AuthorizationScope[0])))
.build();
}
}
5. 项目部署与监控
5.1 前端部署
使用Nginx部署Vue3项目:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /var/www/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
5.2 后端部署
使用Docker部署Spring Boot应用:
dockerfile复制FROM openjdk:17-jdk-slim
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
5.3 监控方案
集成Prometheus和Grafana监控系统:
- 添加依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
- 配置application.yml:
yaml复制management:
endpoints:
web:
exposure:
include: health,info,prometheus
metrics:
tags:
application: ${spring.application.name}
- 在Grafana中导入Spring Boot仪表板模板(ID: 6756)
