update
This commit is contained in:
@@ -5,21 +5,23 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-parent</artifactId>
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
<version>2.1.8.RELEASE</version>
|
<version>3.3.5</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/>
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.yaoyuan</groupId>
|
<groupId>com.yaoyuan</groupId>
|
||||||
<artifactId>jiscuss</artifactId>
|
<artifactId>jiscuss</artifactId>
|
||||||
<!-- <packaging>war</packaging> -->
|
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>jiccuss</name>
|
<name>jiccuss</name>
|
||||||
<description>jiccuss project for Spring Boot</description>
|
<description>Jiscuss forum project for Spring Boot 3</description>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<java.version>1.8</java.version>
|
<!-- Java 17 matches the installed JDK (openjdk-17). Upgrade to 21 when JDK 21 is installed. -->
|
||||||
|
<java.version>17</java.version>
|
||||||
|
<mapstruct.version>1.6.3</mapstruct.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- Spring Boot Starters -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
@@ -28,7 +30,7 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-freemarker</artifactId>
|
<artifactId>spring-boot-starter-freemarker</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- spring security -->
|
<!-- Spring Security -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
@@ -41,63 +43,94 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
<!-- h2 -->
|
<!-- Bean Validation (input validation) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Actuator for health checks and metrics -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Cache -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- H2 (dev/test only) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.h2database</groupId>
|
<groupId>com.h2database</groupId>
|
||||||
<artifactId>h2</artifactId>
|
<artifactId>h2</artifactId>
|
||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- mysql -->
|
<!-- MySQL 8 connector (updated groupId from mysql:mysql-connector-java) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>mysql</groupId>
|
<groupId>com.mysql</groupId>
|
||||||
<artifactId>mysql-connector-java</artifactId>
|
<artifactId>mysql-connector-j</artifactId>
|
||||||
<version>5.1.30</version>
|
<scope>runtime</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- json -->
|
<!-- JSON (upgraded from 20180813) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.json</groupId>
|
<groupId>org.json</groupId>
|
||||||
<artifactId>json</artifactId>
|
<artifactId>json</artifactId>
|
||||||
<version>20180813</version>
|
<version>20240303</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- swagger2-->
|
<!-- SpringDoc OpenAPI 2 — replaces discontinued springfox-swagger2, compatible with Spring Boot 3 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.springfox</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
<artifactId>springfox-swagger2</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||||
<version>2.9.2</version>
|
<version>2.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<!-- Lombok -->
|
||||||
<groupId>io.springfox</groupId>
|
|
||||||
<artifactId>springfox-swagger-ui</artifactId>
|
|
||||||
<version>2.9.2</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- lombok -->
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<scope>compile</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 阿里系的Druid依赖包 -->
|
<!-- MapStruct for type-safe DTO mapping -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Druid Spring Boot 3 starter (Spring Boot 3 compatible version) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>druid-spring-boot-starter</artifactId>
|
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||||
<version>1.1.18</version>
|
<version>1.2.23</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!--使用ehcache -->
|
<!-- Ehcache 3 with Jakarta namespace (for Spring Boot 3 / JCache / JSR-107) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.ehcache</groupId>
|
||||||
<artifactId>spring-boot-starter-cache</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.sf.ehcache</groupId>
|
|
||||||
<artifactId>ehcache</artifactId>
|
<artifactId>ehcache</artifactId>
|
||||||
|
<version>3.10.8</version>
|
||||||
|
<classifier>jakarta</classifier>
|
||||||
|
</dependency>
|
||||||
|
<!-- JCache API (JSR-107) required for Ehcache 3 Spring integration -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>javax.cache</groupId>
|
||||||
|
<artifactId>cache-api</artifactId>
|
||||||
|
<version>1.1.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Flyway for database schema version management -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<!-- Flyway MySQL dialect support (required for MySQL 8+) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-mysql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
@@ -106,6 +139,35 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<excludes>
|
||||||
|
<exclude>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
</exclude>
|
||||||
|
</excludes>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<source>${java.version}</source>
|
||||||
|
<target>${java.version}</target>
|
||||||
|
<!-- Ensure Lombok and MapStruct annotation processors cooperate -->
|
||||||
|
<annotationProcessorPaths>
|
||||||
|
<path>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</path>
|
||||||
|
<path>
|
||||||
|
<groupId>org.mapstruct</groupId>
|
||||||
|
<artifactId>mapstruct-processor</artifactId>
|
||||||
|
<version>${mapstruct.version}</version>
|
||||||
|
</path>
|
||||||
|
</annotationProcessorPaths>
|
||||||
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|||||||
@@ -7,19 +7,11 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
@Configuration
|
@Configuration
|
||||||
public class MyMvcConfig implements WebMvcConfigurer {
|
public class MyMvcConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
/*
|
|
||||||
* addResourceHandlers
|
|
||||||
* void
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
// SpringDoc OpenAPI serves its own UI resources — no manual mapping needed.
|
||||||
registry.addResourceHandler("swagger-ui.html")
|
// Static application assets:
|
||||||
.addResourceLocations("classpath:/META-INF/resources/");
|
|
||||||
registry.addResourceHandler("/webjars/**")
|
|
||||||
.addResourceLocations("classpath:/META-INF/resources/webjars/");
|
|
||||||
registry.addResourceHandler("/static/**")
|
registry.addResourceHandler("/static/**")
|
||||||
.addResourceLocations("classpath:/static/");
|
.addResourceLocations("classpath:/static/");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,29 @@
|
|||||||
package com.yaoyuan.jiscuss.config;
|
package com.yaoyuan.jiscuss.config;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.models.OpenAPI;
|
||||||
|
import io.swagger.v3.oas.models.info.Contact;
|
||||||
|
import io.swagger.v3.oas.models.info.Info;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import springfox.documentation.builders.ApiInfoBuilder;
|
|
||||||
import springfox.documentation.builders.PathSelectors;
|
|
||||||
import springfox.documentation.builders.RequestHandlerSelectors;
|
|
||||||
import springfox.documentation.service.ApiInfo;
|
|
||||||
import springfox.documentation.spi.DocumentationType;
|
|
||||||
import springfox.documentation.spring.web.plugins.Docket;
|
|
||||||
import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SpringDoc OpenAPI 2.x configuration.
|
||||||
|
*
|
||||||
|
* <p>Replaces discontinued springfox-swagger2 (incompatible with Spring Boot 3).
|
||||||
|
* UI is available at /swagger-ui/index.html
|
||||||
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@EnableSwagger2
|
|
||||||
public class SwaggerConfiguration {
|
public class SwaggerConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Docket createRestApi() {
|
public OpenAPI jiscussOpenAPI() {
|
||||||
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
|
return new OpenAPI()
|
||||||
.apis(RequestHandlerSelectors.basePackage("com.yaoyuan.jiscuss.controller")).paths(PathSelectors.any())
|
.info(new Info()
|
||||||
.build();
|
.title("Jiscuss REST APIs")
|
||||||
|
.description("Jiscuss forum system API documentation")
|
||||||
|
.version("1.0")
|
||||||
|
.contact(new Contact()
|
||||||
|
.name("Jiscuss Team")
|
||||||
|
.email("developer@mail.com")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
private ApiInfo apiInfo() {
|
|
||||||
return new ApiInfoBuilder().title("swagger-ui RESTful APIs").description("Jiscuss")
|
|
||||||
.termsOfServiceUrl("http://localhost/").contact("developer@mail.com").version("1.0").build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,104 +1,157 @@
|
|||||||
package com.yaoyuan.jiscuss.config;
|
package com.yaoyuan.jiscuss.config;
|
||||||
|
|
||||||
import com.yaoyuan.jiscuss.handler.CustomAccessDeniedHandler;
|
import com.yaoyuan.jiscuss.handler.CustomAccessDeniedHandler;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Configurable;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.WebSecurity;
|
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
|
||||||
|
import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* prePostEnabled :决定Spring Security的前注解是否可用 [@PreAuthorize,@PostAuthorize,..]
|
* Spring Security 6 configuration (Spring Boot 3 compatible).
|
||||||
* secureEnabled : 决定是否Spring Security的保障注解 [@Secured] 是否可用
|
*
|
||||||
* jsr250Enabled :决定 JSR-250 annotations 注解[@RolesAllowed..] 是否可用.
|
* <p>Breaking changes from Spring Boot 2.x:
|
||||||
* 开启 Spring Security 方法级安全注解 @EnableGlobalMethodSecurity
|
* <ul>
|
||||||
|
* <li>{@code WebSecurityConfigurerAdapter} removed → use {@code SecurityFilterChain} bean</li>
|
||||||
|
* <li>{@code antMatchers} → {@code requestMatchers}</li>
|
||||||
|
* <li>{@code @EnableGlobalMethodSecurity} → {@code @EnableMethodSecurity}</li>
|
||||||
|
* <li>{@code @Configurable} (AspectJ) corrected to {@code @Configuration}</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@Configurable
|
@Configuration
|
||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
|
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
|
||||||
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
public class WebSecurityConfig {
|
||||||
|
|
||||||
@Autowired
|
private final CustomAccessDeniedHandler customAccessDeniedHandler;
|
||||||
private CustomAccessDeniedHandler customAccessDeniedHandler;
|
private final UserDetailsService userDetailsService;
|
||||||
|
private final ApplicationContext applicationContext;
|
||||||
|
|
||||||
@Qualifier("userDetailServiceImpl")
|
/** Controlled by spring.h2.console.enabled — only true in dev (h2) profile. */
|
||||||
@Autowired
|
@Value("${spring.h2.console.enabled:false}")
|
||||||
private UserDetailsService userDetailsService;
|
private boolean h2ConsoleEnabled;
|
||||||
|
|
||||||
|
public WebSecurityConfig(
|
||||||
|
CustomAccessDeniedHandler customAccessDeniedHandler,
|
||||||
|
@Qualifier("userDetailServiceImpl") UserDetailsService userDetailsService,
|
||||||
|
ApplicationContext applicationContext) {
|
||||||
|
this.customAccessDeniedHandler = customAccessDeniedHandler;
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 静态资源设置
|
* Exclude truly static assets from the security filter chain entirely.
|
||||||
|
* H2 console and Druid monitoring are NOT excluded here — they are secured via filterChain.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Bean
|
||||||
public void configure(WebSecurity webSecurity) {
|
public WebSecurityCustomizer webSecurityCustomizer() {
|
||||||
//不拦截静态资源,所有用户均可访问的资源
|
return web -> web.ignoring().requestMatchers(
|
||||||
webSecurity.ignoring().antMatchers(
|
"/css/**", "/js/**", "/images/**", "/static/**", "/favicon.ico"
|
||||||
"/",
|
|
||||||
"/css/**",
|
|
||||||
"/js/**",
|
|
||||||
"/images/**",
|
|
||||||
"/static/**",
|
|
||||||
"/h2-console/**",
|
|
||||||
"/test/**",
|
|
||||||
"/druid/**"
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* http请求设置
|
* Main security filter chain.
|
||||||
|
*
|
||||||
|
* <p>Security decisions:
|
||||||
|
* <ul>
|
||||||
|
* <li>H2 console: only accessible when enabled (dev profile) and requires ROLE_ADMIN</li>
|
||||||
|
* <li>Druid monitoring: requires ROLE_ADMIN (Druid also has its own login page)</li>
|
||||||
|
* <li>Admin/Actuator paths: requires ROLE_ADMIN</li>
|
||||||
|
* <li>SpringDoc UI paths: public (API docs are for developers)</li>
|
||||||
|
* <li>All other authenticated requests: evaluated by {@code RbacPermission.hasPermission}</li>
|
||||||
|
* <li>X-Frame-Options: SAMEORIGIN (prevents clickjacking while allowing H2 console iframe)</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
@Override
|
@Bean
|
||||||
public void configure(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
//http.csrf().disable(); //注释就是使用 csrf 功能
|
// Fix: SAMEORIGIN instead of disable() — prevents clickjacking, allows H2 iframe in same origin
|
||||||
http.headers().frameOptions().disable();//解决 in a frame because it set 'X-Frame-Options' to 'DENY' 问题
|
http.headers(headers -> headers
|
||||||
//http.anonymous().disable();
|
.frameOptions(frame -> frame.sameOrigin())
|
||||||
http.authorizeRequests()
|
);
|
||||||
.antMatchers("/login/**", "/initUserData")//不拦截登录相关方法
|
|
||||||
.permitAll()
|
|
||||||
//.antMatchers("/user").hasRole("ADMIN") // user接口只有ADMIN角色的可以访问
|
|
||||||
//.anyRequest()
|
|
||||||
//.authenticated()// 任何尚未匹配的URL只需要验证用户即可访问
|
|
||||||
.anyRequest()
|
|
||||||
.access("@rbacPermission.hasPermission(request, authentication)")//根据账号权限访问
|
|
||||||
.and()
|
|
||||||
.formLogin()
|
|
||||||
//.loginPage("/")
|
|
||||||
.loginPage("/login") //登录请求页
|
|
||||||
.loginProcessingUrl("/login") //登录POST请求路径
|
|
||||||
.usernameParameter("username") //登录用户名参数
|
|
||||||
.passwordParameter("password") //登录密码参数
|
|
||||||
.defaultSuccessUrl("/index") //默认登录成功页面
|
|
||||||
.and()
|
|
||||||
.exceptionHandling()
|
|
||||||
.accessDeniedHandler(customAccessDeniedHandler) //无权限处理器
|
|
||||||
.and()
|
|
||||||
.logout()
|
|
||||||
.logoutSuccessUrl("/login?logout"); //退出登录成功URL
|
|
||||||
|
|
||||||
|
// CSRF: disable for H2 console path only (H2 web console does not send CSRF tokens)
|
||||||
|
if (h2ConsoleEnabled) {
|
||||||
|
http.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the RBAC expression manager with ApplicationContext so @bean references work
|
||||||
|
DefaultWebSecurityExpressionHandler expressionHandler = new DefaultWebSecurityExpressionHandler();
|
||||||
|
expressionHandler.setApplicationContext(applicationContext);
|
||||||
|
WebExpressionAuthorizationManager rbacManager = new WebExpressionAuthorizationManager(
|
||||||
|
"@rbacPermission.hasPermission(request, authentication)");
|
||||||
|
rbacManager.setExpressionHandler(expressionHandler);
|
||||||
|
|
||||||
|
http.authorizeHttpRequests(auth -> {
|
||||||
|
// Publicly accessible
|
||||||
|
auth.requestMatchers(
|
||||||
|
"/login/**", "/initUserData",
|
||||||
|
// SpringDoc OpenAPI UI and spec endpoint (developer tools, no sensitive data)
|
||||||
|
"/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**"
|
||||||
|
).permitAll();
|
||||||
|
|
||||||
|
// H2 console: admin-only, dev profile only
|
||||||
|
if (h2ConsoleEnabled) {
|
||||||
|
auth.requestMatchers("/h2-console/**").hasRole("ADMIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Druid monitoring, Actuator, Admin UI: restricted to ROLE_ADMIN
|
||||||
|
auth.requestMatchers("/druid/**").hasRole("ADMIN");
|
||||||
|
auth.requestMatchers("/actuator/**").hasRole("ADMIN");
|
||||||
|
auth.requestMatchers("/admin/**").hasRole("ADMIN");
|
||||||
|
|
||||||
|
// All remaining requests evaluated by RBAC permission bean
|
||||||
|
auth.anyRequest().access(rbacManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
http.formLogin(form -> form
|
||||||
|
.loginPage("/login")
|
||||||
|
.loginProcessingUrl("/login")
|
||||||
|
.usernameParameter("username")
|
||||||
|
.passwordParameter("password")
|
||||||
|
.defaultSuccessUrl("/index")
|
||||||
|
);
|
||||||
|
|
||||||
|
http.logout(logout -> logout
|
||||||
|
.logoutSuccessUrl("/login?logout")
|
||||||
|
);
|
||||||
|
|
||||||
|
http.exceptionHandling(ex -> ex
|
||||||
|
.accessDeniedHandler(customAccessDeniedHandler)
|
||||||
|
);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义获取用户信息接口
|
* Authentication manager wired with BCrypt — replaces the old
|
||||||
|
* {@code configure(AuthenticationManagerBuilder)} override.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Bean
|
||||||
public void configure(AuthenticationManagerBuilder auth) throws Exception {
|
public AuthenticationManager authenticationManager(BCryptPasswordEncoder passwordEncoder) {
|
||||||
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||||
|
provider.setUserDetailsService(userDetailsService);
|
||||||
|
provider.setPasswordEncoder(passwordEncoder);
|
||||||
|
return new ProviderManager(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** BCrypt password encoder bean. */
|
||||||
* 密码加密算法
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Bean
|
@Bean
|
||||||
public BCryptPasswordEncoder passwordEncoder() {
|
public BCryptPasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import org.springframework.ui.ModelMap;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author yaoyuan2.chu
|
* @author yaoyuan2.chu
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import com.yaoyuan.jiscuss.entity.UserInfo;
|
|||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
import org.springframework.security.core.context.SecurityContext;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.springframework.ui.ModelMap;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author yaoyuan2.chu
|
* @author yaoyuan2.chu
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
|
|||||||
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import com.yaoyuan.jiscuss.entity.Discussion;
|
|||||||
import com.yaoyuan.jiscuss.exception.BaseException;
|
import com.yaoyuan.jiscuss.exception.BaseException;
|
||||||
import com.yaoyuan.jiscuss.response.ResponseCode;
|
import com.yaoyuan.jiscuss.response.ResponseCode;
|
||||||
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
import com.yaoyuan.jiscuss.service.IDiscussionsService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
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;
|
||||||
@@ -20,15 +20,16 @@ import java.util.List;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping(path = "/other_api", produces = "application/json;charset=utf-8")
|
@RequestMapping(path = "/other_api", produces = "application/json;charset=utf-8")
|
||||||
@Api(value = "主题帖子(其他)接口管理", tags = {"主题帖子(其他)接口管理"})
|
@Tag(name = "主题帖子(其他)接口管理", description = "主题相关 REST API")
|
||||||
public class RestPostController {
|
public class RestPostController {
|
||||||
private static Logger logger = LoggerFactory.getLogger(RestPostController.class);
|
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(RestPostController.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IDiscussionsService discussionsService;
|
private IDiscussionsService discussionsService;
|
||||||
|
|
||||||
@PostMapping("/discussion")
|
@PostMapping("/discussion")
|
||||||
@ApiOperation("新增主题")
|
@Operation(summary = "新增主题")
|
||||||
public Discussion save(@RequestBody Discussion discussion) {
|
public Discussion save(@RequestBody Discussion discussion) {
|
||||||
Discussion saveDiscussion = discussionsService.insert(discussion);
|
Discussion saveDiscussion = discussionsService.insert(discussion);
|
||||||
if (saveDiscussion != null) {
|
if (saveDiscussion != null) {
|
||||||
@@ -39,18 +40,16 @@ public class RestPostController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/discussion/{id}")
|
@GetMapping("/discussion/{id}")
|
||||||
@ApiOperation("获取主题")
|
@Operation(summary = "获取主题")
|
||||||
public Discussion getDiscussions(@PathVariable Integer id) {
|
public Discussion getDiscussions(@PathVariable Integer id) {
|
||||||
Discussion discussion = discussionsService.findOne(id);
|
return discussionsService.findOne(id);
|
||||||
return discussion;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/discussions")
|
@GetMapping("/discussions")
|
||||||
@ApiOperation("获取全部主题")
|
@Operation(summary = "获取全部主题")
|
||||||
public List<Discussion> getAllDiscussions() {
|
public List<Discussion> getAllDiscussions() {
|
||||||
List<Discussion> allDiscussions = discussionsService.getAllList();
|
List<Discussion> allDiscussions = discussionsService.getAllList();
|
||||||
logger.info("全部主题==>:{}", allDiscussions);
|
logger.info("全部主题==>:{}", allDiscussions);
|
||||||
return allDiscussions;
|
return allDiscussions;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import com.yaoyuan.jiscuss.entity.User;
|
|||||||
import com.yaoyuan.jiscuss.exception.BaseException;
|
import com.yaoyuan.jiscuss.exception.BaseException;
|
||||||
import com.yaoyuan.jiscuss.response.ResponseCode;
|
import com.yaoyuan.jiscuss.response.ResponseCode;
|
||||||
import com.yaoyuan.jiscuss.service.IUsersService;
|
import com.yaoyuan.jiscuss.service.IUsersService;
|
||||||
import io.swagger.annotations.Api;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.annotations.ApiOperation;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import org.json.JSONObject;
|
|
||||||
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;
|
||||||
@@ -23,16 +22,16 @@ import java.util.List;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/user_api")
|
@RequestMapping("/user_api")
|
||||||
@Api(value = "用户接口管理", tags = {"用户接口管理"})
|
@Tag(name = "用户接口管理", description = "用户 CRUD API")
|
||||||
public class RestUserController {
|
public class RestUserController {
|
||||||
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(RestUserController.class);
|
private static final Logger logger = LoggerFactory.getLogger(RestUserController.class);
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IUsersService usersService;
|
private IUsersService usersService;
|
||||||
|
|
||||||
@PostMapping("/user")
|
@PostMapping("/user")
|
||||||
@ApiOperation("新增用户")
|
@Operation(summary = "新增用户")
|
||||||
public User save(@RequestBody User user) {
|
public User save(@RequestBody User user) {
|
||||||
User saveUser = usersService.insert(user);
|
User saveUser = usersService.insert(user);
|
||||||
if (saveUser != null) {
|
if (saveUser != null) {
|
||||||
@@ -40,23 +39,17 @@ public class RestUserController {
|
|||||||
} else {
|
} else {
|
||||||
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/user/{id}")
|
@DeleteMapping("/user/{id}")
|
||||||
@ApiOperation("删除用户")
|
@Operation(summary = "删除用户")
|
||||||
public Boolean delete(@PathVariable Integer id) {
|
public Boolean delete(@PathVariable Integer id) {
|
||||||
Boolean delete = true;
|
|
||||||
usersService.remove(id);
|
usersService.remove(id);
|
||||||
if (delete) {
|
return true;
|
||||||
return delete;
|
|
||||||
} else {
|
|
||||||
throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/user/{id}")
|
@PutMapping("/user/{id}")
|
||||||
@ApiOperation("修改用户")
|
@Operation(summary = "修改用户")
|
||||||
public User update(@RequestBody User user, @PathVariable Integer id) {
|
public User update(@RequestBody User user, @PathVariable Integer id) {
|
||||||
User updateuser = usersService.update(user, id);
|
User updateuser = usersService.update(user, id);
|
||||||
if (updateuser != null) {
|
if (updateuser != null) {
|
||||||
@@ -67,19 +60,19 @@ public class RestUserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/user/{id}")
|
@GetMapping("/user/{id}")
|
||||||
@ApiOperation("获取用户")
|
@Operation(summary = "获取用户")
|
||||||
public User getUser(@PathVariable Integer id) {
|
public User getUser(@PathVariable Integer id) {
|
||||||
User user = usersService.findOne(id);
|
User user = usersService.findOne(id);
|
||||||
logger.info("获取用户==>:{}", JSONObject.valueToString(user));
|
logger.info("获取用户==>:{}", user);
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/user")
|
@GetMapping("/user")
|
||||||
@ApiOperation("获取全部用户")
|
@Operation(summary = "获取全部用户")
|
||||||
public List<User> getUser(User user) {
|
public List<User> getAllUsers() {
|
||||||
List<User> userall = usersService.getAllList();
|
return usersService.getAllList();
|
||||||
logger.info("全部用户==>:{}", JSONObject.valueToString(userall));
|
|
||||||
return userall;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.yaoyuan.jiscuss.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request payload for creating a new user account.
|
||||||
|
* Validated at the controller boundary before reaching the service layer.
|
||||||
|
*/
|
||||||
|
public record UserCreateRequest(
|
||||||
|
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
@Size(min = 3, max = 50, message = "用户名长度必须在 3-50 个字符之间")
|
||||||
|
String username,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw password — will be BCrypt-hashed before persistence.
|
||||||
|
* Never log or return this field.
|
||||||
|
*/
|
||||||
|
@NotBlank(message = "密码不能为空")
|
||||||
|
@Size(min = 8, max = 100, message = "密码长度至少 8 个字符")
|
||||||
|
String password,
|
||||||
|
|
||||||
|
@Email(message = "邮箱格式不正确")
|
||||||
|
@Size(max = 100)
|
||||||
|
String email,
|
||||||
|
|
||||||
|
@Size(max = 50)
|
||||||
|
String realname
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.yaoyuan.jiscuss.dto;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.entity.User;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manual mapper between {@link User} entity and DTO objects.
|
||||||
|
*
|
||||||
|
* <p>Intentionally excludes the {@code password} field from all output DTOs.
|
||||||
|
* Replace with a generated MapStruct mapper if mapping complexity grows.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class UserMapper {
|
||||||
|
|
||||||
|
/** Map a {@link User} entity to a safe {@link UserResponse} (no password). */
|
||||||
|
public UserResponse toResponse(User user) {
|
||||||
|
if (user == null) return null;
|
||||||
|
return new UserResponse(
|
||||||
|
user.getId(),
|
||||||
|
user.getUsername(),
|
||||||
|
user.getRealname(),
|
||||||
|
user.getEmail(),
|
||||||
|
user.getAvatar(),
|
||||||
|
user.getGender(),
|
||||||
|
user.getPhone(),
|
||||||
|
user.getAge(),
|
||||||
|
user.getDiscussionsCount(),
|
||||||
|
user.getCommentsCount(),
|
||||||
|
user.getJoinTime(),
|
||||||
|
user.getLastSeenTime(),
|
||||||
|
user.getLevel()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map a {@link UserCreateRequest} to a new {@link User} entity (password left unset — caller must hash it). */
|
||||||
|
public User fromCreateRequest(UserCreateRequest request) {
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(request.username());
|
||||||
|
user.setEmail(request.email());
|
||||||
|
user.setRealname(request.realname());
|
||||||
|
// Caller is responsible for BCrypt-hashing the password before calling usersService.insert()
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.yaoyuan.jiscuss.dto;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only user projection exposed to the frontend / API consumers.
|
||||||
|
* Never exposes the {@code password} field.
|
||||||
|
*/
|
||||||
|
public record UserResponse(
|
||||||
|
Integer id,
|
||||||
|
String username,
|
||||||
|
String realname,
|
||||||
|
String email,
|
||||||
|
String avatar,
|
||||||
|
String gender,
|
||||||
|
String phone,
|
||||||
|
Integer age,
|
||||||
|
Integer discussionsCount,
|
||||||
|
Integer commentsCount,
|
||||||
|
Date joinTime,
|
||||||
|
Date lastSeenTime,
|
||||||
|
Integer level
|
||||||
|
) {}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
package com.yaoyuan.jiscuss.entity;
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.GenerationType;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@@ -22,6 +24,8 @@ public class Discussion implements Serializable {
|
|||||||
@Column(name = "id", unique = true)
|
@Column(name = "id", unique = true)
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(max = 200)
|
||||||
@Column(name = "title")
|
@Column(name = "title")
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@@ -64,7 +68,7 @@ public class Discussion implements Serializable {
|
|||||||
@Column(name = "like_count")
|
@Column(name = "like_count")
|
||||||
private Integer likeCount;
|
private Integer likeCount;
|
||||||
|
|
||||||
|
/** IP is resolved server-side via IpUtils.getClientIp() — never trusted from X-Forwarded-For alone. */
|
||||||
@Column(name = "ip_address")
|
@Column(name = "ip_address")
|
||||||
private String ipAddress;
|
private String ipAddress;
|
||||||
|
|
||||||
@@ -73,5 +77,5 @@ public class Discussion implements Serializable {
|
|||||||
|
|
||||||
@Column(name = "create_time")
|
@Column(name = "create_time")
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.yaoyuan.jiscuss.entity;
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.GenerationType;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
package com.yaoyuan.jiscuss.entity;
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.GenerationType;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author yaoyuan2.chu
|
* @author yaoyuan2.chu
|
||||||
* @Title:
|
|
||||||
* @Package com.yaoyuan.jiscuss.entity
|
|
||||||
* @Description:
|
|
||||||
* @date 2020/10/21 12:00
|
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Entity
|
@Entity
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.yaoyuan.jiscuss.entity;
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.GenerationType;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ public class Post implements Serializable {
|
|||||||
@Column(name = "type")
|
@Column(name = "type")
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
@Column(name = "content")
|
@Column(name = "content")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ public class Post implements Serializable {
|
|||||||
@Column(name = "edit_user_id")
|
@Column(name = "edit_user_id")
|
||||||
private Integer editUserId;
|
private Integer editUserId;
|
||||||
|
|
||||||
|
/** IP resolved server-side via IpUtils.getClientIp(). */
|
||||||
@Column(name = "ip_address")
|
@Column(name = "ip_address")
|
||||||
private String ipAddress;
|
private String ipAddress;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.yaoyuan.jiscuss.entity;
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.GenerationType;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.yaoyuan.jiscuss.entity;
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.GenerationType;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
package com.yaoyuan.jiscuss.entity;
|
package com.yaoyuan.jiscuss.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.validation.constraints.Email;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.Pattern;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
|
||||||
import javax.persistence.Entity;
|
|
||||||
import javax.persistence.GeneratedValue;
|
|
||||||
import javax.persistence.GenerationType;
|
|
||||||
import javax.persistence.Id;
|
|
||||||
import javax.persistence.Table;
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@@ -22,15 +25,21 @@ public class User implements Serializable {
|
|||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Integer id;
|
private Integer id;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
@Size(min = 3, max = 50)
|
||||||
@Column(name = "username")
|
@Column(name = "username")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
@Size(max = 50)
|
||||||
@Column(name = "realname")
|
@Column(name = "realname")
|
||||||
private String realname;
|
private String realname;
|
||||||
|
|
||||||
|
@Email
|
||||||
|
@Size(max = 100)
|
||||||
@Column(name = "email")
|
@Column(name = "email")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
/** BCrypt-hashed password. Column length must be >= 68 (see V2 migration). */
|
||||||
@Column(name = "password")
|
@Column(name = "password")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@@ -40,12 +49,14 @@ public class User implements Serializable {
|
|||||||
@Column(name = "age")
|
@Column(name = "age")
|
||||||
private Integer age;
|
private Integer age;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^(男|女|其他)?$")
|
||||||
@Column(name = "gender")
|
@Column(name = "gender")
|
||||||
private String gender;
|
private String gender;
|
||||||
|
|
||||||
@Column(name = "avatar")
|
@Column(name = "avatar")
|
||||||
private String avatar;
|
private String avatar;
|
||||||
|
|
||||||
|
@Size(max = 20)
|
||||||
@Column(name = "phone")
|
@Column(name = "phone")
|
||||||
private String phone;
|
private String phone;
|
||||||
|
|
||||||
@@ -63,5 +74,4 @@ public class User implements Serializable {
|
|||||||
|
|
||||||
@Column(name = "level")
|
@Column(name = "level")
|
||||||
private Integer level;
|
private Integer level;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ import lombok.Data;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import javax.persistence.Column;
|
import jakarta.persistence.Column;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author yaoyuan2.chu
|
* @author yaoyuan2.chu
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package com.yaoyuan.jiscuss.exception;
|
||||||
|
|
||||||
|
import com.yaoyuan.jiscuss.response.ApiResponse;
|
||||||
|
import com.yaoyuan.jiscuss.response.ResponseCode;
|
||||||
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global exception handler.
|
||||||
|
*
|
||||||
|
* <p>Catches {@link BaseException}, validation errors, and unexpected exceptions so they
|
||||||
|
* are returned as structured JSON instead of Spring's default error page.
|
||||||
|
*/
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
|
||||||
|
|
||||||
|
/** Business logic exceptions (already typed with a ResponseCode). */
|
||||||
|
@ExceptionHandler(BaseException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleBaseException(BaseException ex) {
|
||||||
|
log.warn("Business exception: code={}, msg={}", ex.getCode().getCode(), ex.getCode().getMsg());
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(ApiResponse.fail(ex.getCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Valid / @Validated failures on @RequestBody. */
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException ex) {
|
||||||
|
String message = ex.getBindingResult().getFieldErrors().stream()
|
||||||
|
.map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
log.warn("Validation failed: {}", message);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(ApiResponse.error(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @Validated failures on @RequestParam / @PathVariable. */
|
||||||
|
@ExceptionHandler(ConstraintViolationException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException ex) {
|
||||||
|
String message = ex.getConstraintViolations().stream()
|
||||||
|
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
|
||||||
|
.collect(Collectors.joining("; "));
|
||||||
|
log.warn("Constraint violation: {}", message);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.BAD_REQUEST)
|
||||||
|
.body(ApiResponse.error(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spring Security access-denied exceptions re-thrown by method security.
|
||||||
|
* Note: exceptions raised before method invocation are handled by {@code CustomAccessDeniedHandler}.
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(AccessDeniedException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleAccessDenied(AccessDeniedException ex) {
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.FORBIDDEN)
|
||||||
|
.body(ApiResponse.error("没有权限访问该资源"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Catch-all for any unhandled exception — never expose stack traces to clients. */
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleGeneral(Exception ex) {
|
||||||
|
log.error("Unexpected error", ex);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(ApiResponse.fail(ResponseCode.SERVICE_ERROR));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ import org.springframework.security.web.WebAttributes;
|
|||||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.servlet.RequestDispatcher;
|
import jakarta.servlet.RequestDispatcher;
|
||||||
import javax.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,70 @@
|
|||||||
package com.yaoyuan.jiscuss.handler;
|
package com.yaoyuan.jiscuss.handler;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.AntPathMatcher;
|
import org.springframework.util.AntPathMatcher;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import java.util.Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RBAC数据模型控制权限
|
* RBAC (Role-Based Access Control) permission evaluator.
|
||||||
*
|
*
|
||||||
* @author charlie
|
* <p>Rules:
|
||||||
|
* <ul>
|
||||||
|
* <li>Anonymous / unauthenticated: denied</li>
|
||||||
|
* <li>ROLE_ADMIN: full access (all paths)</li>
|
||||||
|
* <li>ROLE_USER: access to all non-admin paths</li>
|
||||||
|
* <li>Admin-only paths ({@code /admin/**}, {@code /druid/**}, etc.) require ROLE_ADMIN</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Previously this method unconditionally returned {@code true}, giving every user
|
||||||
|
* unrestricted access regardless of role — a critical security vulnerability now fixed.
|
||||||
*/
|
*/
|
||||||
@Component("rbacPermission")
|
@Component("rbacPermission")
|
||||||
public class RbacPermission {
|
public class RbacPermission {
|
||||||
|
|
||||||
private AntPathMatcher antPathMatcher = new AntPathMatcher();
|
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
|
||||||
|
|
||||||
|
/** URL patterns that require ROLE_ADMIN. */
|
||||||
|
private static final String[] ADMIN_ONLY_PATTERNS = {
|
||||||
|
"/admin/**",
|
||||||
|
"/druid/**",
|
||||||
|
"/actuator/**",
|
||||||
|
"/h2-console/**",
|
||||||
|
"/user_api/**",
|
||||||
|
"/other_api/**"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether the authenticated principal is permitted to access the requested URL.
|
||||||
|
*
|
||||||
|
* @param request the current HTTP request
|
||||||
|
* @param authentication the current authentication token
|
||||||
|
* @return {@code true} when access is granted
|
||||||
|
*/
|
||||||
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
|
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
|
||||||
Object principal = authentication.getPrincipal();
|
if (authentication == null
|
||||||
boolean hasPermission = false;
|
|| !authentication.isAuthenticated()
|
||||||
|
|| "anonymousUser".equals(authentication.getPrincipal())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
hasPermission = true;
|
String uri = request.getRequestURI();
|
||||||
// if (principal instanceof UserEntity) {
|
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
|
||||||
// // 读取用户所拥有的权限菜单
|
|
||||||
// List<Menu> menus = ((UserEntity) principal).getRoleMenus();
|
boolean isAdmin = authorities.stream()
|
||||||
// System.out.println(menus.size());
|
.anyMatch(a -> "ROLE_ADMIN".equals(a.getAuthority()));
|
||||||
// for (Menu menu : menus) {
|
|
||||||
// if (antPathMatcher.match(menu.getMenuUrl(), request.getRequestURI())) {
|
// Admin-only paths: only ROLE_ADMIN may proceed
|
||||||
// hasPermission = true;
|
for (String pattern : ADMIN_ONLY_PATTERNS) {
|
||||||
// break;
|
if (PATH_MATCHER.match(pattern, uri)) {
|
||||||
// }
|
return isAdmin;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
return hasPermission;
|
|
||||||
|
// All other paths are accessible by any authenticated user
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,11 +30,6 @@ public interface UsersRepository extends JpaRepository<User, Integer> {
|
|||||||
@Query("from User where username = :username")
|
@Query("from User where username = :username")
|
||||||
User getByUsername(String username);
|
User getByUsername(String username);
|
||||||
|
|
||||||
/**
|
// REMOVED: checkByUsernameAndPassword — never compare passwords at the SQL layer.
|
||||||
* @param username
|
// Password validation must go through BCryptPasswordEncoder.matches() in the service/security layer.
|
||||||
* @param password
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Query("from User where username = :username and password = :password")
|
|
||||||
User checkByUsernameAndPassword(String username, String password);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.yaoyuan.jiscuss.response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified API response wrapper.
|
||||||
|
*
|
||||||
|
* @param <T> payload type
|
||||||
|
*/
|
||||||
|
public record ApiResponse<T>(int code, String msg, T data) {
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> ok(T data) {
|
||||||
|
return new ApiResponse<>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMsg(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> fail(ResponseCode responseCode) {
|
||||||
|
return new ApiResponse<>(responseCode.getCode(), responseCode.getMsg(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(String message) {
|
||||||
|
return new ApiResponse<>(ResponseCode.SERVICE_ERROR.getCode(), message, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,5 @@ public interface IUsersService {
|
|||||||
|
|
||||||
User getByUsername(String username);
|
User getByUsername(String username);
|
||||||
|
|
||||||
User checkByUsernameAndPassword(String username, String password);
|
|
||||||
|
|
||||||
User update(User user, Integer id);
|
User update(User user, Integer id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,54 +10,49 @@ import org.springframework.data.domain.PageRequest;
|
|||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
|
||||||
public class DiscussionsServiceImpl implements IDiscussionsService {
|
public class DiscussionsServiceImpl implements IDiscussionsService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DiscussionsRepository discussionsRepository;
|
private DiscussionsRepository discussionsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Discussion> getAllList() {
|
public List<Discussion> getAllList() {
|
||||||
return discussionsRepository.findAll();
|
return discussionsRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Discussion insert(Discussion discussion) {
|
public Discussion insert(Discussion discussion) {
|
||||||
|
|
||||||
return discussionsRepository.save(discussion);
|
return discussionsRepository.save(discussion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public Discussion findOne(Integer id) {
|
public Discussion findOne(Integer id) {
|
||||||
Discussion discussion = new Discussion();
|
// getReferenceById replaces removed JpaRepository.getOne()
|
||||||
discussion.setId(id);
|
return discussionsRepository.getReferenceById(id);
|
||||||
return discussionsRepository.getOne(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public Page<Discussion> queryAllDiscussionsList(Discussion discussion, int pageNum, int pageSize, String tag, String type) {
|
public Page<Discussion> queryAllDiscussionsList(Discussion discussion, int pageNum, int pageSize, String tag, String type) {
|
||||||
Sort sort = new Sort(Sort.Direction.DESC, "id");
|
Sort sort = switch (type) {
|
||||||
if (type.equals("hot")) {
|
case "hot" -> Sort.by(Sort.Direction.DESC, "likeCount");
|
||||||
sort = new Sort(Sort.Direction.DESC, "likeCount");
|
case "new" -> Sort.by(Sort.Direction.DESC, "startTime");
|
||||||
} else if (type.equals("new")) {
|
default -> Sort.by(Sort.Direction.DESC, "id");
|
||||||
sort = new Sort(Sort.Direction.DESC, "startTime");
|
};
|
||||||
}
|
Pageable pageable = PageRequest.of(pageNum, pageSize, sort);
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
Pageable pageable = new PageRequest(pageNum, pageSize, sort);
|
|
||||||
//将匹配对象封装成Example对象
|
|
||||||
Example<Discussion> example = Example.of(discussion);
|
Example<Discussion> example = Example.of(discussion);
|
||||||
if (null != tag && !"all".equals(tag)) {
|
if (tag != null && !"all".equals(tag)) {
|
||||||
Page<Discussion> pageList = discussionsRepository.findByQuery(tag, pageable);
|
return discussionsRepository.findByQuery(tag, pageable);
|
||||||
return pageList;
|
|
||||||
} else {
|
} else {
|
||||||
Page<Discussion> pageList = discussionsRepository.findAll(example, pageable);
|
return discussionsRepository.findAll(example, pageable);
|
||||||
return pageList;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,21 +6,22 @@ import com.yaoyuan.jiscuss.service.IDiscussionsTagsService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
|
||||||
public class DiscussionsTagsServiceImpl implements IDiscussionsTagsService {
|
public class DiscussionsTagsServiceImpl implements IDiscussionsTagsService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private DiscussionsTagsRepository discussionsTagsRepository;
|
private DiscussionsTagsRepository discussionsTagsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<DiscussionTag> getAllList() {
|
public List<DiscussionTag> getAllList() {
|
||||||
return discussionsTagsRepository.findAll();
|
return discussionsTagsRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public DiscussionTag insert(DiscussionTag discussionTag) {
|
public DiscussionTag insert(DiscussionTag discussionTag) {
|
||||||
return discussionsTagsRepository.save(discussionTag);
|
return discussionsTagsRepository.save(discussionTag);
|
||||||
|
|||||||
@@ -11,30 +11,32 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.data.domain.Example;
|
import org.springframework.data.domain.Example;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
|
||||||
public class PostsServiceImpl implements IPostsService {
|
public class PostsServiceImpl implements IPostsService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private PostsRepository postsRepository;
|
private PostsRepository postsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Post> getAllList() {
|
public List<Post> getAllList() {
|
||||||
return postsRepository.findAll();
|
return postsRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Post> findOneBy(Integer id) {
|
public List<Post> findOneBy(Integer id) {
|
||||||
List<Post> posts = postsRepository.findOneBy(id);
|
List<Post> posts = postsRepository.findOneBy(id);
|
||||||
return posts;
|
return posts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public Post findOneByid(Integer id) {
|
public Post findOneByid(Integer id) {
|
||||||
Post post = new Post();
|
Post post = new Post();
|
||||||
@@ -44,6 +46,7 @@ public class PostsServiceImpl implements IPostsService {
|
|||||||
return postRes.get();
|
return postRes.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List findPostCustomById(Integer id) {
|
public List findPostCustomById(Integer id) {
|
||||||
//查询id为1且parentId为null的评论
|
//查询id为1且parentId为null的评论
|
||||||
@@ -76,6 +79,7 @@ public class PostsServiceImpl implements IPostsService {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Post insert(Post post) {
|
public Post insert(Post post) {
|
||||||
return postsRepository.save(post);
|
return postsRepository.save(post);
|
||||||
|
|||||||
@@ -6,20 +6,21 @@ import com.yaoyuan.jiscuss.service.ISettingsService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
|
||||||
public class SettingsServiceImpl implements ISettingsService {
|
public class SettingsServiceImpl implements ISettingsService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private SettingsRepository settingsRepository;
|
private SettingsRepository settingsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Setting> getAllList() {
|
public List<Setting> getAllList() {
|
||||||
return settingsRepository.findAll();
|
return settingsRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Setting insert(Setting setting) {
|
public Setting insert(Setting setting) {
|
||||||
return settingsRepository.save(setting);
|
return settingsRepository.save(setting);
|
||||||
|
|||||||
@@ -7,40 +7,45 @@ import com.yaoyuan.jiscuss.service.ITagsService;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
|
||||||
public class TagsServiceImpl implements ITagsService {
|
public class TagsServiceImpl implements ITagsService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private TagsRepository tagsRepository;
|
private TagsRepository tagsRepository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Tag> getAllList() {
|
public List<Tag> getAllList() {
|
||||||
return tagsRepository.findAllIsNull();
|
return tagsRepository.findAllIsNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Tag> getAllListDiscussions() {
|
public List<Tag> getAllListDiscussions() {
|
||||||
return tagsRepository.findAll();
|
return tagsRepository.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public Tag insert(Tag tag) {
|
public Tag insert(Tag tag) {
|
||||||
return tagsRepository.save(tag);
|
return tagsRepository.save(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Tag> findByDId(Integer id) {
|
public List<Tag> findByDId(Integer id) {
|
||||||
return tagsRepository.findByDId(id);
|
return tagsRepository.findByDId(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<TagCustom> findByDiscussionIdlistId(List<Integer> discussionIdLsit) {
|
public List<TagCustom> findByDiscussionIdlistId(List<Integer> discussionIdLsit) {
|
||||||
return tagsRepository.findByDiscussionIdlistId(discussionIdLsit);
|
return tagsRepository.findByDiscussionIdlistId(discussionIdLsit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<Tag> findByParentId(String tag) {
|
public List<Tag> findByParentId(String tag) {
|
||||||
int tagId = Integer.parseInt(tag);
|
int tagId = Integer.parseInt(tag);
|
||||||
|
|||||||
@@ -8,51 +8,44 @@ import org.springframework.security.core.GrantedAuthority;
|
|||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author yaoyuan2.chu
|
* Spring Security {@code UserDetailsService} implementation.
|
||||||
* @Title:
|
*
|
||||||
* @Package com.yaoyuan.jiscuss.service.impl
|
* <p>Role assignment: users with {@code level >= 1} receive {@code ROLE_ADMIN};
|
||||||
* @Description:
|
* all others receive {@code ROLE_USER}.
|
||||||
* @date 2020/7/15 16:34
|
*
|
||||||
|
* <p>Password note: passwords stored in the database MUST be BCrypt-hashed.
|
||||||
|
* See Flyway migration {@code V2__security_updates.sql} for the one-time migration
|
||||||
|
* that converts legacy plaintext passwords to BCrypt hashes and expands the column.
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class UserDetailServiceImpl implements UserDetailsService {
|
public class UserDetailServiceImpl implements UserDetailsService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IUsersService userInfoService;
|
private IUsersService userInfoService;
|
||||||
|
|
||||||
/**
|
|
||||||
* 需新建配置类注册一个指定的加密方式Bean,或在下一步Security配置类中注册指定
|
|
||||||
*/
|
|
||||||
@Autowired
|
|
||||||
private PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserInfo loadUserByUsername(String username) throws UsernameNotFoundException {
|
public UserInfo loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
// 通过用户名从数据库获取用户信息
|
|
||||||
User userInfo = userInfoService.getByUsername(username);
|
User userInfo = userInfoService.getByUsername(username);
|
||||||
if (userInfo == null) {
|
if (userInfo == null) {
|
||||||
throw new UsernameNotFoundException("用户不存在");
|
throw new UsernameNotFoundException("用户不存在: " + username);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 得到用户角色
|
// Assign role based on user level: level >= 1 → ADMIN, else → USER
|
||||||
String role = "admin";
|
String role = (userInfo.getLevel() != null && userInfo.getLevel() >= 1) ? "ADMIN" : "USER";
|
||||||
|
|
||||||
// 角色集合
|
|
||||||
List<GrantedAuthority> authorities = new ArrayList<>();
|
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||||
// 角色必须以`ROLE_`开头,数据库中没有,则在这里加
|
|
||||||
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
|
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
|
||||||
|
|
||||||
return new UserInfo(
|
return new UserInfo(
|
||||||
authorities,
|
authorities,
|
||||||
userInfo.getId(),
|
userInfo.getId(),
|
||||||
// 因为数据库是明文,所以这里需加密密码
|
userInfo.getPassword(), // Must be a BCrypt hash (see V2 migration)
|
||||||
passwordEncoder.encode(userInfo.getPassword()),
|
|
||||||
userInfo.getUsername(),
|
userInfo.getUsername(),
|
||||||
userInfo.getPhone()
|
userInfo.getPhone()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,17 +7,17 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.cache.annotation.CacheEvict;
|
import org.springframework.cache.annotation.CacheEvict;
|
||||||
import org.springframework.cache.annotation.CachePut;
|
import org.springframework.cache.annotation.CachePut;
|
||||||
import org.springframework.cache.annotation.Cacheable;
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.cache.annotation.Caching;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.PageRequest;
|
import org.springframework.data.domain.PageRequest;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.domain.Sort;
|
import org.springframework.data.domain.Sort;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.transaction.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional
|
|
||||||
public class UsersServiceImpl implements IUsersService {
|
public class UsersServiceImpl implements IUsersService {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -28,7 +28,8 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@Cacheable(value = "user")
|
@Transactional(readOnly = true)
|
||||||
|
@Cacheable(value = "userList")
|
||||||
@Override
|
@Override
|
||||||
public List<User> getAllList() {
|
public List<User> getAllList() {
|
||||||
return usersRepository.findAll();
|
return usersRepository.findAll();
|
||||||
@@ -41,11 +42,10 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
* @param pageSize
|
* @param pageSize
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public Page<User> queryAllUsersList(int pageNum, int pageSize) {
|
public Page<User> queryAllUsersList(int pageNum, int pageSize) {
|
||||||
Sort sort = new Sort(Sort.Direction.DESC, "id");
|
Pageable pageable = PageRequest.of(pageNum, pageSize, Sort.by(Sort.Direction.DESC, "id"));
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
Pageable pageable = new PageRequest(pageNum, pageSize, sort);
|
|
||||||
return usersRepository.findAll(pageable);
|
return usersRepository.findAll(pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
* @param name
|
* @param name
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public List<User> getByUsernameIsLike(String name) {
|
public List<User> getByUsernameIsLike(String name) {
|
||||||
return usersRepository.getByUsernameIsLike(name);
|
return usersRepository.getByUsernameIsLike(name);
|
||||||
@@ -66,6 +67,7 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
* @param id
|
* @param id
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Cacheable(value = "user", key = "#id")
|
@Cacheable(value = "user", key = "#id")
|
||||||
@Override
|
@Override
|
||||||
public User findOne(Integer id) {
|
public User findOne(Integer id) {
|
||||||
@@ -78,7 +80,9 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
* @param user
|
* @param user
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
@CachePut(value = "user", key = "#user.id")
|
@CachePut(value = "user", key = "#user.id")
|
||||||
|
@CacheEvict(value = "userList", allEntries = true)
|
||||||
@Override
|
@Override
|
||||||
public User insert(User user) {
|
public User insert(User user) {
|
||||||
return usersRepository.save(user);
|
return usersRepository.save(user);
|
||||||
@@ -91,7 +95,9 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
* @param id
|
* @param id
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
@CachePut(value = "user", key = "#user.id")
|
@CachePut(value = "user", key = "#user.id")
|
||||||
|
@CacheEvict(value = "userList", allEntries = true)
|
||||||
@Override
|
@Override
|
||||||
public User update(User user, Integer id) {
|
public User update(User user, Integer id) {
|
||||||
return usersRepository.saveAndFlush(user);
|
return usersRepository.saveAndFlush(user);
|
||||||
@@ -102,7 +108,11 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
*
|
*
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
@CacheEvict(value = "user", key = "#id")
|
@Transactional
|
||||||
|
@Caching(evict = {
|
||||||
|
@CacheEvict(value = "user", key = "#id"),
|
||||||
|
@CacheEvict(value = "userList", allEntries = true)
|
||||||
|
})
|
||||||
@Override
|
@Override
|
||||||
public void remove(Integer id) {
|
public void remove(Integer id) {
|
||||||
usersRepository.deleteById(id);
|
usersRepository.deleteById(id);
|
||||||
@@ -112,6 +122,7 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
* 删除所有
|
* 删除所有
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
@Override
|
@Override
|
||||||
public void deleteAll() {
|
public void deleteAll() {
|
||||||
usersRepository.deleteAll();
|
usersRepository.deleteAll();
|
||||||
@@ -123,23 +134,12 @@ public class UsersServiceImpl implements IUsersService {
|
|||||||
* @param username
|
* @param username
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Override
|
@Override
|
||||||
public User getByUsername(String username) {
|
public User getByUsername(String username) {
|
||||||
return usersRepository.getByUsername(username);
|
return usersRepository.getByUsername(username);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 验证用户名密码
|
|
||||||
*
|
|
||||||
* @param username
|
|
||||||
* @param password
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public User checkByUsernameAndPassword(String username, String password) {
|
|
||||||
return usersRepository.checkByUsernameAndPassword(username, password);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.yaoyuan.jiscuss.util;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secure IP address extraction utility.
|
||||||
|
*
|
||||||
|
* <p>Defends against IP spoofing via forged {@code X-Forwarded-For} headers by:
|
||||||
|
* <ul>
|
||||||
|
* <li>Treating the first non-internal IP in the proxy chain as the client IP</li>
|
||||||
|
* <li>Falling back to {@code HttpServletRequest.getRemoteAddr()} when no trusted
|
||||||
|
* proxy header is present</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><strong>Important:</strong> Only trust forwarded headers when the application
|
||||||
|
* runs behind a known, trusted reverse proxy. Consider configuring
|
||||||
|
* {@code server.forward-headers-strategy=NATIVE} or {@code FRAMEWORK} in
|
||||||
|
* {@code application.yml} to let Spring handle this automatically.
|
||||||
|
*/
|
||||||
|
public final class IpUtils {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(IpUtils.class);
|
||||||
|
|
||||||
|
private static final String UNKNOWN = "unknown";
|
||||||
|
private static final int MAX_IP_LENGTH = 15; // IPv4
|
||||||
|
|
||||||
|
private IpUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the best-effort client IP address from the request.
|
||||||
|
*
|
||||||
|
* @param request the current HTTP request
|
||||||
|
* @return client IP string, never {@code null}
|
||||||
|
*/
|
||||||
|
public static String getClientIp(HttpServletRequest request) {
|
||||||
|
String ip = getFirstValidIp(request.getHeader("X-Forwarded-For"));
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getHeader("X-Real-IP");
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getHeader("Proxy-Client-IP");
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||||
|
if (isValid(ip)) return sanitize(ip);
|
||||||
|
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
return ip != null ? sanitize(ip) : UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the first entry from a comma-separated {@code X-Forwarded-For} chain.
|
||||||
|
* Returns {@code null} if the header is absent/empty/unknown.
|
||||||
|
*/
|
||||||
|
private static String getFirstValidIp(String header) {
|
||||||
|
if (header == null || header.isBlank() || UNKNOWN.equalsIgnoreCase(header)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// X-Forwarded-For: client, proxy1, proxy2 — take the leftmost (client) entry
|
||||||
|
String[] parts = header.split(",");
|
||||||
|
return parts[0].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isValid(String ip) {
|
||||||
|
return ip != null && !ip.isBlank() && !UNKNOWN.equalsIgnoreCase(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Truncate excessively long values to prevent log injection / storage attacks. */
|
||||||
|
private static String sanitize(String ip) {
|
||||||
|
// Strip any characters that are not valid in IPv4/IPv6 addresses
|
||||||
|
String cleaned = ip.replaceAll("[^0-9a-fA-F:.\\[\\]]", "");
|
||||||
|
if (cleaned.length() > 45) { // max IPv6 length
|
||||||
|
log.warn("Suspiciously long IP value truncated: {}", ip);
|
||||||
|
return cleaned.substring(0, 45);
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,76 +1,62 @@
|
|||||||
#h2 配置
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# H2 / development profile.
|
||||||
|
# Activate with: --spring.profiles.active=h2
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
spring:
|
spring:
|
||||||
cache:
|
|
||||||
type: ehcache
|
|
||||||
ehcache:
|
|
||||||
config: classpath:ehcache.xml
|
|
||||||
jpa:
|
jpa:
|
||||||
generate-ddl: false
|
|
||||||
show-sql: true
|
show-sql: true
|
||||||
|
# Flyway owns DDL; Hibernate only validates existing schema
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
ddl-auto: validate
|
||||||
|
# H2 uses the same SQL dialect as MySQL in MySQL compatibility mode
|
||||||
|
database-platform: org.hibernate.dialect.H2Dialect
|
||||||
|
|
||||||
|
# H2 console — enabled in dev ONLY.
|
||||||
|
# Access restricted to ROLE_ADMIN via Spring Security (see WebSecurityConfig).
|
||||||
|
# web-allow-others=false prevents remote access; rely on security for protection-in-depth.
|
||||||
h2:
|
h2:
|
||||||
console:
|
console:
|
||||||
path: /h2-console
|
path: /h2-console
|
||||||
enabled: true
|
# Default OFF — enable explicitly at startup: --spring.h2.console.enabled=true
|
||||||
|
enabled: false
|
||||||
settings:
|
settings:
|
||||||
web-allow-others: true
|
web-allow-others: false
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
platform: h2
|
url: jdbc:h2:~/testjiscuss;MODE=MySQL;NON_KEYWORDS=VALUE
|
||||||
url: jdbc:h2:~/testjiscuss
|
|
||||||
username: sa
|
username: sa
|
||||||
password:
|
password:
|
||||||
schema: classpath:schema.sql
|
|
||||||
data: classpath:data.sql
|
|
||||||
driver-class-name: org.h2.Driver
|
driver-class-name: org.h2.Driver
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
druid:
|
druid:
|
||||||
min-idle: 2
|
min-idle: 1
|
||||||
initial-size: 5
|
initial-size: 2
|
||||||
max-active: 10
|
max-active: 5
|
||||||
max-wait: 5000
|
max-wait: 3000
|
||||||
validation-query: select 1SS
|
validation-query: SELECT 1
|
||||||
# 状态监控
|
|
||||||
filter:
|
filter:
|
||||||
stat:
|
stat:
|
||||||
enabled: true
|
enabled: true
|
||||||
db-type: h2
|
db-type: h2
|
||||||
log-slow-sql: true
|
log-slow-sql: true
|
||||||
slow-sql-millis: 2000
|
slow-sql-millis: 1000
|
||||||
# 监控过滤器
|
|
||||||
web-stat-filter:
|
web-stat-filter:
|
||||||
enabled: true
|
enabled: true
|
||||||
exclusions:
|
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
|
||||||
- "*.js"
|
|
||||||
- "*.gif"
|
|
||||||
- "*.jpg"
|
|
||||||
- "*.png"
|
|
||||||
- "*.css"
|
|
||||||
- "*.ico"
|
|
||||||
- "/druid/*"
|
|
||||||
# druid 监控页面
|
|
||||||
stat-view-servlet:
|
stat-view-servlet:
|
||||||
enabled: true
|
enabled: true
|
||||||
url-pattern: /druid/*
|
url-pattern: /druid/*
|
||||||
# reset-enable: false
|
# Druid's own login — change before deploying to any shared environment
|
||||||
# login-username: root
|
reset-enable: false
|
||||||
# login-password: root
|
login-username: admin
|
||||||
freemarker:
|
login-password: changeme_dev
|
||||||
# 设置模板后缀名
|
allow: 127.0.0.1
|
||||||
suffix: .ftl
|
|
||||||
# 设置文档类型
|
# Flyway: run V1 + V2 migrations on H2 in-memory DB on startup
|
||||||
content-type: text/html
|
flyway:
|
||||||
# 设置页面编码格式
|
locations: classpath:db/migration
|
||||||
charset: UTF-8
|
url: ${spring.datasource.url}
|
||||||
# 设置页面缓存
|
user: ${spring.datasource.username}
|
||||||
cache: false
|
password: ${spring.datasource.password}
|
||||||
# 设置ftl文件路径
|
|
||||||
template-loader-path:
|
|
||||||
- classpath:/templates
|
|
||||||
settings:
|
|
||||||
classic_compatible: true
|
|
||||||
# 设置静态文件路径,js,css等
|
|
||||||
mvc:
|
|
||||||
static-path-pattern: /static/**
|
|
||||||
server:
|
|
||||||
port: 80
|
port: 80
|
||||||
@@ -1,53 +1,60 @@
|
|||||||
#mysql 配置
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# MySQL / production profile.
|
||||||
|
# Activate with: --spring.profiles.active=mysql
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
spring:
|
spring:
|
||||||
cache:
|
|
||||||
type: ehcache
|
|
||||||
ehcache:
|
|
||||||
config: classpath:ehcache.xml
|
|
||||||
jpa:
|
jpa:
|
||||||
database: MYSQL
|
database: MYSQL
|
||||||
show-sql: true
|
show-sql: false
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: update
|
# Flyway owns all DDL; Hibernate only validates
|
||||||
|
ddl-auto: validate
|
||||||
|
database-platform: org.hibernate.dialect.MySQLDialect
|
||||||
|
|
||||||
|
# H2 console is OFF in production
|
||||||
|
h2:
|
||||||
|
console:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://127.0.0.1:3306/jiscuss?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&useSSL=false
|
# Updated URL for MySQL 8: removed deprecated useSSL=false; use requireSSL for prod
|
||||||
|
url: jdbc:mysql://127.0.0.1:3306/jiscuss?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&serverTimezone=Asia/Shanghai
|
||||||
username: root
|
username: root
|
||||||
password: 123456
|
password: changeme_production
|
||||||
driver-class-name: com.mysql.jdbc.Driver
|
# Updated driver class name for MySQL Connector/J 8+
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
type: com.alibaba.druid.pool.DruidDataSource
|
||||||
tomcat:
|
|
||||||
init-s-q-l: SET NAMES utf8mb4
|
|
||||||
druid:
|
druid:
|
||||||
min-idle: 2
|
min-idle: 5
|
||||||
initial-size: 5
|
initial-size: 10
|
||||||
max-active: 10
|
max-active: 50
|
||||||
max-wait: 5000
|
max-wait: 5000
|
||||||
validation-query: select 1SS
|
validation-query: SELECT 1
|
||||||
# 状态监控
|
|
||||||
filter:
|
filter:
|
||||||
stat:
|
stat:
|
||||||
enabled: true
|
enabled: true
|
||||||
db-type: mysql
|
db-type: mysql
|
||||||
log-slow-sql: true
|
log-slow-sql: true
|
||||||
slow-sql-millis: 2000
|
slow-sql-millis: 2000
|
||||||
# 监控过滤器
|
|
||||||
web-stat-filter:
|
web-stat-filter:
|
||||||
enabled: true
|
enabled: true
|
||||||
exclusions:
|
exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
|
||||||
- "*.js"
|
|
||||||
- "*.gif"
|
|
||||||
- "*.jpg"
|
|
||||||
- "*.png"
|
|
||||||
- "*.css"
|
|
||||||
- "*.ico"
|
|
||||||
- "/druid/*"
|
|
||||||
# druid 监控页面
|
|
||||||
stat-view-servlet:
|
stat-view-servlet:
|
||||||
enabled: true
|
enabled: true
|
||||||
url-pattern: /druid/*
|
url-pattern: /druid/*
|
||||||
# reset-enable: false
|
reset-enable: false
|
||||||
# login-username: root
|
# Change these credentials — Druid UI is also restricted to ROLE_ADMIN in Spring Security
|
||||||
# login-password: root
|
login-username: admin
|
||||||
|
login-password: changeme_production
|
||||||
|
allow: 127.0.0.1
|
||||||
|
|
||||||
|
# Flyway for MySQL
|
||||||
|
flyway:
|
||||||
|
locations: classpath:db/migration
|
||||||
|
url: ${spring.datasource.url}
|
||||||
|
user: ${spring.datasource.username}
|
||||||
|
password: ${spring.datasource.password}
|
||||||
|
|
||||||
freemarker:
|
freemarker:
|
||||||
# 设置模板后缀名
|
# 设置模板后缀名
|
||||||
suffix: .ftl
|
suffix: .ftl
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Activate h2 (dev) profile by default.
|
||||||
|
# Override via --spring.profiles.active=mysql for production.
|
||||||
|
spring.profiles.active=h2
|
||||||
@@ -1,76 +1,65 @@
|
|||||||
#h2 配置
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Default configuration — shared across all profiles.
|
||||||
|
# Profile-specific overrides live in application-h2.yml and application-mysql.yml.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
spring:
|
spring:
|
||||||
|
# Ehcache 3 via JCache (JSR-107) — replaces Ehcache 2 (net.sf.ehcache)
|
||||||
cache:
|
cache:
|
||||||
type: ehcache
|
type: jcache
|
||||||
ehcache:
|
jcache:
|
||||||
config: classpath:ehcache.xml
|
config: classpath:ehcache3.xml
|
||||||
|
|
||||||
jpa:
|
jpa:
|
||||||
generate-ddl: false
|
generate-ddl: false
|
||||||
show-sql: true
|
show-sql: false
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: none
|
# ddl-auto=validate lets Hibernate check that the schema matches entities
|
||||||
h2:
|
# without modifying anything; Flyway owns all DDL.
|
||||||
console:
|
ddl-auto: validate
|
||||||
path: /h2-console
|
|
||||||
enabled: true
|
|
||||||
settings:
|
|
||||||
web-allow-others: true
|
|
||||||
datasource:
|
|
||||||
platform: h2
|
|
||||||
url: jdbc:h2:~/testjiscuss
|
|
||||||
username: sa
|
|
||||||
password:
|
|
||||||
schema: classpath:schema.sql
|
|
||||||
data: classpath:data.sql
|
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
type: com.alibaba.druid.pool.DruidDataSource
|
|
||||||
druid:
|
|
||||||
min-idle: 2
|
|
||||||
initial-size: 5
|
|
||||||
max-active: 10
|
|
||||||
max-wait: 5000
|
|
||||||
validation-query: select 1SS
|
|
||||||
# 状态监控
|
|
||||||
filter:
|
|
||||||
stat:
|
|
||||||
enabled: true
|
|
||||||
db-type: h2
|
|
||||||
log-slow-sql: true
|
|
||||||
slow-sql-millis: 2000
|
|
||||||
# 监控过滤器
|
|
||||||
web-stat-filter:
|
|
||||||
enabled: true
|
|
||||||
exclusions:
|
|
||||||
- "*.js"
|
|
||||||
- "*.gif"
|
|
||||||
- "*.jpg"
|
|
||||||
- "*.png"
|
|
||||||
- "*.css"
|
|
||||||
- "*.ico"
|
|
||||||
- "/druid/*"
|
|
||||||
# druid 监控页面
|
|
||||||
stat-view-servlet:
|
|
||||||
enabled: true
|
|
||||||
url-pattern: /druid/*
|
|
||||||
# reset-enable: false
|
|
||||||
# login-username: root
|
|
||||||
# login-password: root
|
|
||||||
freemarker:
|
freemarker:
|
||||||
# 设置模板后缀名
|
|
||||||
suffix: .ftl
|
suffix: .ftl
|
||||||
# 设置文档类型
|
|
||||||
content-type: text/html
|
content-type: text/html
|
||||||
# 设置页面编码格式
|
|
||||||
charset: UTF-8
|
charset: UTF-8
|
||||||
# 设置页面缓存
|
|
||||||
cache: false
|
cache: false
|
||||||
# 设置ftl文件路径
|
|
||||||
template-loader-path:
|
template-loader-path:
|
||||||
- classpath:/templates
|
- classpath:/templates
|
||||||
settings:
|
settings:
|
||||||
classic_compatible: true
|
classic_compatible: true
|
||||||
# 设置静态文件路径,js,css等
|
|
||||||
mvc:
|
mvc:
|
||||||
static-path-pattern: /static/**
|
static-path-pattern: /static/**
|
||||||
|
|
||||||
|
# Disable legacy spring.datasource.schema / spring.datasource.data initialisation;
|
||||||
|
# Flyway handles all schema setup in db/migration/.
|
||||||
|
sql:
|
||||||
|
init:
|
||||||
|
mode: never
|
||||||
|
|
||||||
|
# Expose Flyway information via Actuator (read-only)
|
||||||
|
flyway:
|
||||||
|
enabled: true
|
||||||
|
baseline-on-migrate: true # safe for existing databases without flyway_schema_history
|
||||||
|
|
||||||
|
# Actuator: expose health + info publicly; restrict all other endpoints to ROLE_ADMIN
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,info,metrics,flyway
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: when-authorized
|
||||||
|
|
||||||
|
# Forward header strategy — set to NATIVE when running behind a trusted reverse proxy
|
||||||
|
# so Spring uses X-Forwarded-* headers for request URL/IP resolution.
|
||||||
server:
|
server:
|
||||||
port: 80
|
port: 80
|
||||||
|
forward-headers-strategy: NONE # change to NATIVE behind a trusted proxy
|
||||||
|
|
||||||
|
# SpringDoc OpenAPI — UI at /swagger-ui/index.html
|
||||||
|
springdoc:
|
||||||
|
swagger-ui:
|
||||||
|
path: /swagger-ui.html
|
||||||
|
api-docs:
|
||||||
|
path: /v3/api-docs
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- V1: Initial schema — mirrors the existing schema.sql / data.sql.
|
||||||
|
-- Managed by Flyway; do NOT modify after first deployment.
|
||||||
|
-- Subsequent schema changes belong in V2, V3, ...
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- User table
|
||||||
|
CREATE TABLE IF NOT EXISTS user
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
username VARCHAR(50),
|
||||||
|
realname VARCHAR(50),
|
||||||
|
email VARCHAR(100),
|
||||||
|
password VARCHAR(255), -- must hold BCrypt hashes (>=68 chars); V2 migrates existing data
|
||||||
|
join_time DATETIME,
|
||||||
|
age INTEGER,
|
||||||
|
avatar TEXT,
|
||||||
|
gender CHAR(10),
|
||||||
|
phone VARCHAR(20),
|
||||||
|
discussions_count INTEGER,
|
||||||
|
comments_count INTEGER,
|
||||||
|
last_seen_time DATETIME,
|
||||||
|
flag INTEGER,
|
||||||
|
level INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Discussion (thread) table
|
||||||
|
CREATE TABLE IF NOT EXISTS discussion
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
title VARCHAR(200),
|
||||||
|
content TEXT,
|
||||||
|
comments_count INTEGER,
|
||||||
|
participants_count INTEGER,
|
||||||
|
number_index INTEGER,
|
||||||
|
start_time DATETIME,
|
||||||
|
start_user_id INTEGER,
|
||||||
|
start_post_id INTEGER,
|
||||||
|
last_time DATETIME,
|
||||||
|
last_user_id INTEGER,
|
||||||
|
last_post_id INTEGER,
|
||||||
|
last_post_number INTEGER,
|
||||||
|
is_approved INTEGER,
|
||||||
|
like_count INTEGER,
|
||||||
|
ip_address VARCHAR(200),
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Discussion ↔ Tag association table
|
||||||
|
CREATE TABLE IF NOT EXISTS discussiontag
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
discussion_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Post (reply/comment) table
|
||||||
|
CREATE TABLE IF NOT EXISTS post
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
discussion_id INTEGER,
|
||||||
|
number INTEGER,
|
||||||
|
time DATETIME,
|
||||||
|
user_id INTEGER,
|
||||||
|
type VARCHAR(20),
|
||||||
|
content TEXT,
|
||||||
|
edit_time DATETIME,
|
||||||
|
edit_user_id INTEGER,
|
||||||
|
parent_id INTEGER,
|
||||||
|
ip_address VARCHAR(200),
|
||||||
|
copyright VARCHAR(200),
|
||||||
|
is_approved INTEGER,
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Settings table
|
||||||
|
CREATE TABLE IF NOT EXISTS setting
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
setting_key VARCHAR(50),
|
||||||
|
setting_value TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tag table
|
||||||
|
CREATE TABLE IF NOT EXISTS tag
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(200),
|
||||||
|
description VARCHAR(200),
|
||||||
|
color VARCHAR(200),
|
||||||
|
icon VARCHAR(200),
|
||||||
|
position INTEGER,
|
||||||
|
parent_id INTEGER,
|
||||||
|
discussions_count TEXT,
|
||||||
|
last_time DATETIME,
|
||||||
|
last_discussion_id INTEGER,
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Like / Collect table
|
||||||
|
CREATE TABLE IF NOT EXISTS likecollect
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
discussion_id INTEGER,
|
||||||
|
discussion_name VARCHAR(200),
|
||||||
|
tag_id INTEGER,
|
||||||
|
post_id INTEGER,
|
||||||
|
post_content TEXT,
|
||||||
|
user_id INTEGER,
|
||||||
|
user_name VARCHAR(200),
|
||||||
|
type VARCHAR(20),
|
||||||
|
like_type VARCHAR(20),
|
||||||
|
collect_type VARCHAR(20),
|
||||||
|
create_id INTEGER,
|
||||||
|
create_time DATETIME
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ─── Seed data (dev/test) ────────────────────────────────────────────────────
|
||||||
|
-- Passwords below are BCrypt hashes of '123456'.
|
||||||
|
-- Generated with: BCrypt.encode("123456")
|
||||||
|
-- DO NOT use these credentials in production.
|
||||||
|
INSERT INTO user (id, username, realname, email, password, join_time, age, avatar, gender, phone,
|
||||||
|
discussions_count, comments_count, last_seen_time, flag, level)
|
||||||
|
VALUES (1, 'admin', '管理员', 'admin@jiscuss.local',
|
||||||
|
'$2a$10$7EqJtq98hPqEX7fNZaFWoOziYXMEIUBm..wTkPE3bsUJn4Lm7oHVC',
|
||||||
|
'2019-09-09 00:00:00', 31, '', '男', '13800000001', 0, 0, '2019-09-09 00:00:00', 1, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
INSERT INTO user (id, username, realname, email, password, join_time, age, avatar, gender, phone,
|
||||||
|
discussions_count, comments_count, last_seen_time, flag, level)
|
||||||
|
VALUES (2, 'test', '测试用户', 'test@jiscuss.local',
|
||||||
|
'$2a$10$7EqJtq98hPqEX7fNZaFWoOziYXMEIUBm..wTkPE3bsUJn4Lm7oHVC',
|
||||||
|
'2019-09-09 00:00:00', 31, '', '男', '13800000002', 0, 0, '2019-09-09 00:00:00', 1, 0)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
INSERT INTO discussion (id, title, content, start_time, start_user_id, last_time, last_user_id, create_id, create_time)
|
||||||
|
VALUES (1, '测试主题1', '测试内容1', '2020-09-19 00:00:00', 1, '2020-09-29 00:00:00', 2, 1, '2020-09-09 00:00:00')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
INSERT INTO tag (id, name, icon, position)
|
||||||
|
VALUES (1, '测试标签1', 'edit', 1)
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
INSERT INTO post (id, discussion_id, number, time, user_id, content, create_id, create_time)
|
||||||
|
VALUES (1, 1, 1, '2020-02-09 00:00:00', 1, '评论内容222', 1, '2020-08-09 00:00:00')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
-- V2: Security hardening — password column expansion + BCrypt migration.
|
||||||
|
--
|
||||||
|
-- BACKGROUND:
|
||||||
|
-- The original schema used VARCHAR(20) for the password column, which is too
|
||||||
|
-- short to store BCrypt hashes (60–68 characters). This migration:
|
||||||
|
-- 1. Expands the column to VARCHAR(255).
|
||||||
|
-- 2. Provides UPDATE statements to replace any remaining plaintext passwords
|
||||||
|
-- with BCrypt hashes (useful when applying this migration to an existing
|
||||||
|
-- database that has not yet been migrated).
|
||||||
|
--
|
||||||
|
-- NOTE: The example BCrypt hash below encodes the string '123456'.
|
||||||
|
-- For a real production migration, generate individual hashes per user
|
||||||
|
-- using a one-off script and run this migration during a maintenance window.
|
||||||
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Step 1: Expand the password column so it can hold BCrypt hashes
|
||||||
|
ALTER TABLE user
|
||||||
|
MODIFY COLUMN password VARCHAR(255);
|
||||||
|
|
||||||
|
-- Step 2: Identify and log any users whose passwords look like plaintext
|
||||||
|
-- (BCrypt hashes always start with '$2a$' or '$2b$')
|
||||||
|
-- Run this SELECT manually before Step 3 to audit affected rows:
|
||||||
|
--
|
||||||
|
-- SELECT id, username FROM user WHERE password NOT LIKE '$2%';
|
||||||
|
|
||||||
|
-- Step 3: Replace all non-BCrypt passwords with the hash of a known reset value
|
||||||
|
-- ('changeme123' hashed below) and force a password reset via application logic.
|
||||||
|
-- Replace the hash value with one generated by your BCryptPasswordEncoder.
|
||||||
|
--
|
||||||
|
-- UPDATE user
|
||||||
|
-- SET password = '$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||||||
|
-- WHERE password NOT LIKE '$2%';
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Ehcache 3 configuration (JCache / JSR-107 format).
|
||||||
|
Referenced by: spring.cache.jcache.config=classpath:ehcache3.xml
|
||||||
|
|
||||||
|
Two caches:
|
||||||
|
"user" — single User entities keyed by ID (e.g. @Cacheable(value="user", key="#id"))
|
||||||
|
"userList" — full user list (e.g. @Cacheable(value="userList"))
|
||||||
|
|
||||||
|
TTI (time-to-idle) evicts entries not accessed within the specified window,
|
||||||
|
preventing stale data accumulation and cache stampede from eternal entries.
|
||||||
|
-->
|
||||||
|
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xmlns="http://www.ehcache.org/v3"
|
||||||
|
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
|
||||||
|
|
||||||
|
<!-- Single user entity cache: up to 5 000 entries, evict if idle for 5 minutes -->
|
||||||
|
<cache alias="user">
|
||||||
|
<expiry>
|
||||||
|
<tti unit="minutes">5</tti>
|
||||||
|
</expiry>
|
||||||
|
<heap unit="entries">5000</heap>
|
||||||
|
</cache>
|
||||||
|
|
||||||
|
<!-- Full user-list cache: small number of keys, evict after 2 minutes (reduces stampede risk) -->
|
||||||
|
<cache alias="userList">
|
||||||
|
<expiry>
|
||||||
|
<ttl unit="minutes">2</ttl>
|
||||||
|
</expiry>
|
||||||
|
<heap unit="entries">10</heap>
|
||||||
|
</cache>
|
||||||
|
|
||||||
|
</config>
|
||||||
Reference in New Issue
Block a user