SpringBoot 项目配置

项目目录

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("✅ 加解密成功!");
//    }
}
相关推荐
计算机毕业设计小途1 小时前
计算机毕业设计推荐:基于springboot的快递物流仓库管理系统【Java+spring boot+MySQL、Java项目、Java毕设、Java项目定制定
java·spring boot·mysql
月屯1 小时前
后端go完成文档分享链接功能
开发语言·后端·golang
苹果醋31 小时前
VueX(Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式)
java·运维·spring boot·mysql·nginx
Franciz小测测2 小时前
Python连接RabbitMQ三大方案全解析
开发语言·后端·ruby
海梨花2 小时前
又是秒杀又是高并发,你的接口真的扛得住吗?
java·后端·jmeter
小肖爱笑不爱笑2 小时前
2025/11/19 网络编程
java·运维·服务器·开发语言·计算机网络
Livingbody2 小时前
win11上wsl本地安装版本ubuntu25.10
后端
i***58672 小时前
springboot中配置logback-spring.xml
spring boot·spring·logback
郑州光合科技余经理2 小时前
开发指南:海外版外卖跑腿系统源码解析与定制
java·开发语言·mysql·spring cloud·uni-app·php·深度优先