feat(security): 实现JWT认证并增强API安全控制
添加JWT依赖并实现token生成与验证功能 在控制器方法上添加权限注解保护API端点 更新安全配置以集成JWT过滤器 移除无用的编码测试工具类 修改JWT相关配置为更安全的设置
This commit is contained in:
19819
logs/web_project.log
19819
logs/web_project.log
File diff suppressed because it is too large
Load Diff
Binary file not shown.
BIN
logs/web_project.log.2025-10-30.0.gz
Normal file
BIN
logs/web_project.log.2025-10-30.0.gz
Normal file
Binary file not shown.
BIN
logs/web_project.log.2025-10-31.0.gz
Normal file
BIN
logs/web_project.log.2025-10-31.0.gz
Normal file
Binary file not shown.
7
pom.xml
7
pom.xml
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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, "登录成功");
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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("===== 系统编码测试结束 =====");
|
||||
// }
|
||||
// }
|
||||
103
src/main/java/com/qf/myafterprojecy/utils/JwtUtils.java
Normal file
103
src/main/java/com/qf/myafterprojecy/utils/JwtUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user