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] 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