From c06ab6022cfd3250a07c0245eb3ec86834b7ff2a Mon Sep 17 00:00:00 2001 From: Egorniiillll <119012231+Egorniiillll@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:08:40 +0300 Subject: [PATCH 1/4] create security --- build.gradle.kts | 25 ++++++--- gradle/wrapper/gradle-wrapper.properties | 2 +- .../Admin.java => AdminController.java} | 25 ++++----- .../{user/User.java => UserController.java} | 9 ++-- .../feedback/security/JwtTokenFilter.java | 48 +++++++++++++++++ .../feedback/security/JwtTokenProvider.java | 51 +++++++++++++++++++ .../feedback/security/SecurityConfig.java | 36 +++++++++++++ src/main/resources/application.properties | 11 ++-- 8 files changed, 182 insertions(+), 25 deletions(-) rename src/main/java/drip/competition/feedback/controller/{competition/Admin.java => AdminController.java} (61%) rename src/main/java/drip/competition/feedback/controller/{user/User.java => UserController.java} (81%) create mode 100644 src/main/java/drip/competition/feedback/security/JwtTokenFilter.java create mode 100644 src/main/java/drip/competition/feedback/security/JwtTokenProvider.java create mode 100644 src/main/java/drip/competition/feedback/security/SecurityConfig.java diff --git a/build.gradle.kts b/build.gradle.kts index 6319510..979246f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -21,19 +21,32 @@ repositories { extra["snippetsDir"] = file("build/generated-snippets") dependencies { - implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") - implementation ("org.springframework.boot:spring-boot-starter-data-jpa") - implementation ("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0") - implementation ("org.hibernate.orm:hibernate-core:6.4.4.Final") + implementation("org.springframework.boot:spring-boot-starter-security") + + runtimeOnly("org.postgresql:postgresql") + + implementation("io.jsonwebtoken:jjwt-api:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5") + + + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0") + + + compileOnly("org.projectlombok:lombok:1.18.30") + annotationProcessor("org.projectlombok:lombok:1.18.30") + + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.springframework.restdocs:spring-restdocs-mockmvc") testImplementation("org.testcontainers:postgresql:1.20.0") testRuntimeOnly("org.junit.platform:junit-platform-launcher") - compileOnly("org.projectlombok:lombok:1.18.30") - annotationProcessor("org.projectlombok:lombok:1.18.30") + + implementation ("org.hibernate.validator:hibernate-validator:8.0.1.Final") } tasks.withType { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff23a68..a80b22c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/drip/competition/feedback/controller/competition/Admin.java b/src/main/java/drip/competition/feedback/controller/AdminController.java similarity index 61% rename from src/main/java/drip/competition/feedback/controller/competition/Admin.java rename to src/main/java/drip/competition/feedback/controller/AdminController.java index 5dcdb7e..0aaa871 100644 --- a/src/main/java/drip/competition/feedback/controller/competition/Admin.java +++ b/src/main/java/drip/competition/feedback/controller/AdminController.java @@ -1,40 +1,41 @@ -package drip.competition.feedback.controller.competition; +package drip.competition.feedback.controller; import drip.competition.feedback.entities.Competition; import drip.competition.feedback.entities.Game; import drip.competition.feedback.repository.CompetitionRepository; import drip.competition.feedback.repository.GameRepository; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; import java.util.UUID; + @RestController @RequestMapping("/admin") -public class Admin { +@PreAuthorize("hasRole('ADMIN')") +public class AdminController { private final GameRepository gameRepository; private final CompetitionRepository competitionRepository; - public Admin(GameRepository gameRepository, CompetitionRepository competitionRepository) { + public AdminController(GameRepository gameRepository, + CompetitionRepository competitionRepository) { this.gameRepository = gameRepository; this.competitionRepository = competitionRepository; } - @Transactional(readOnly = true) @GetMapping("/user") + @Transactional(readOnly = true) public List getByUser(@RequestParam UUID iduser) { List games = gameRepository.findByIduser(iduser); List competitions = competitionRepository.findByIduser(iduser); - List combined = new ArrayList<>(); - combined.addAll(games); - combined.addAll(competitions); - return combined; + List result = new ArrayList<>(); + result.addAll(games); + result.addAll(competitions); + return result; } } diff --git a/src/main/java/drip/competition/feedback/controller/user/User.java b/src/main/java/drip/competition/feedback/controller/UserController.java similarity index 81% rename from src/main/java/drip/competition/feedback/controller/user/User.java rename to src/main/java/drip/competition/feedback/controller/UserController.java index 9e5d922..9f59285 100644 --- a/src/main/java/drip/competition/feedback/controller/user/User.java +++ b/src/main/java/drip/competition/feedback/controller/UserController.java @@ -1,9 +1,10 @@ -package drip.competition.feedback.controller.user; +package drip.competition.feedback.controller; import drip.competition.feedback.entities.Competition; import drip.competition.feedback.entities.Game; import drip.competition.feedback.repository.CompetitionRepository; import drip.competition.feedback.repository.GameRepository; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -12,12 +13,14 @@ @RestController @RequestMapping("/user") -public class User { +@PreAuthorize("isAuthenticated()") +public class UserController { private final GameRepository gameRepository; private final CompetitionRepository competitionRepository; - public User(GameRepository gameRepository, CompetitionRepository competitionRepository) { + public UserController(GameRepository gameRepository, + CompetitionRepository competitionRepository) { this.gameRepository = gameRepository; this.competitionRepository = competitionRepository; } diff --git a/src/main/java/drip/competition/feedback/security/JwtTokenFilter.java b/src/main/java/drip/competition/feedback/security/JwtTokenFilter.java new file mode 100644 index 0000000..a2ca780 --- /dev/null +++ b/src/main/java/drip/competition/feedback/security/JwtTokenFilter.java @@ -0,0 +1,48 @@ +package drip.competition.feedback.security; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.lang.NonNull; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +public class JwtTokenFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + String token = resolveToken(request); + + if (token != null && jwtTokenProvider.validateToken(token)) { + String role = jwtTokenProvider.getRoleFromToken(token); + + if (request.getRequestURI().startsWith("/admin") && !"ADMIN".equals(role)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied"); + return; + } + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest req) { + String bearerToken = req.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/src/main/java/drip/competition/feedback/security/JwtTokenProvider.java b/src/main/java/drip/competition/feedback/security/JwtTokenProvider.java new file mode 100644 index 0000000..9207fcf --- /dev/null +++ b/src/main/java/drip/competition/feedback/security/JwtTokenProvider.java @@ -0,0 +1,51 @@ +package drip.competition.feedback.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + + +import java.security.Key; +import java.util.Date; + +@Component +public class JwtTokenProvider { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private long expiration; + + private Key key; + + @PostConstruct + public void init() { + this.key = Keys.hmacShaKeyFor(secret.getBytes()); + } + + + + public Claims getAllClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public boolean isTokenExpired(String token) { + return this.getAllClaimsFromToken(token).getExpiration().before(new Date()); + } + + public boolean validateToken(String token) { + return !isTokenExpired(token); + } + + public String getRoleFromToken(String token) { + return getAllClaimsFromToken(token).get("role", String.class); + } +} diff --git a/src/main/java/drip/competition/feedback/security/SecurityConfig.java b/src/main/java/drip/competition/feedback/security/SecurityConfig.java new file mode 100644 index 0000000..90e7611 --- /dev/null +++ b/src/main/java/drip/competition/feedback/security/SecurityConfig.java @@ -0,0 +1,36 @@ +package drip.competition.feedback.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + public SecurityConfig(JwtTokenProvider jwtTokenProvider) { + this.jwtTokenProvider = jwtTokenProvider; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(csrf -> csrf.disable()) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().permitAll() + ) + .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ca8c6ac..8ed7704 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,8 +1,13 @@ -spring.application.name=feedback - +# DB connection spring.datasource.url=jdbc:postgresql://localhost:5433/database spring.datasource.username=postgres spring.datasource.password=postgres - spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +# JWT +jwt.secret=eyJhbGciOiJIUzI1NiJ9MySuperSecretLongKeyJustForTesting123456 +jwt.expiration=3600000 + +# App +spring.application.name=feedback From bd5b0ed2b42ba92e59cdec523cef67ef1e58f9e3 Mon Sep 17 00:00:00 2001 From: Egorniiillll <119012231+Egorniiillll@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:34:44 +0300 Subject: [PATCH 2/4] create security1.1 --- src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8ed7704..d8ddce3 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,7 +6,7 @@ spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect # JWT -jwt.secret=eyJhbGciOiJIUzI1NiJ9MySuperSecretLongKeyJustForTesting123456 +jwt.secret=MySuperSecretKeyWhichShouldBeLongEnough123456 jwt.expiration=3600000 # App From ec18945535385569dcb91d4c587dbd6e36b4a7b7 Mon Sep 17 00:00:00 2001 From: Egorniiillll <119012231+Egorniiillll@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:41:45 +0300 Subject: [PATCH 3/4] create security1.2 --- .../drip/competition/feedback/controller/AdminController.java | 1 - .../drip/competition/feedback/FeedbackApplicationTests.java | 2 ++ src/test/resources/application.properties | 2 ++ 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/drip/competition/feedback/controller/AdminController.java b/src/main/java/drip/competition/feedback/controller/AdminController.java index 0aaa871..a65ee68 100644 --- a/src/main/java/drip/competition/feedback/controller/AdminController.java +++ b/src/main/java/drip/competition/feedback/controller/AdminController.java @@ -32,7 +32,6 @@ public AdminController(GameRepository gameRepository, public List getByUser(@RequestParam UUID iduser) { List games = gameRepository.findByIduser(iduser); List competitions = competitionRepository.findByIduser(iduser); - List result = new ArrayList<>(); result.addAll(games); result.addAll(competitions); diff --git a/src/test/java/drip/competition/feedback/FeedbackApplicationTests.java b/src/test/java/drip/competition/feedback/FeedbackApplicationTests.java index f5e845d..c6f50d9 100644 --- a/src/test/java/drip/competition/feedback/FeedbackApplicationTests.java +++ b/src/test/java/drip/competition/feedback/FeedbackApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; @SpringBootTest +@TestPropertySource("classpath:application.properties") class FeedbackApplicationTests { @Test diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index cda8a0f..10a4841 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -2,3 +2,5 @@ spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDri spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:tc:postgresql:15:///integration-tests-db spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +jwt.secret=jwt.secret=ThisIsASecureJWTKeyWithEnoughLength123456 +jwt.expiration=3600000 From 99e8c05a18e234b256e29bf81f4716d276b05f5b Mon Sep 17 00:00:00 2001 From: Egorniiillll <119012231+Egorniiillll@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:54:12 +0300 Subject: [PATCH 4/4] create security1.3 --- .../feedback/security/JwtTokenFilter.java | 34 ++++++++++++++----- .../feedback/security/JwtTokenGenerator.java | 25 ++++++++++++++ .../feedback/security/SecurityConfig.java | 2 ++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/main/java/drip/competition/feedback/security/JwtTokenGenerator.java diff --git a/src/main/java/drip/competition/feedback/security/JwtTokenFilter.java b/src/main/java/drip/competition/feedback/security/JwtTokenFilter.java index a2ca780..b6220de 100644 --- a/src/main/java/drip/competition/feedback/security/JwtTokenFilter.java +++ b/src/main/java/drip/competition/feedback/security/JwtTokenFilter.java @@ -5,9 +5,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import java.util.List; public class JwtTokenFilter extends OncePerRequestFilter { @@ -23,21 +27,35 @@ protected void doFilterInternal( @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { + try { + String token = resolveToken(request); - String token = resolveToken(request); + if (token != null && jwtTokenProvider.validateToken(token)) { + String role = jwtTokenProvider.getRoleFromToken(token); - if (token != null && jwtTokenProvider.validateToken(token)) { - String role = jwtTokenProvider.getRoleFromToken(token); + if (request.getRequestURI().startsWith("/admin") && !"ADMIN".equals(role)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied"); + return; + } - if (request.getRequestURI().startsWith("/admin") && !"ADMIN".equals(role)) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access denied"); - return; + List authorities = + List.of(new SimpleGrantedAuthority("ROLE_" + role)); + UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken("user", null, authorities); + SecurityContextHolder.getContext().setAuthentication(auth); } - } - filterChain.doFilter(request, response); + filterChain.doFilter(request, response); + + } catch (Exception ex) { + ex.printStackTrace(); + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token: " + ex.getMessage()); + } } + + + private String resolveToken(HttpServletRequest req) { String bearerToken = req.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { diff --git a/src/main/java/drip/competition/feedback/security/JwtTokenGenerator.java b/src/main/java/drip/competition/feedback/security/JwtTokenGenerator.java new file mode 100644 index 0000000..efc188f --- /dev/null +++ b/src/main/java/drip/competition/feedback/security/JwtTokenGenerator.java @@ -0,0 +1,25 @@ +package drip.competition.feedback.security; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +import java.util.Date; +import java.util.Map; + +public class JwtTokenGenerator { + public static void main(String[] args) { + String secret = "MySuperSecretKeyWhichShouldBeLongEnough123456"; + byte[] keyBytes = secret.getBytes(); + + String token = Jwts.builder() + .setSubject("admin123") + .addClaims(Map.of("role", "ADMIN")) + .setExpiration(new Date(System.currentTimeMillis() + 3600_000)) + .signWith(Keys.hmacShaKeyFor(keyBytes), SignatureAlgorithm.HS256) + .compact(); + + System.out.println("✅ YOUR VALID TOKEN:"); + System.out.println(token); + } +} diff --git a/src/main/java/drip/competition/feedback/security/SecurityConfig.java b/src/main/java/drip/competition/feedback/security/SecurityConfig.java index 90e7611..93544c8 100644 --- a/src/main/java/drip/competition/feedback/security/SecurityConfig.java +++ b/src/main/java/drip/competition/feedback/security/SecurityConfig.java @@ -2,6 +2,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +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.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; @@ -10,6 +11,7 @@ @Configuration @EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) public class SecurityConfig { private final JwtTokenProvider jwtTokenProvider;