feat(security): 实现JWT认证并增强API安全控制

添加JWT依赖并实现token生成与验证功能
在控制器方法上添加权限注解保护API端点
更新安全配置以集成JWT过滤器
移除无用的编码测试工具类
修改JWT相关配置为更安全的设置
This commit is contained in:
qingfeng1121
2025-11-03 16:14:53 +08:00
parent f6d1d719a9
commit 25eeab4940
16 changed files with 17549 additions and 2561 deletions

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -81,6 +81,13 @@
<version>0.18.2</version>
</dependency>
<!-- JWT依赖 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- JCache API -->
<dependency>
<groupId>javax.cache</groupId>

View File

@@ -0,0 +1,96 @@
package com.qf.myafterprojecy.config;
import com.qf.myafterprojecy.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* JWT认证过滤器用于验证token并授权用户
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Value("${jwt.header:Authorization}")
private String tokenHeader;
@Value("${jwt.token-prefix:Bearer}")
private String tokenPrefix;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 获取token
String token = getTokenFromRequest(request);
System.out.println(token);
if (token != null && validateToken(token)) {
// 从token中获取用户名
String username = jwtUtils.getUsernameFromToken(token);
// 加载用户信息
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 e) {
logger.error("无法设置用户认证: {}", e);
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
/**
* 从请求头中获取token
*/
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader(tokenHeader);
if (bearerToken != null && bearerToken.startsWith(tokenPrefix + " ")) {
return bearerToken.substring(tokenPrefix.length() + 1);
}
return null;
}
/**
* 验证token
*/
private boolean validateToken(String token) {
try {
String username = jwtUtils.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return jwtUtils.validateToken(token, userDetails);
} catch (Exception e) {
logger.error("无效的token: {}", e);
return false;
}
}
}

View File

@@ -32,6 +32,9 @@ public class SecurityConfig {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 配置AuthenticationManager Bean
* 使用AuthenticationConfiguration来获取认证管理器这是更现代的方式
@@ -75,6 +78,9 @@ public class SecurityConfig {
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 添加JWT认证过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// 确保Spring Security不会添加额外的CharacterEncodingFilter
// 因为我们在CharacterEncodingConfig中已经配置了自定义的过滤器
http.addFilterBefore((request, response, chain) -> {
@@ -82,7 +88,7 @@ public class SecurityConfig {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
chain.doFilter(request, response);
}, UsernamePasswordAuthenticationFilter.class);
}, JwtAuthenticationFilter.class);
return http.build();
}

View File

@@ -1,6 +1,7 @@
package com.qf.myafterprojecy.controller;
import com.qf.myafterprojecy.config.ResponseMessage;
import com.qf.myafterprojecy.utils.JwtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -31,6 +32,9 @@ public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
/**
* 用户登录请求体
*/
@@ -77,11 +81,14 @@ public class AuthController {
// 获取认证后的用户信息
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 生成JWT token
String token = jwtUtils.generateToken(userDetails);
// 构建返回数据
Map<String, Object> data = new HashMap<>();
data.put("username", userDetails.getUsername());
data.put("authorities", userDetails.getAuthorities());
// data.put("message", "登录成功");
data.put("token", token);
data.put("tokenPrefix", jwtUtils.getTokenPrefix());
return ResponseMessage.success(data, "登录成功");

View File

@@ -8,6 +8,7 @@ import com.qf.myafterprojecy.service.imp.ICategoryAttributeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -53,6 +54,7 @@ public class CategoryAttributeController {
* @return 创建结果
*/
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category_attribute> createAttribute(@Valid @RequestBody CategoryAttributeDto dto) {
log.info("接收创建分类属性的请求: 分类ID={}, 属性名称={}",
dto.getCategoryid(), dto.getAttributename());
@@ -66,6 +68,7 @@ public class CategoryAttributeController {
* @return 更新结果
*/
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category_attribute> updateAttribute(
@PathVariable Integer id,
@Valid @RequestBody CategoryAttributeDto dto) {
@@ -80,6 +83,7 @@ public class CategoryAttributeController {
* @return 删除结果
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Boolean> deleteAttribute(@PathVariable Integer id) {
log.info("接收删除分类属性的请求: ID={}", id);
return categoryAttributeService.deleteCategoryAttribute(id);

View File

@@ -8,6 +8,7 @@ import com.qf.myafterprojecy.service.imp.ICategoryService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -55,6 +56,7 @@ public class CategoryController {
* @return 返回创建结果
*/
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category> createCategory(@Valid @RequestBody CategoryDto categoryDto) {
log.info("接收创建分类的请求: {}", categoryDto.getTypename());
return categoryService.saveCategory(categoryDto);
@@ -67,6 +69,7 @@ public class CategoryController {
* @return 返回更新结果
*/
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category> updateCategory(
@PathVariable Integer id,
@Valid @RequestBody CategoryDto categoryDto) {
@@ -80,6 +83,7 @@ public class CategoryController {
* @return 返回删除结果
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Boolean> deleteCategory(@PathVariable Integer id) {
log.info("接收删除分类的请求: {}", id);
return categoryService.deleteCategory(id);

View File

@@ -8,6 +8,7 @@ import com.qf.myafterprojecy.service.imp.IMessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -87,6 +88,7 @@ public class MessageController {
* 创建新消息
*/
@PostMapping
@PreAuthorize("isAuthenticated()")
public ResponseMessage<Message> createMessage(@RequestBody MessageDto message) {
logger.info("接收创建消息的请求: {}", message != null ? message.getNickname() : "null");
return messageService.saveMessage(message);
@@ -101,6 +103,7 @@ public class MessageController {
* 根据ID删除消息
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public ResponseMessage<Message> deleteMessage(@PathVariable Integer id) {
logger.info("接收删除消息的请求: {}", id);
return messageService.deleteMessage(id);

View File

@@ -8,6 +8,8 @@ import com.qf.myafterprojecy.service.imp.IUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@@ -15,6 +17,7 @@ import java.util.List;
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@@ -38,6 +41,7 @@ public class UserController {
* @return 用户列表
*/
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<List<Users>> getAllUsers() {
logger.info("获取所有用户列表");
return userService.getAllUsers();
@@ -45,10 +49,10 @@ public class UserController {
/**
* 根据用户名获取用户信息
* @param username 用户名
* @return 用户信息
*/
@GetMapping("/username/{username}")
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public ResponseMessage<Users> getUserByUsername(@PathVariable String username) {
logger.info("根据用户名获取用户信息,用户名: {}", username);
return userService.getUserByUsername(username);
@@ -72,6 +76,7 @@ public class UserController {
* @return 更新结果
*/
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public ResponseMessage<Users> updateUser(@PathVariable Long id, @Valid @RequestBody UserDto userDto) {
logger.info("更新用户信息用户ID: {}", id);
return userService.updateUser(id, userDto);
@@ -83,6 +88,7 @@ public class UserController {
* @return 删除结果
*/
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Boolean> deleteUser(@PathVariable Long id) {
logger.info("删除用户用户ID: {}", id);
return userService.deleteUser(id);
@@ -94,6 +100,7 @@ public class UserController {
* @return 用户列表
*/
@GetMapping("/role/{role}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<List<Users>> getUsersByRole(@PathVariable int role) {
logger.info("根据角色查询用户列表,角色: {}", role);
return userService.getUsersByRole(role);

View File

@@ -9,8 +9,7 @@ public class ArticleDto {
@NotBlank(message = "标题不能为空")
private String title;
@NotBlank(message = "内容不能为空")
private String content;
private String content;// 如果为空说明是长篇文章 不为空则是短篇说说
@NotNull(message = "属性ID不能为空")
private Integer attributeid;
@@ -23,8 +22,7 @@ public class ArticleDto {
private Integer status;
@NotBlank(message = "Markdown内容不能为空")
private String markdownscontent;
private String markdownscontent; // 文章内容的Markdown格式
// Getters and Setters
public Integer getArticleid() {

View File

@@ -1,38 +0,0 @@
// package com.qf.myafterprojecy.util;
// import org.slf4j.Logger;
// import org.slf4j.LoggerFactory;
// import org.springframework.stereotype.Component;
// import javax.annotation.PostConstruct;
// /**
// * 编码测试工具类
// * 用于验证系统编码配置是否正确,解决中文乱码问题
// */
// @Component
// public class EncodingTestUtil {
// private static final Logger logger = LoggerFactory.getLogger(EncodingTestUtil.class);
// /**
// * 在Bean初始化时执行编码测试
// * 验证日志系统是否能正确输出中文
// */
// @PostConstruct
// public void testEncoding() {
// // 输出系统编码信息
// logger.info("===== 系统编码测试开始 =====");
// logger.info("默认字符编码: {}", java.nio.charset.Charset.defaultCharset());
// logger.info("file.encoding: {}", System.getProperty("file.encoding"));
// logger.info("sun.stdout.encoding: {}", System.getProperty("sun.stdout.encoding"));
// logger.info("sun.stderr.encoding: {}", System.getProperty("sun.stderr.encoding"));
// // 测试中文字符输出
// logger.info("中文测试 - 这是一条测试日志,用于验证中文是否正常显示");
// logger.warn("中文警告测试 - 这是一条警告日志,用于验证中文是否正常显示");
// logger.error("中文错误测试 - 这是一条错误日志,用于验证中文是否正常显示");
// logger.info("===== 系统编码测试结束 =====");
// }
// }

View File

@@ -0,0 +1,103 @@
package com.qf.myafterprojecy.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* JWT工具类用于生成和验证token
*/
@Component
public class JwtUtils {
@Value("${jwt.secret:default_secret_key_for_development}")
private String secret;
@Value("${jwt.expiration:86400000}")
private long expiration;
@Value("${jwt.token-prefix:Bearer}")
private String tokenPrefix;
/**
* 从token中获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 从token中获取过期时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
/**
* 从token中获取指定的claim
*/
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* 获取token中的所有claims
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**
* 检查token是否过期
*/
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 生成token
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("authorities", userDetails.getAuthorities());
return doGenerateToken(claims, userDetails.getUsername());
}
/**
* 生成token的核心方法
*/
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 验证token
*/
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
/**
* 获取token前缀
*/
public String getTokenPrefix() {
return tokenPrefix;
}
}

View File

@@ -72,10 +72,10 @@ management.endpoint.health.show-details=when_authorized
management.metrics.export.prometheus.enabled=true
# JWT配置 - 生产环境应使用更安全的密钥和环境变量
jwt.secret=mySecretKey
jwt.secret=myAfterProjectSecretKey2024SecureJwtTokenGeneration
jwt.expiration=86400000
jwt.header=Authorization
jwt.token-prefix=Bearer
jwt.token-prefix=Bearer
# CORS配置 - 生产环境应限制允许的源
cors.allowed-origins=http://localhost:3000