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

View File

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

View File

@@ -8,6 +8,7 @@ import com.qf.myafterprojecy.service.imp.ICategoryAttributeService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -53,6 +54,7 @@ public class CategoryAttributeController {
* @return 创建结果 * @return 创建结果
*/ */
@PostMapping @PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category_attribute> createAttribute(@Valid @RequestBody CategoryAttributeDto dto) { public ResponseMessage<Category_attribute> createAttribute(@Valid @RequestBody CategoryAttributeDto dto) {
log.info("接收创建分类属性的请求: 分类ID={}, 属性名称={}", log.info("接收创建分类属性的请求: 分类ID={}, 属性名称={}",
dto.getCategoryid(), dto.getAttributename()); dto.getCategoryid(), dto.getAttributename());
@@ -66,6 +68,7 @@ public class CategoryAttributeController {
* @return 更新结果 * @return 更新结果
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category_attribute> updateAttribute( public ResponseMessage<Category_attribute> updateAttribute(
@PathVariable Integer id, @PathVariable Integer id,
@Valid @RequestBody CategoryAttributeDto dto) { @Valid @RequestBody CategoryAttributeDto dto) {
@@ -80,6 +83,7 @@ public class CategoryAttributeController {
* @return 删除结果 * @return 删除结果
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Boolean> deleteAttribute(@PathVariable Integer id) { public ResponseMessage<Boolean> deleteAttribute(@PathVariable Integer id) {
log.info("接收删除分类属性的请求: ID={}", id); log.info("接收删除分类属性的请求: ID={}", id);
return categoryAttributeService.deleteCategoryAttribute(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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -55,6 +56,7 @@ public class CategoryController {
* @return 返回创建结果 * @return 返回创建结果
*/ */
@PostMapping @PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category> createCategory(@Valid @RequestBody CategoryDto categoryDto) { public ResponseMessage<Category> createCategory(@Valid @RequestBody CategoryDto categoryDto) {
log.info("接收创建分类的请求: {}", categoryDto.getTypename()); log.info("接收创建分类的请求: {}", categoryDto.getTypename());
return categoryService.saveCategory(categoryDto); return categoryService.saveCategory(categoryDto);
@@ -67,6 +69,7 @@ public class CategoryController {
* @return 返回更新结果 * @return 返回更新结果
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Category> updateCategory( public ResponseMessage<Category> updateCategory(
@PathVariable Integer id, @PathVariable Integer id,
@Valid @RequestBody CategoryDto categoryDto) { @Valid @RequestBody CategoryDto categoryDto) {
@@ -80,6 +83,7 @@ public class CategoryController {
* @return 返回删除结果 * @return 返回删除结果
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Boolean> deleteCategory(@PathVariable Integer id) { public ResponseMessage<Boolean> deleteCategory(@PathVariable Integer id) {
log.info("接收删除分类的请求: {}", id); log.info("接收删除分类的请求: {}", id);
return categoryService.deleteCategory(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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
@@ -87,6 +88,7 @@ public class MessageController {
* 创建新消息 * 创建新消息
*/ */
@PostMapping @PostMapping
@PreAuthorize("isAuthenticated()")
public ResponseMessage<Message> createMessage(@RequestBody MessageDto message) { public ResponseMessage<Message> createMessage(@RequestBody MessageDto message) {
logger.info("接收创建消息的请求: {}", message != null ? message.getNickname() : "null"); logger.info("接收创建消息的请求: {}", message != null ? message.getNickname() : "null");
return messageService.saveMessage(message); return messageService.saveMessage(message);
@@ -101,6 +103,7 @@ public class MessageController {
* 根据ID删除消息 * 根据ID删除消息
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public ResponseMessage<Message> deleteMessage(@PathVariable Integer id) { public ResponseMessage<Message> deleteMessage(@PathVariable Integer id) {
logger.info("接收删除消息的请求: {}", id); logger.info("接收删除消息的请求: {}", id);
return messageService.deleteMessage(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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; 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 org.springframework.web.bind.annotation.*;
import javax.validation.Valid; import javax.validation.Valid;
@@ -15,6 +17,7 @@ import java.util.List;
@RestController @RestController
@RequestMapping("/api/users") @RequestMapping("/api/users")
@Validated
public class UserController { public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class); private static final Logger logger = LoggerFactory.getLogger(UserController.class);
@@ -38,6 +41,7 @@ public class UserController {
* @return 用户列表 * @return 用户列表
*/ */
@GetMapping @GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<List<Users>> getAllUsers() { public ResponseMessage<List<Users>> getAllUsers() {
logger.info("获取所有用户列表"); logger.info("获取所有用户列表");
return userService.getAllUsers(); return userService.getAllUsers();
@@ -45,10 +49,10 @@ public class UserController {
/** /**
* 根据用户名获取用户信息 * 根据用户名获取用户信息
* @param username 用户名
* @return 用户信息 * @return 用户信息
*/ */
@GetMapping("/username/{username}") @GetMapping("/username/{username}")
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public ResponseMessage<Users> getUserByUsername(@PathVariable String username) { public ResponseMessage<Users> getUserByUsername(@PathVariable String username) {
logger.info("根据用户名获取用户信息,用户名: {}", username); logger.info("根据用户名获取用户信息,用户名: {}", username);
return userService.getUserByUsername(username); return userService.getUserByUsername(username);
@@ -72,6 +76,7 @@ public class UserController {
* @return 更新结果 * @return 更新结果
*/ */
@PutMapping("/{id}") @PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
public ResponseMessage<Users> updateUser(@PathVariable Long id, @Valid @RequestBody UserDto userDto) { public ResponseMessage<Users> updateUser(@PathVariable Long id, @Valid @RequestBody UserDto userDto) {
logger.info("更新用户信息用户ID: {}", id); logger.info("更新用户信息用户ID: {}", id);
return userService.updateUser(id, userDto); return userService.updateUser(id, userDto);
@@ -83,6 +88,7 @@ public class UserController {
* @return 删除结果 * @return 删除结果
*/ */
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<Boolean> deleteUser(@PathVariable Long id) { public ResponseMessage<Boolean> deleteUser(@PathVariable Long id) {
logger.info("删除用户用户ID: {}", id); logger.info("删除用户用户ID: {}", id);
return userService.deleteUser(id); return userService.deleteUser(id);
@@ -94,6 +100,7 @@ public class UserController {
* @return 用户列表 * @return 用户列表
*/ */
@GetMapping("/role/{role}") @GetMapping("/role/{role}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseMessage<List<Users>> getUsersByRole(@PathVariable int role) { public ResponseMessage<List<Users>> getUsersByRole(@PathVariable int role) {
logger.info("根据角色查询用户列表,角色: {}", role); logger.info("根据角色查询用户列表,角色: {}", role);
return userService.getUsersByRole(role); return userService.getUsersByRole(role);

View File

@@ -9,8 +9,7 @@ public class ArticleDto {
@NotBlank(message = "标题不能为空") @NotBlank(message = "标题不能为空")
private String title; private String title;
@NotBlank(message = "内容不能为空") private String content;// 如果为空说明是长篇文章 不为空则是短篇说说
private String content;
@NotNull(message = "属性ID不能为空") @NotNull(message = "属性ID不能为空")
private Integer attributeid; private Integer attributeid;
@@ -23,8 +22,7 @@ public class ArticleDto {
private Integer status; private Integer status;
@NotBlank(message = "Markdown内容不能为空") private String markdownscontent; // 文章内容的Markdown格式
private String markdownscontent;
// Getters and Setters // Getters and Setters
public Integer getArticleid() { 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,7 +72,7 @@ management.endpoint.health.show-details=when_authorized
management.metrics.export.prometheus.enabled=true management.metrics.export.prometheus.enabled=true
# JWT配置 - 生产环境应使用更安全的密钥和环境变量 # JWT配置 - 生产环境应使用更安全的密钥和环境变量
jwt.secret=mySecretKey jwt.secret=myAfterProjectSecretKey2024SecureJwtTokenGeneration
jwt.expiration=86400000 jwt.expiration=86400000
jwt.header=Authorization jwt.header=Authorization
jwt.token-prefix=Bearer jwt.token-prefix=Bearer