项目目录
demo/
├── common
│ ├── base
│ │ ├── ResultCode.java
│ │ ├── ResultGenerator.java
│ │ └── Result.java
│ └── utils
│ ├── CustomUserDetails.java
│ ├── JWTAuthenticationFilter.java
│ ├── JWTAuthEntryPoint.java
│ ├── JWTUtil.java
│ ├── SM4Util.java
│ └── UserDetailsServiceImpl.java
├── configuration
│ ├── Log4jConfiguration.java
│ ├── OpenApiConfig.java
│ ├── RequestLoggingConfig.java
│ └── SecurityConfig.java
├── controller
├── DemoApplication.java
├── mapper
│ └── user
│ ├── RoleMapper.java
│ └── UserMapper.java
├── pojo
│ ├── dto
│ ├── entity
│ │ └── user
│ │ ├── Role.java
│ │ └── User.java
│ └── vo
└── service
├── impl
│ └── UserServiceImpl.java
└── IUserService.java
pom.xml
XML
<dependencies>
<!-- Web + 排除默认日志 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-jakarta-web</artifactId>
<version>2.24.3</version> <!-- 或最新版,如 2.24.3 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JDBC 支持(推荐显式添加) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<!-- 此配置在启动 Spring Boot 服务时,需要配置连接数据库 -->
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Plus for Spring Boot 3 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.7</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JWT (推荐使用 Auth0 的 Java-JWT) -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version> <!-- 最新版,请根据需要调整 -->
</dependency>
<!-- Swagger / OpenAPI UI (Springdoc for Spring Boot 3) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version> <!-- 兼容 Spring Boot 3 -->
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.9</version>
</dependency>
<!-- SM4 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
</dependency>
</dependencies>
applcation.yml
XML
# 应用基本信息
spring:
application:
name: SpringBootDemoBase
profiles:
active: dev
# 数据源配置(MySQL)
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 30000
pool-name: MyHikariCP
# Redis 配置
# data:
# redis:
# host: localhost
# port: 6379
# # password: your_redis_password # 如有密码请取消注释
# database: 0
# timeout: 2000ms
# lettuce:
# pool:
# max-active: 8
# max-idle: 8
# min-idle: 0
# max-wait: -1ms
# 服务器配置
server:
port: 18080
servlet:
context-path: /api # 注意上下文路径
# application.yml
mybatis-plus:
# mapper-locations: "classpath*:mapper/**/*.xml" # Mapper.xml文件地址,默认值
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
cache-enabled: false # 是否开启二级缓存
# 日志配置(Log4j2)
logging:
config: classpath:log4j2-dev.xml
# 注意:详细日志规则应在 log4j2-spring.xml 中定义,此处仅指定配置文件位置
# Swagger / OpenAPI (Springdoc)
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
packages-to-scan: org.example.springbootdemobase.manage.controller # 扫描位置
#e.g : 访问地址:http://localhost:8080/api/swagger-ui.html(注意上下文路径)
# JWT 自定义配置(供代码中注入使用)
jwt:
secret: yourSuperSecretKeyChangeItInProduction1234567890 # 至少 256 位
expiration: 86400 # 24 小时(秒)
# SM4
sm4:
key: 1234567890abcdef # 16 字节(128 位
统一响应
common/base/Result.java
java
public class Result <T> {
private int code;
private String message;
private T data;
private LocalDateTime time;
public Result() {
this.time = LocalDateTime.now();
}
public Result setCode(int code) {
this.code = code;
return this;
}
public Result setMessage(String message) {
this.message = message;
return this;
}
public Result setData(T data) {
this.data = data;
return this;
}
public Result setTime(LocalDateTime time) {
this.time = time;
return this;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
public T getData() {
return data;
}
public LocalDateTime getTime() {
return time;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
common/base/ResultCode.java
java
public enum ResultCode {
SUCCESS(200, "成功"),
FAILED(400, "失败"),
UNAUTHORIZED(401, "认证失败"),
NOT_FOUND(404, "未找到"),
FORBIDDEN(403, "无权限访问"),
INTERNAL_SERVER_ERROR(500, "服务端错误");
private final int code;
private final String message;
private ResultCode(int code, String msg) {
this.code = code;
this.message = msg;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
common/base/ResultGenerator.java
java
public class ResultGenerator {
public static Result success() {
return new Result()
.setCode(ResultCode.SUCCESS.getCode())
.setMessage(ResultCode.SUCCESS.getMessage());
}
public static <T> Result success(T data) {
return new Result<>()
.setCode(ResultCode.SUCCESS.getCode())
.setMessage(ResultCode.SUCCESS.getMessage())
.setData(data);
}
public static Result falied() {
return new Result()
.setCode(ResultCode.FAILED.getCode())
.setMessage(ResultCode.FAILED.getMessage());
}
public static Result falied(String message) {
return new Result()
.setCode(ResultCode.FAILED.getCode())
.setMessage(message);
}
public static Result error() {
return new Result()
.setCode(ResultCode.INTERNAL_SERVER_ERROR.getCode())
.setMessage(ResultCode.INTERNAL_SERVER_ERROR.getMessage());
}
}
日志配置
Log4jConfiguration.java
java
/**
* Log4j2在web中的过滤器 过滤REQUEST, FORWARD, INCLUDE, ERROR, ASYNC类型请求
*
* @author
* @date
*/
@Configuration
public class Log4jConfiguration {
@Bean
public FilterRegistrationBean log4jServletFilter() {
FilterRegistrationBean<Log4jServletFilter> filterRegistrationBean = new FilterRegistrationBean<>(new Log4jServletFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setDispatcherTypes(REQUEST, FORWARD, INCLUDE, ERROR, ASYNC);
return filterRegistrationBean;
}
}
RequestLoggingConfig.java
java
@Configuration
public class RequestLoggingConfig {
@Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter loggingFilter = new CommonsRequestLoggingFilter();
loggingFilter.setIncludeClientInfo(true); // 包含客户端 IP 和 session ID
loggingFilter.setIncludeQueryString(true); // 包含查询参数
loggingFilter.setIncludePayload(true); // 包含请求体(谨慎使用,可能泄露敏感数据)
loggingFilter.setMaxPayloadLength(1000); // 设置最大 payload 长度
loggingFilter.setIncludeHeaders(false); // 一般不需要头信息(可选)
return loggingFilter;
}
}
log4j2.xml
XML
<?xml version="1.0" encoding="UTF-8"?>
<!--
status : 这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成TRACE时,会看到log4j2内部各种详细输出
monitorInterval : Log4j能够自动检测修改配置文件和重新配置本身, 设置间隔秒数。此处表示每隔几秒重读一次配置文件.
日志级别:TRACE < DEBUG < INFO < WARN < ERROR < FATAL
如果设置为WARN,则低于WARN的信息都不会输出
-->
<Configuration status="INFO" monitorInterval="30">
<!-- 参数配置 -->
<Properties>
<!-- 配置日志文件输出目录 -->
<Property name="LOG_HOME">./logs</Property>
<!-- 日志输出文件名 -->
<property name="FILE_NAME">dev</property>
<!-- 日志格式化 -->
<property name="console_pattern_layout">
%highlight{%d{yyyy-MM-dd HH:mm:ss.SSS} [%5level][%logger{36}]-(%t)} %m%n
</property>
<property name="pattern_layout">
%d{yyyy-MM-dd HH:mm:ss.SSS} [%5level][%logger{36}]-(%t) %m%n
</property>
</Properties>
<!-- 日志配置Appender -->
<Appenders>
<!-- 输出控制台的配置 -->
<Console name="Console" target="SYSTEM_OUT">
<!-- ThresholdFilter:配置的日志过滤
如果要输出的日志级别在当前级别及以上,则为match,否则走mismatch
ACCEPT: 执行日志输出;DENY: 不执行日志输出,结束过滤;NEUTRAL: 不执行日志输出,执行下一个过滤器 -->
<!--<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>-->
<!-- 日志输出的格式
%d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间,输出到毫秒的时间
%-5p (level) : 输出日志级别,-5表示左对齐并且固定输出5个字符,如果不足在右边补0
%c (logger) : logger的名称(%logger)
%t (thread) : 输出当前线程名称
%m : 日志内容,即 logger.info("message")
%n : 换行符
%C : Java类名(%F)
%L : 行号
%M : 方法名
%l : 输出语句所在的行数, 包括类名、方法名、文件名、行数
hostName : 本地机器名
hostAddress : 本地ip地址
-->
<PatternLayout pattern="${console_pattern_layout}"/>
</Console>
<!-- 文件输出配置,文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用 -->
<!--<File name="log" fileName="log/test.log" append="false">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</File>-->
<!--
循环日志文件配置:日志文件大于阀值的时候,就开始写一个新的日志文件
这个会打印出所有的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档
fileName : 指定当前日志文件的位置和文件名称
filePattern : 指定当发生Rolling时,文件的转移和重命名规则
SizeBasedTriggeringPolicy : 指定当文件体积大于size指定的值时,触发Rolling
DefaultRolloverStrategy : 指定最多保存的文件个数
TimeBasedTriggeringPolicy : 这个配置需要和filePattern结合使用
注意filePattern中配置的文件重命名规则是${FILE_NAME}_%d{yyyy-MM-dd}_%i,最小的时间粒度是dd,即天,
TimeBasedTriggeringPolicy指定的size是1,结合起来就是每1天生成一个新文件
-->
<RollingRandomAccessFile name="ALL"
fileName="${LOG_HOME}/${FILE_NAME}.log"
filePattern="${LOG_HOME}/${FILE_NAME}.log.%d{yyyy-MM-dd}_%i">
<!--<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>-->
<PatternLayout pattern="${pattern_layout}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<!--<SizeBasedTriggeringPolicy size="100MB"/>-->
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>
<!-- 异步日志配置 -->
<Async name="AsyncAll">
<AppenderRef ref="Console"/>
<AppenderRef ref="ALL"/>
</Async>
</Appenders>
<!-- 日志记录Logger -->
<Loggers>
<Logger name="druid.sql" level="DEBUG" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<Logger name="druid.sql.Connection" level="INFO" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<Logger name="net.sf.ehcache" level="INFO" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<Logger name="org.springframework.web.filter.CommonsRequestLoggingFilter"
level="DEBUG" additivity="false">
<AppenderRef ref="AsyncAll"/>
</Logger>
<!--
Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。
level:日志输出级别,共有8个级别,按照从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF.
name:用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点.
AppenderRef:Logger的子节点,用来指定该日志输出到哪个Appender,如果没有指定,就会默认继承自Root.
如果指定了,那么会在指定的这个Appender和Root的Appender中都会输出,
此时我们可以设置Logger的additivity="false"只在自定义的Appender中进行输出。
-->
<Root level="INFO">
<AppenderRef ref="Console"/>
<AppenderRef ref="ALL"/>
</Root>
</Loggers>
</Configuration>
application.yml
XML
# 日志配置(Log4j2)
logging:
config: classpath:log4j2-dev.xml
# 注意:详细日志规则应在 log4j2-dev.xml 中定义,此处仅指定配置文件位置
Swagger配置
OpenApiConfig.java
java
@Configuration
public class OpenApiConfig {
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("springbootdemo API")
.version("1.0.0")
.description("API documentation for springbootdemo")
.contact(new Contact()
.name("Developer")
.email("developer@springbootdemo.com")));
}
}
application.yml
XML
# Swagger / OpenAPI (Springdoc)
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
enabled: true
packages-to-scan: org.example.springbootdemobase.manage.controller # 扫描位置
#e.g : 访问地址:http://localhost:8080/api/swagger-ui.html(注意上下文路径)
JWT
mapper/UserMapper.java
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
pojo/entity/user/User.java
java
@Data
@TableName("user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@TableField(value = "username")
private String username;
@TableField(value = "password")
private String password;
@TableField(value = "role_id") // 外键
private Long roleId;
/**
* 用户角色信息(非表字段,只做用户角色数据信息存储使用)
*/
// @Transient //注解用于标记实体类的字段为非数据库字段 不会被 JSON 序列化或 Java 序列化包含。
// 与 使用 @TableField(exist = false) - 效果相同
@TableField(exist = false)
private Role role;
@TableField(exist = false)
private String roleName;
}
common/utils/JWTUtil.java
java
import com.demo.pojo.entity.user.User;
@Component
public class JWTUtil {
@Value("${jwt.secret}")
private String SECRET;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 根据输入的user生成JWT令牌
* 可以创建一个带有过期时间及指定算法的签名令牌
*
* @param user 包含令牌payload数据的键值对,其中键是claim名称,值是claim值
* @return Token JWT令牌
*/
public String createToken(User user) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
// 创建Token构建器
JWTCreator.Builder builder = JWT.create();
// 设置payload中的claim,使用user的username,password设置
builder.withClaim("username",user.getUsername());
builder.withClaim("id",user.getId());
// 生成带有过期时间的token,并使用HMAC256算法进行签名
String token = builder.withExpiresAt(expiryDate)
.sign(Algorithm.HMAC256(SECRET));
return token;
}
public DecodedJWT verifyToken(String token) {
return JWT.require(Algorithm.HMAC256(SECRET))
.build().verify(token);
}
}
common/utils/JWTInterceptors.java
java
/**
*
* JWT 拦截器
*
* 与CorsConfig配合使用
*
* 若使用了 springboot Security, 此类可以弃用
*
* */
@Slf4j
public class JWTInterceptors implements HandlerInterceptor {
private JWTUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Result res;
// 获取请求头中令牌
String token = request.getHeader("Authorization");
// log.info("token:{}", token);
try {
// 验证令牌
DecodedJWT jwt = jwtUtil.verifyToken(token);
String username = jwt.getClaim("username").asString();
// ---- 认证通过后,接口中拿到登录用户的两种方法 ----
// 方式一:
// 步骤:1 将 username 放入 request 属性中,供后续 controller 使用
// 步骤:2 controller中使用 @RequestAttribute("username") String username 直接注入即可在接口中拿到当前登录的用户
request.setAttribute("username", username);
// 方式二:使用 ThreadLocal (适合多层调用)
// 步骤:1 创建一个 LoginUserHolder 工具类
// 步骤:2 设置当前登录用户 LoginUserHolder.setUsername(username)
// 步骤:3 在需要获取当前登录用户的地方使用 LoginUserHolder.getUsername() 获取当前登录用户
// 步骤:4 在 afterCompletion 或 finally 中清理
return true; // 放行请求
} catch (SignatureVerificationException e) {
res = ResultGenerator.unauthorized();
} catch (TokenExpiredException e) {
res = ResultGenerator.unauthorized();
} catch (Exception e) {
res = ResultGenerator.unauthorized();
}
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(res.toString());
return false;
}
// 接口请求离开后做一些清理工作
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// LoginUserHolder.clear(); // 防止内存泄漏
}
}
/**
* ThreadLocal(适合多层调用)
* 存储、获取当前登录的用户
* */
public LoginUserHolder {
private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
public static void setUsername(String username) {
userThreadLocal.set(username);
}
public static String getUsername() {
return userThreadLocal.get();
}
public static void clear() {
userThreadLocal.remove();
}
}
configuration/CorsConfig.java
java
/**
*
*
* 若使用了 springboot Security, CorsConfig 可弃用
*
* */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
// CORS 配置方法
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
// .allowedOriginPatterns("http://localhost:8000", "http://127.0.0.1:*") // allowedOriginPatterns("*") + allowCredentials(true) 的兼容性问题
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptors())
.addPathPatterns("/**") // 拦截所有 注意: Spring MVC 的拦截器路径匹配 不包含 server.servlet.context-path
.excludePathPatterns(
"/user/login", // 忽略登录接口
"/swagger-ui/**", // Swagger
"/v1/api-docs/**" // Swagger
);
}
}
springboot Security
jwt认证 + 接口权限控制
common/utils/CustomUserDetails.java
java
/**
*
* springboot Security
* 实现 UserDetails
*
* **/
@Slf4j
@Data
public class CustomUserDetails implements UserDetails {
private Long userId;
private String username;
private String password;
private String roleName;
private Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(User user) {
this.userId = user.getId();
this.username = user.getUsername();
this.password = user.getPassword();
// 注意:Spring Security 角色需以 ROLE_ 开头
// 数据库中的 role_name 是小写 "admin" 或 "root"
// 转换为 Spring Security 格式:ROLE_ADMIN 或 ROLE_ROOT
String dbRoleName = user.getRole().getRoleName();
this.roleName = "ROLE_" + dbRoleName.toUpperCase(); // 统一转为大写
log.info("用户角色转换: {} -> {}", dbRoleName, this.roleName);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(new SimpleGrantedAuthority(roleName));
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}
}
common/utils/UserDetailsServiceImpl.java
java
/**
* springboot Security
* 实现 UserDetailsService
* **/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* 数据库查询用户
* */
User user = userService.queryByUserName(username);
return new CustomUserDetails(user);
}
}
common/utils/JWTAuthenticationFilter.java
java
/**
*
* springboot Security
* 实现 OncePerRequestFilter 认证过滤
*
* */
@Slf4j
@Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中令牌
String authorization = request.getHeader("Authorization");
log.info("开始JWT认证过滤,Authorization: {}", authorization);
// 检测Authorization是否为空或格式是否正确
if (authorization == null || !authorization.startsWith("token ")) {
log.info("未找到有效token,跳过JWT认证");
filterChain.doFilter(request, response);
return;
}
// 去掉前缀 "token "
String token = authorization.substring(6);
try {
// 验证令牌
DecodedJWT jwt = jwtUtil.verifyToken(token);
String username = jwt.getClaim("username").asString();
log.info(token);
log.info(username);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 创建Authentication对象
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 设置到SecurityContext
SecurityContextHolder.getContext().setAuthentication(authentication);
log.info("用户 {} 认证成功,角色: {}", username, userDetails.getAuthorities());
// 1 将 username 放入 request 属性中,供后续 controller 使用
// 2 controller中使用 @RequestAttribute("username") String username 直接注入即可在接口中拿到当前登录的用户
request.setAttribute("username", username);
// 继续执行过滤器链
filterChain.doFilter(request, response);
} catch (Exception e) {
log.info(e.getMessage());
filterChain.doFilter(request, response);
}
}
}
common/utilsJWTAuthEntryPoint.java
java
/**
* springboot Security
* 当未登录或 token 失效时,返回 401
*/
@Component
public class JWTAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Result result = ResultGenerator.unauthorized();
response.getWriter().println(result.toString());
}
}
configuration/SecurityConfig.java
java
/**
*
* springboot Security: jwt认证 + 接口权限控制
*
* */
@Slf4j
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true) // 启用 @PreAuthorize 方法级安全控制
public class SecurityConfig {
@Autowired
private JWTAuthEntryPoint unauthorizedHandler;
@Autowired
private JWTAuthenticationFilter jwtAuthenticationFilter;
// 添加权限拒绝处理器, 若不加会出现用户权限不足导致静默失败,Spring Security 会拦截请求但可能没有正确返回错误信息
@Bean
public AccessDeniedHandler accessDeniedHandler() {
return (request, response, accessDeniedException) -> {
log.warn("权限不足: {}", accessDeniedException.getMessage());
log.warn("请求路径: {}", request.getRequestURI());
log.warn("当前用户权限: {}", SecurityContextHolder.getContext().getAuthentication().getAuthorities());
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Result result = ResultGenerator.genFailResult(ResultCode.FORBIDDEN, "权限不足");
response.getWriter().println(result.toString());
};
}
/**
* Spring Security 要创建一个怎样的 安全过滤器链(Security Filter Chain)
* 把 JWTAuthenticationFilter 插入到 UsernamePasswordAuthenticationFilter 之前执行
* */
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF
.csrf(csrf -> csrf.disable())
// 会话管理
.exceptionHandling(eh -> eh
// 认证失败处理
.authenticationEntryPoint(unauthorizedHandler)
// 权限不足处理
.accessDeniedHandler(accessDeniedHandler())
)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 认证配置
.authorizeHttpRequests(authz -> authz
// 公开接口 不包含 server.servlet.context-path
.requestMatchers(
"/user/login",
"/user/register",
"/swagger-ui.html",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/error"
).permitAll()
// ================
// 注意有优先级: 方法级注解 > 配置类中的匹配规则
// 是 "叠加判断" ------ 用户必须同时满足 配置类中的权限要求 和 方法层面的权限要求!
// 推荐策略 :统一使用方法级注解(推荐),只使用一种即可
// 管理员接口
//.requestMatchers("/admin/**").hasRole("ROOT")
// 用户接口(需要认证)
//.requestMatchers("/user/**").hasAnyRole("ROOT", "ADMIN")
// ================
// 其他接口需要认证
.anyRequest().authenticated()
)
// 添加JWT过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
方法级注解 控制接口权限案例
java
// 注意:不需要写 ROLE_ 前缀
@PreAuthorize("hasRole('ADMIN')")
@PostMapping("/admin/reset")
public Result resetPassword() { ... }
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
@GetMapping("/profile")
public Result profile() { ... }
认证流程
HTTP Request → Tomcat → DispatcherServlet
↓
Spring Security Filter Chain 执行(按顺序)
↓
[1] CORS Filter?
[2] CSRF Filter (已禁用)
[3] JWTAuthenticationFilter.doFilterInternal() ← 先执行这个
[4] UsernamePasswordAuthenticationFilter
[5] AnonymousAuthenticationFilter
[6] ExceptionTranslationFilter
[7] FilterSecurityInterceptor → 检查权限(.permitAll() 在这里起作用)
↓
到达 Controller
SM4
common/utils/SM4Util.java
java
@Component
public class SM4Util {
private static String KEY;
static {
// 注册 Bouncy Castle Provider(只需一次)
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
// SM4 密钥必须为 16 字节(128 位)
@Value("${sm4.key}")
private void setKey(String key){
KEY = key;
}
/**
* SM4 加密(ECB 模式,PKCS5Padding)
*/
public static String encrypt(String plainText) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "SM4");
Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encrypted); // 返回 Base64 字符串
}
/**
* SM4 解密(ECB 模式,PKCS5Padding)
*/
public static String decrypt(String cipherText) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), "SM4");
Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText));
return new String(decrypted, StandardCharsets.UTF_8);
}
// 测试
// public static void main(String[] args) throws Exception {
// String original = "Hello, 国密 SM4!";
// System.out.println("原文: " + original);
//
// String encrypted = encrypt(original);
// System.out.println("密文 (Base64): " + encrypted);
//
// String decrypted = decrypt(encrypted);
// System.out.println("解密后: " + decrypted);
//
// assert original.equals(decrypted) : "解密失败!";
// System.out.println("✅ 加解密成功!");
// }
}