From 6bc20c9c3bbd7fe9daf0d64f6fbabfc670c38ef8 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Mon, 25 Nov 2024 22:27:38 +0500 Subject: [PATCH 01/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20spring=20security=20dependency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index e6a570f..5af26db 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,7 @@ dependencies { // spring implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-security' // db implementation 'org.springframework.boot:spring-boot-starter-data-jpa' From 49ab3b0310cb5e34da5c823e865aa4c09dfb0830 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Mon, 25 Nov 2024 22:29:23 +0500 Subject: [PATCH 02/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B2=20=D0=91=D0=94=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=D1=83=20User=20=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81?= =?UTF-8?q?=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D1=8B=20=D1=81=20=D1=8D=D1=82=D0=BE=D0=B9=20=D1=82=D0=B0=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D1=86=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/fin/spr/models/security/User.java | 53 +++++++++++++++++++ .../repository/security/UserRepository.java | 11 ++++ .../db/changelog/security-changelog.sql | 17 ++++++ 3 files changed, 81 insertions(+) create mode 100644 src/main/java/com/fin/spr/models/security/User.java create mode 100644 src/main/java/com/fin/spr/repository/security/UserRepository.java create mode 100644 src/main/resources/db/changelog/security-changelog.sql diff --git a/src/main/java/com/fin/spr/models/security/User.java b/src/main/java/com/fin/spr/models/security/User.java new file mode 100644 index 0000000..bb8bc86 --- /dev/null +++ b/src/main/java/com/fin/spr/models/security/User.java @@ -0,0 +1,53 @@ +package com.fin.spr.models.security; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@Builder +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "t_users", schema = "security") +public class User implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "c_name", nullable = false) + private String name; + + @Column(name = "c_login", unique = true, nullable = false) + private String login; + + @Column(name = "c_hashed_password", nullable = false) + private String hashedPassword; + + @Enumerated(EnumType.STRING) + @Column(name = "c_role", nullable = false) + private Role role; + + @Override + public Collection getAuthorities() { + return Collections.singleton(new SimpleGrantedAuthority(role.name())); + } + + @Override + public String getPassword() { + return hashedPassword; + } + + @Override + public String getUsername() { + return login; + } +} diff --git a/src/main/java/com/fin/spr/repository/security/UserRepository.java b/src/main/java/com/fin/spr/repository/security/UserRepository.java new file mode 100644 index 0000000..dd70bda --- /dev/null +++ b/src/main/java/com/fin/spr/repository/security/UserRepository.java @@ -0,0 +1,11 @@ +package com.fin.spr.repository.security; + +import com.fin.spr.models.security.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository { + + Optional findByLogin(String login); +} diff --git a/src/main/resources/db/changelog/security-changelog.sql b/src/main/resources/db/changelog/security-changelog.sql new file mode 100644 index 0000000..bd710c8 --- /dev/null +++ b/src/main/resources/db/changelog/security-changelog.sql @@ -0,0 +1,17 @@ +--liquibase formatted sql + +--changeset alexandergarifullin:1 +CREATE SCHEMA IF NOT EXISTS security; + +--changeset alexandergarifullin:2 +CREATE TABLE security.t_users +( + id BIGSERIAL PRIMARY KEY, + c_name TEXT NOT NULL, + c_login TEXT UNIQUE NOT NULL, + c_hashed_password TEXT NOT NULL, + c_role TEXT NOT NULL +); + +--changeset alexandergarifullin:3 +CREATE INDEX IF NOT EXISTS idx_users_login ON security.t_users (c_login); \ No newline at end of file From ca157f1285bf4f1255faffb42673ecdb922aa886 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Mon, 25 Nov 2024 22:29:55 +0500 Subject: [PATCH 03/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D1=83=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fin/spr/auth/AuthenticationResponse.java | 5 +++ .../com/fin/spr/auth/RegistrationRequest.java | 8 ++++ .../java/com/fin/spr/auth/UserController.java | 20 ++++++++++ .../com/fin/spr/config/SecurityConfig.java | 30 +++++++++++++++ .../UserAlreadyRegisterException.java | 14 +++++++ .../com/fin/spr/models/security/Role.java | 6 +++ .../security/AuthenticationService.java | 38 +++++++++++++++++++ .../db/changelog/db.changelog-master.yaml | 4 +- 8 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/fin/spr/auth/AuthenticationResponse.java create mode 100644 src/main/java/com/fin/spr/auth/RegistrationRequest.java create mode 100644 src/main/java/com/fin/spr/auth/UserController.java create mode 100644 src/main/java/com/fin/spr/config/SecurityConfig.java create mode 100644 src/main/java/com/fin/spr/exceptions/UserAlreadyRegisterException.java create mode 100644 src/main/java/com/fin/spr/models/security/Role.java create mode 100644 src/main/java/com/fin/spr/services/security/AuthenticationService.java diff --git a/src/main/java/com/fin/spr/auth/AuthenticationResponse.java b/src/main/java/com/fin/spr/auth/AuthenticationResponse.java new file mode 100644 index 0000000..1ed669c --- /dev/null +++ b/src/main/java/com/fin/spr/auth/AuthenticationResponse.java @@ -0,0 +1,5 @@ +package com.fin.spr.auth; + +public record AuthenticationResponse () { + +} diff --git a/src/main/java/com/fin/spr/auth/RegistrationRequest.java b/src/main/java/com/fin/spr/auth/RegistrationRequest.java new file mode 100644 index 0000000..8b89a06 --- /dev/null +++ b/src/main/java/com/fin/spr/auth/RegistrationRequest.java @@ -0,0 +1,8 @@ +package com.fin.spr.auth; + +public record RegistrationRequest ( + String name, + String login, + String password +) { +} diff --git a/src/main/java/com/fin/spr/auth/UserController.java b/src/main/java/com/fin/spr/auth/UserController.java new file mode 100644 index 0000000..00c0fc8 --- /dev/null +++ b/src/main/java/com/fin/spr/auth/UserController.java @@ -0,0 +1,20 @@ +package com.fin.spr.auth; + +import com.fin.spr.services.security.AuthenticationService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/auth") +public class UserController { + private final AuthenticationService authenticationService; + + @PostMapping("/register") + public AuthenticationResponse register(@RequestBody RegistrationRequest registrationRequest) { + return authenticationService.register(registrationRequest); + } +} diff --git a/src/main/java/com/fin/spr/config/SecurityConfig.java b/src/main/java/com/fin/spr/config/SecurityConfig.java new file mode 100644 index 0000000..3b1b71c --- /dev/null +++ b/src/main/java/com/fin/spr/config/SecurityConfig.java @@ -0,0 +1,30 @@ +package com.fin.spr.config; + +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.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/v1/auth/register").permitAll() + .anyRequest().authenticated() + ); + + return http.build(); + } +} diff --git a/src/main/java/com/fin/spr/exceptions/UserAlreadyRegisterException.java b/src/main/java/com/fin/spr/exceptions/UserAlreadyRegisterException.java new file mode 100644 index 0000000..8e5fb00 --- /dev/null +++ b/src/main/java/com/fin/spr/exceptions/UserAlreadyRegisterException.java @@ -0,0 +1,14 @@ +package com.fin.spr.exceptions; + +import lombok.Getter; + +@Getter +public class UserAlreadyRegisterException extends RuntimeException { + + private final String login; + + public UserAlreadyRegisterException(String login) { + super("login.already_register"); + this.login = login; + } +} diff --git a/src/main/java/com/fin/spr/models/security/Role.java b/src/main/java/com/fin/spr/models/security/Role.java new file mode 100644 index 0000000..6df0538 --- /dev/null +++ b/src/main/java/com/fin/spr/models/security/Role.java @@ -0,0 +1,6 @@ +package com.fin.spr.models.security; + +public enum Role { + USER, + ADMIN +} diff --git a/src/main/java/com/fin/spr/services/security/AuthenticationService.java b/src/main/java/com/fin/spr/services/security/AuthenticationService.java new file mode 100644 index 0000000..f559e65 --- /dev/null +++ b/src/main/java/com/fin/spr/services/security/AuthenticationService.java @@ -0,0 +1,38 @@ +package com.fin.spr.services.security; + +import com.fin.spr.auth.AuthenticationResponse; +import com.fin.spr.auth.RegistrationRequest; +import com.fin.spr.exceptions.UserAlreadyRegisterException; +import com.fin.spr.models.security.Role; +import com.fin.spr.models.security.User; +import com.fin.spr.repository.security.UserRepository; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthenticationService { + + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + + public AuthenticationResponse register(@NotNull RegistrationRequest registrationRequest) { + userRepository.findByLogin(registrationRequest.login()) + .ifPresent(user -> { + throw new UserAlreadyRegisterException(user.getLogin()); + }); + + User user = User.builder() + .name(registrationRequest.name()) + .login(registrationRequest.login()) + .role(Role.USER) + .hashedPassword(passwordEncoder.encode(registrationRequest.password())) + .build(); + userRepository.save(user); + + return new AuthenticationResponse(); + } +} + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index a8a51b0..01f95d8 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -1,3 +1,5 @@ databaseChangeLog: - include: - file: db/changelog/kudago-changelog.sql \ No newline at end of file + file: db/changelog/kudago-changelog.sql + - include: + file: db/changelog/security-changelog.sql \ No newline at end of file From 9b9056ed6d27007f8debca8599e8f974b2836bb3 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Tue, 26 Nov 2024 12:37:39 +0500 Subject: [PATCH 04/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B4=D0=BB=D1=8F=20jwt=20=D1=82=D0=BE=D0=BA=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 5af26db..de21a35 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,11 @@ dependencies { implementation 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.6' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6' + // aop implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.aspectj:aspectjweaver' From 8405313ca4913a2ca886cd0248cdf98535c96b97 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Tue, 26 Nov 2024 12:38:25 +0500 Subject: [PATCH 05/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20Token=20=D0=B2=20=D0=B1=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/fin/spr/models/security/Token.java | 29 +++++++++++++++++++ .../com/fin/spr/models/security/User.java | 3 ++ .../repository/security/TokenRepository.java | 11 +++++++ .../db/changelog/db.changelog-master.yaml | 4 ++- .../changelog/security-tokens-changelog.sql | 8 +++++ 5 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/fin/spr/models/security/Token.java create mode 100644 src/main/java/com/fin/spr/repository/security/TokenRepository.java create mode 100644 src/main/resources/db/changelog/security-tokens-changelog.sql diff --git a/src/main/java/com/fin/spr/models/security/Token.java b/src/main/java/com/fin/spr/models/security/Token.java new file mode 100644 index 0000000..e457db2 --- /dev/null +++ b/src/main/java/com/fin/spr/models/security/Token.java @@ -0,0 +1,29 @@ +package com.fin.spr.models.security; + +import jakarta.persistence.*; +import lombok.*; + +@Builder +@Setter +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "t_tokens", schema = "security") +public class Token { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "c_token", nullable = false) + private String token; + + @Column(name = "c_revoked", nullable = false) + private boolean revoked; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "c_user_id", nullable = false) + private User user; + +} diff --git a/src/main/java/com/fin/spr/models/security/User.java b/src/main/java/com/fin/spr/models/security/User.java index bb8bc86..114db05 100644 --- a/src/main/java/com/fin/spr/models/security/User.java +++ b/src/main/java/com/fin/spr/models/security/User.java @@ -36,6 +36,9 @@ public class User implements UserDetails { @Column(name = "c_role", nullable = false) private Role role; + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List tokens; + @Override public Collection getAuthorities() { return Collections.singleton(new SimpleGrantedAuthority(role.name())); diff --git a/src/main/java/com/fin/spr/repository/security/TokenRepository.java b/src/main/java/com/fin/spr/repository/security/TokenRepository.java new file mode 100644 index 0000000..30866fe --- /dev/null +++ b/src/main/java/com/fin/spr/repository/security/TokenRepository.java @@ -0,0 +1,11 @@ +package com.fin.spr.repository.security; + +import com.fin.spr.models.security.Token; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface TokenRepository extends JpaRepository { + Optional findByToken(String token); + +} diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 01f95d8..70bd948 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -2,4 +2,6 @@ databaseChangeLog: - include: file: db/changelog/kudago-changelog.sql - include: - file: db/changelog/security-changelog.sql \ No newline at end of file + file: db/changelog/security-changelog.sql + - include: + file: db/changelog/security-tokens-changelog.sql \ No newline at end of file diff --git a/src/main/resources/db/changelog/security-tokens-changelog.sql b/src/main/resources/db/changelog/security-tokens-changelog.sql new file mode 100644 index 0000000..d01f2c0 --- /dev/null +++ b/src/main/resources/db/changelog/security-tokens-changelog.sql @@ -0,0 +1,8 @@ +CREATE TABLE security.t_tokens +( + id BIGSERIAL PRIMARY KEY, + c_token TEXT NOT NULL, + c_revoked BOOLEAN NOT NULL, + c_user_id BIGINT NOT NULL, + CONSTRAINT fk_user FOREIGN KEY (c_user_id) REFERENCES security.t_users (id) +); \ No newline at end of file From 9dede456e69ff12d87ac435b3f28f481e87f137a Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Tue, 26 Nov 2024 12:39:11 +0500 Subject: [PATCH 06/17] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20?= =?UTF-8?q?=D0=B2=D0=BE=D0=B7=D1=80=D0=B0=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?jwt=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0=20=D0=B2=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/fin/spr/auth/AuthenticationResponse.java | 5 ----- .../java/com/fin/spr/auth/JwtAuthenticationResponse.java | 6 ++++++ src/main/java/com/fin/spr/auth/UserController.java | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 src/main/java/com/fin/spr/auth/AuthenticationResponse.java create mode 100644 src/main/java/com/fin/spr/auth/JwtAuthenticationResponse.java diff --git a/src/main/java/com/fin/spr/auth/AuthenticationResponse.java b/src/main/java/com/fin/spr/auth/AuthenticationResponse.java deleted file mode 100644 index 1ed669c..0000000 --- a/src/main/java/com/fin/spr/auth/AuthenticationResponse.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.fin.spr.auth; - -public record AuthenticationResponse () { - -} diff --git a/src/main/java/com/fin/spr/auth/JwtAuthenticationResponse.java b/src/main/java/com/fin/spr/auth/JwtAuthenticationResponse.java new file mode 100644 index 0000000..79164c8 --- /dev/null +++ b/src/main/java/com/fin/spr/auth/JwtAuthenticationResponse.java @@ -0,0 +1,6 @@ +package com.fin.spr.auth; + +public record JwtAuthenticationResponse( + String token +) { +} diff --git a/src/main/java/com/fin/spr/auth/UserController.java b/src/main/java/com/fin/spr/auth/UserController.java index 00c0fc8..9858cba 100644 --- a/src/main/java/com/fin/spr/auth/UserController.java +++ b/src/main/java/com/fin/spr/auth/UserController.java @@ -14,7 +14,7 @@ public class UserController { private final AuthenticationService authenticationService; @PostMapping("/register") - public AuthenticationResponse register(@RequestBody RegistrationRequest registrationRequest) { + public JwtAuthenticationResponse register(@RequestBody RegistrationRequest registrationRequest) { return authenticationService.register(registrationRequest); } } From 36180ab2bc9c7b1822b126b2b62a1dcc4b11a4f9 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Tue, 26 Nov 2024 12:40:13 +0500 Subject: [PATCH 07/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=20=D0=B4=D0=BB?= =?UTF-8?q?=D1=8F=20=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= =?UTF-8?q?=20jwt=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/AuthenticationService.java | 18 +++++-- .../fin/spr/services/security/JwtService.java | 50 +++++++++++++++++++ src/main/resources/application.yml | 7 +++ 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/fin/spr/services/security/JwtService.java diff --git a/src/main/java/com/fin/spr/services/security/AuthenticationService.java b/src/main/java/com/fin/spr/services/security/AuthenticationService.java index f559e65..3c43034 100644 --- a/src/main/java/com/fin/spr/services/security/AuthenticationService.java +++ b/src/main/java/com/fin/spr/services/security/AuthenticationService.java @@ -1,10 +1,12 @@ package com.fin.spr.services.security; -import com.fin.spr.auth.AuthenticationResponse; +import com.fin.spr.auth.JwtAuthenticationResponse; import com.fin.spr.auth.RegistrationRequest; import com.fin.spr.exceptions.UserAlreadyRegisterException; import com.fin.spr.models.security.Role; +import com.fin.spr.models.security.Token; import com.fin.spr.models.security.User; +import com.fin.spr.repository.security.TokenRepository; import com.fin.spr.repository.security.UserRepository; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; @@ -16,9 +18,11 @@ public class AuthenticationService { private final UserRepository userRepository; + private final TokenRepository tokenRepository; private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; - public AuthenticationResponse register(@NotNull RegistrationRequest registrationRequest) { + public JwtAuthenticationResponse register(@NotNull RegistrationRequest registrationRequest) { userRepository.findByLogin(registrationRequest.login()) .ifPresent(user -> { throw new UserAlreadyRegisterException(user.getLogin()); @@ -32,7 +36,15 @@ public AuthenticationResponse register(@NotNull RegistrationRequest registration .build(); userRepository.save(user); - return new AuthenticationResponse(); + String jwtToken = jwtService.generateToken(user, false); + + Token token = Token.builder() + .token(jwtToken) + .user(user) + .build(); + tokenRepository.save(token); + + return new JwtAuthenticationResponse(jwtToken); } } diff --git a/src/main/java/com/fin/spr/services/security/JwtService.java b/src/main/java/com/fin/spr/services/security/JwtService.java new file mode 100644 index 0000000..a692c2a --- /dev/null +++ b/src/main/java/com/fin/spr/services/security/JwtService.java @@ -0,0 +1,50 @@ +package com.fin.spr.services.security; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import io.jsonwebtoken.Jwts; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.time.Duration; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtService { + + @Value("${jwt.short}") + private Duration shortTerm; + + @Value("${jwt.long}") + private Duration longTerm; + + @Value("${jwt.secret}") + private String jwtSigningKey; + + + public String generateToken(UserDetails userDetails, boolean rememberMe) { + return generateToken(Map.of(), userDetails, rememberMe); + } + + private String generateToken(Map extraClaims, UserDetails userDetails, boolean rememberMe) { + var tokenDuration = rememberMe ? longTerm : shortTerm; + return Jwts.builder() + .claims(extraClaims) + .subject(userDetails.getUsername()) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + tokenDuration.toMillis())) + .signWith(getSigningKey(), Jwts.SIG.HS256) + .compact(); + } + + private SecretKey getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 58b4f35..3c80562 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -28,3 +28,10 @@ app: init: fixed-thread-pool-size: 2 schedule-duration: PT1H + + +jwt: + secret: mysuperSecretPasswordformysuperbestAppwithsuperloginandBisness123Good2wow3magic1 + short: 600 # 10 minutes + long: 2592000 # 30 days + From 9d85bffa5f0d41aea8552aa625115fd6a330361e Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Tue, 26 Nov 2024 12:53:30 +0500 Subject: [PATCH 08/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20login=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/fin/spr/auth/UserController.java | 5 ++++ .../com/fin/spr/config/SecurityConfig.java | 7 +++++ .../spr/exceptions/UserNotFoundException.java | 14 +++++++++ .../repository/security/TokenRepository.java | 3 ++ .../security/AuthenticationService.java | 30 ++++++++++++++++++- 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/fin/spr/exceptions/UserNotFoundException.java diff --git a/src/main/java/com/fin/spr/auth/UserController.java b/src/main/java/com/fin/spr/auth/UserController.java index 9858cba..4607bb3 100644 --- a/src/main/java/com/fin/spr/auth/UserController.java +++ b/src/main/java/com/fin/spr/auth/UserController.java @@ -17,4 +17,9 @@ public class UserController { public JwtAuthenticationResponse register(@RequestBody RegistrationRequest registrationRequest) { return authenticationService.register(registrationRequest); } + + @PostMapping("/login") + public JwtAuthenticationResponse login(@RequestBody RegistrationRequest registrationRequest) { + return authenticationService.login(registrationRequest); + } } diff --git a/src/main/java/com/fin/spr/config/SecurityConfig.java b/src/main/java/com/fin/spr/config/SecurityConfig.java index 3b1b71c..253c3e3 100644 --- a/src/main/java/com/fin/spr/config/SecurityConfig.java +++ b/src/main/java/com/fin/spr/config/SecurityConfig.java @@ -2,6 +2,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -27,4 +29,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } } diff --git a/src/main/java/com/fin/spr/exceptions/UserNotFoundException.java b/src/main/java/com/fin/spr/exceptions/UserNotFoundException.java new file mode 100644 index 0000000..50f2bc4 --- /dev/null +++ b/src/main/java/com/fin/spr/exceptions/UserNotFoundException.java @@ -0,0 +1,14 @@ +package com.fin.spr.exceptions; + +import lombok.Getter; + +@Getter +public class UserNotFoundException extends RuntimeException { + + private final String login; + + public UserNotFoundException(String login) { + super("login.not_found"); + this.login = login; + } +} diff --git a/src/main/java/com/fin/spr/repository/security/TokenRepository.java b/src/main/java/com/fin/spr/repository/security/TokenRepository.java index 30866fe..776b0d1 100644 --- a/src/main/java/com/fin/spr/repository/security/TokenRepository.java +++ b/src/main/java/com/fin/spr/repository/security/TokenRepository.java @@ -1,11 +1,14 @@ package com.fin.spr.repository.security; import com.fin.spr.models.security.Token; +import com.fin.spr.models.security.User; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface TokenRepository extends JpaRepository { Optional findByToken(String token); + List findAllByUserAndRevoked(User user, boolean revoked); } diff --git a/src/main/java/com/fin/spr/services/security/AuthenticationService.java b/src/main/java/com/fin/spr/services/security/AuthenticationService.java index 3c43034..6f5ee50 100644 --- a/src/main/java/com/fin/spr/services/security/AuthenticationService.java +++ b/src/main/java/com/fin/spr/services/security/AuthenticationService.java @@ -3,6 +3,7 @@ import com.fin.spr.auth.JwtAuthenticationResponse; import com.fin.spr.auth.RegistrationRequest; import com.fin.spr.exceptions.UserAlreadyRegisterException; +import com.fin.spr.exceptions.UserNotFoundException; import com.fin.spr.models.security.Role; import com.fin.spr.models.security.Token; import com.fin.spr.models.security.User; @@ -10,6 +11,8 @@ import com.fin.spr.repository.security.UserRepository; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -21,10 +24,11 @@ public class AuthenticationService { private final TokenRepository tokenRepository; private final PasswordEncoder passwordEncoder; private final JwtService jwtService; + private final AuthenticationManager authenticationManager; public JwtAuthenticationResponse register(@NotNull RegistrationRequest registrationRequest) { userRepository.findByLogin(registrationRequest.login()) - .ifPresent(user -> { + .ifPresent(user -> { throw new UserAlreadyRegisterException(user.getLogin()); }); @@ -46,5 +50,29 @@ public JwtAuthenticationResponse register(@NotNull RegistrationRequest registrat return new JwtAuthenticationResponse(jwtToken); } + + public JwtAuthenticationResponse login(@NotNull RegistrationRequest registrationRequest) { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( + registrationRequest.login(), + registrationRequest.password() + )); + + var user = userRepository.findByLogin(registrationRequest.login()) + .orElseThrow(() -> new UserNotFoundException(registrationRequest.login())); + + var tokens = tokenRepository.findAllByUserAndRevoked(user, false); + tokens.forEach(token -> token.setRevoked(true)); + tokenRepository.saveAll(tokens); + + String jwtToken = jwtService.generateToken(user, false); + + Token token = Token.builder() + .token(jwtToken) + .user(user) + .build(); + tokenRepository.save(token); + + return new JwtAuthenticationResponse(jwtToken); + } } From c606e3f123997ecd6d9915b5521e906579862713 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Tue, 26 Nov 2024 18:02:11 +0500 Subject: [PATCH 09/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20login=20=D0=BF=D0=BE=20JWT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fin/spr/auth/JwtAuthenticationFilter.java | 58 +++++++++++++++++++ .../com/fin/spr/auth/RegistrationRequest.java | 8 --- .../java/com/fin/spr/auth/UserController.java | 8 ++- .../java/com/fin/spr/auth/UserDetails.java | 32 ++++++++++ .../java/com/fin/spr/config/AppConfig.java | 36 ++++++++++++ .../com/fin/spr/config/SecurityConfig.java | 48 +++++++++++---- .../security/AuthenticationPayload.java | 8 +++ .../payload/security/RegistrationPayload.java | 8 +++ .../com/fin/spr/models/security/User.java | 17 +----- .../security/AuthenticationService.java | 23 ++++---- .../fin/spr/services/security/JwtService.java | 19 ++++++ .../security/MyUserDetailsService.java | 22 +++++++ src/main/resources/application.yml | 4 +- 13 files changed, 240 insertions(+), 51 deletions(-) create mode 100644 src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java delete mode 100644 src/main/java/com/fin/spr/auth/RegistrationRequest.java create mode 100644 src/main/java/com/fin/spr/auth/UserDetails.java create mode 100644 src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java create mode 100644 src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java create mode 100644 src/main/java/com/fin/spr/services/security/MyUserDetailsService.java diff --git a/src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java b/src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java new file mode 100644 index 0000000..267183c --- /dev/null +++ b/src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java @@ -0,0 +1,58 @@ +package com.fin.spr.auth; + +import com.fin.spr.services.security.JwtService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + public static final String BEARER_PREFIX = "Bearer "; + + private final JwtService jwtService; + + private final UserDetailsService userDetailsService; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + var authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); + + if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { + filterChain.doFilter(request, response); + return; + } + + var jwt = authHeader.substring(BEARER_PREFIX.length()); + var userLogin = jwtService.extractUserLogin(jwt); + + var userDetails = userDetailsService.loadUserByUsername(userLogin); + + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, + userDetails.getPassword(), + userDetails.getAuthorities()); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + if (SecurityContextHolder.getContext().getAuthentication() == null) { + SecurityContextHolder.getContext().setAuthentication(authToken); + } + + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/com/fin/spr/auth/RegistrationRequest.java b/src/main/java/com/fin/spr/auth/RegistrationRequest.java deleted file mode 100644 index 8b89a06..0000000 --- a/src/main/java/com/fin/spr/auth/RegistrationRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.fin.spr.auth; - -public record RegistrationRequest ( - String name, - String login, - String password -) { -} diff --git a/src/main/java/com/fin/spr/auth/UserController.java b/src/main/java/com/fin/spr/auth/UserController.java index 4607bb3..c631113 100644 --- a/src/main/java/com/fin/spr/auth/UserController.java +++ b/src/main/java/com/fin/spr/auth/UserController.java @@ -1,5 +1,7 @@ package com.fin.spr.auth; +import com.fin.spr.controllers.payload.security.AuthenticationPayload; +import com.fin.spr.controllers.payload.security.RegistrationPayload; import com.fin.spr.services.security.AuthenticationService; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; @@ -14,12 +16,12 @@ public class UserController { private final AuthenticationService authenticationService; @PostMapping("/register") - public JwtAuthenticationResponse register(@RequestBody RegistrationRequest registrationRequest) { + public JwtAuthenticationResponse register(@RequestBody RegistrationPayload registrationRequest) { return authenticationService.register(registrationRequest); } @PostMapping("/login") - public JwtAuthenticationResponse login(@RequestBody RegistrationRequest registrationRequest) { - return authenticationService.login(registrationRequest); + public JwtAuthenticationResponse login(@RequestBody AuthenticationPayload authenticationPayload) { + return authenticationService.login(authenticationPayload); } } diff --git a/src/main/java/com/fin/spr/auth/UserDetails.java b/src/main/java/com/fin/spr/auth/UserDetails.java new file mode 100644 index 0000000..ad0d8df --- /dev/null +++ b/src/main/java/com/fin/spr/auth/UserDetails.java @@ -0,0 +1,32 @@ +package com.fin.spr.auth; + +import com.fin.spr.models.security.User; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collection; +import java.util.List; + +@RequiredArgsConstructor +@Getter +public class UserDetails implements org.springframework.security.core.userdetails.UserDetails { + + private final User user; + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(user.getRole().name())); + } + + @Override + public String getPassword() { + return user.getHashedPassword(); + } + + @Override + public String getUsername() { + return user.getLogin(); + } +} diff --git a/src/main/java/com/fin/spr/config/AppConfig.java b/src/main/java/com/fin/spr/config/AppConfig.java index ca3e3d9..300e23c 100644 --- a/src/main/java/com/fin/spr/config/AppConfig.java +++ b/src/main/java/com/fin/spr/config/AppConfig.java @@ -1,10 +1,19 @@ package com.fin.spr.config; +import com.fin.spr.services.security.MyUserDetailsService; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.client.RestClient; /** @@ -12,8 +21,12 @@ * It contains bean definitions that are used throughout the application. */ @Configuration +@RequiredArgsConstructor public class AppConfig { + private final MyUserDetailsService myUserDetailsService; + + /** * Creates and returns a new instance of {@link RestClient}. * @@ -26,4 +39,27 @@ public RestClient restClient(@Value("${kudago.api.base.url}") String url) { .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + var authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public UserDetailsService userDetailsService() { + return myUserDetailsService; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } } diff --git a/src/main/java/com/fin/spr/config/SecurityConfig.java b/src/main/java/com/fin/spr/config/SecurityConfig.java index 253c3e3..1940250 100644 --- a/src/main/java/com/fin/spr/config/SecurityConfig.java +++ b/src/main/java/com/fin/spr/config/SecurityConfig.java @@ -1,37 +1,61 @@ package com.fin.spr.config; +import com.fin.spr.auth.JwtAuthenticationFilter; +import com.fin.spr.services.security.MyUserDetailsService; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +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.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; + +import java.util.List; @Configuration +@EnableWebSecurity +@EnableMethodSecurity +@RequiredArgsConstructor public class SecurityConfig { - @Bean - public PasswordEncoder passwordEncoder() { - return new BCryptPasswordEncoder(); - } + private final JwtAuthenticationFilter jwtAuthFilter; + private final AuthenticationProvider authenticationProvider; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) - .authorizeHttpRequests(auth -> auth + .cors(cors -> cors.configurationSource(request -> { + var corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOriginPatterns(List.of("*")); + corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + corsConfiguration.setAllowedHeaders(List.of("*")); + corsConfiguration.setAllowCredentials(true); + return corsConfiguration; + })) + // Настройка доступа к конечным точкам + .authorizeHttpRequests(request -> request + // Можно указать конкретный путь, * - 1 уровень вложенности, ** - любое количество уровней вложенности .requestMatchers("/api/v1/auth/register").permitAll() - .anyRequest().authenticated() - ); + .requestMatchers("/swagger-ui/**", "/swagger-resources/*", "/v3/api-docs/**").permitAll() + .requestMatchers("/endpoint", "/admin/**").hasRole("ADMIN") + .anyRequest().authenticated()) + .sessionManagement(sessionManagementConfigurer -> + sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authenticationProvider(authenticationProvider) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { - return config.getAuthenticationManager(); - } } diff --git a/src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java b/src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java new file mode 100644 index 0000000..661593a --- /dev/null +++ b/src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java @@ -0,0 +1,8 @@ +package com.fin.spr.controllers.payload.security; + +public record AuthenticationPayload ( + String login, + String password, + boolean rememberMe +) { +} diff --git a/src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java b/src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java new file mode 100644 index 0000000..2b2f873 --- /dev/null +++ b/src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java @@ -0,0 +1,8 @@ +package com.fin.spr.controllers.payload.security; + +public record RegistrationPayload( + String name, + String login, + String password +) { +} diff --git a/src/main/java/com/fin/spr/models/security/User.java b/src/main/java/com/fin/spr/models/security/User.java index 114db05..8f0f321 100644 --- a/src/main/java/com/fin/spr/models/security/User.java +++ b/src/main/java/com/fin/spr/models/security/User.java @@ -17,7 +17,7 @@ @AllArgsConstructor @Entity @Table(name = "t_users", schema = "security") -public class User implements UserDetails { +public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -38,19 +38,4 @@ public class User implements UserDetails { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) private List tokens; - - @Override - public Collection getAuthorities() { - return Collections.singleton(new SimpleGrantedAuthority(role.name())); - } - - @Override - public String getPassword() { - return hashedPassword; - } - - @Override - public String getUsername() { - return login; - } } diff --git a/src/main/java/com/fin/spr/services/security/AuthenticationService.java b/src/main/java/com/fin/spr/services/security/AuthenticationService.java index 6f5ee50..1f83187 100644 --- a/src/main/java/com/fin/spr/services/security/AuthenticationService.java +++ b/src/main/java/com/fin/spr/services/security/AuthenticationService.java @@ -1,7 +1,9 @@ package com.fin.spr.services.security; import com.fin.spr.auth.JwtAuthenticationResponse; -import com.fin.spr.auth.RegistrationRequest; +import com.fin.spr.auth.UserDetails; +import com.fin.spr.controllers.payload.security.AuthenticationPayload; +import com.fin.spr.controllers.payload.security.RegistrationPayload; import com.fin.spr.exceptions.UserAlreadyRegisterException; import com.fin.spr.exceptions.UserNotFoundException; import com.fin.spr.models.security.Role; @@ -26,7 +28,7 @@ public class AuthenticationService { private final JwtService jwtService; private final AuthenticationManager authenticationManager; - public JwtAuthenticationResponse register(@NotNull RegistrationRequest registrationRequest) { + public JwtAuthenticationResponse register(@NotNull RegistrationPayload registrationRequest) { userRepository.findByLogin(registrationRequest.login()) .ifPresent(user -> { throw new UserAlreadyRegisterException(user.getLogin()); @@ -38,33 +40,34 @@ public JwtAuthenticationResponse register(@NotNull RegistrationRequest registrat .role(Role.USER) .hashedPassword(passwordEncoder.encode(registrationRequest.password())) .build(); - userRepository.save(user); - String jwtToken = jwtService.generateToken(user, false); + String jwtToken = jwtService.generateToken(new UserDetails(user), false); Token token = Token.builder() .token(jwtToken) .user(user) .build(); + + userRepository.save(user); tokenRepository.save(token); return new JwtAuthenticationResponse(jwtToken); } - public JwtAuthenticationResponse login(@NotNull RegistrationRequest registrationRequest) { + public JwtAuthenticationResponse login(@NotNull AuthenticationPayload authenticationPayload) { authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( - registrationRequest.login(), - registrationRequest.password() + authenticationPayload.login(), + authenticationPayload.password() )); - var user = userRepository.findByLogin(registrationRequest.login()) - .orElseThrow(() -> new UserNotFoundException(registrationRequest.login())); + var user = userRepository.findByLogin(authenticationPayload.login()) + .orElseThrow(() -> new UserNotFoundException(authenticationPayload.login())); var tokens = tokenRepository.findAllByUserAndRevoked(user, false); tokens.forEach(token -> token.setRevoked(true)); tokenRepository.saveAll(tokens); - String jwtToken = jwtService.generateToken(user, false); + String jwtToken = jwtService.generateToken(new UserDetails(user), authenticationPayload.rememberMe()); Token token = Token.builder() .token(jwtToken) diff --git a/src/main/java/com/fin/spr/services/security/JwtService.java b/src/main/java/com/fin/spr/services/security/JwtService.java index a692c2a..a0eddc6 100644 --- a/src/main/java/com/fin/spr/services/security/JwtService.java +++ b/src/main/java/com/fin/spr/services/security/JwtService.java @@ -1,5 +1,6 @@ package com.fin.spr.services.security; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; @@ -14,6 +15,7 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; @Service public class JwtService { @@ -47,4 +49,21 @@ private SecretKey getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey); return Keys.hmacShaKeyFor(keyBytes); } + + public String extractUserLogin(String token) { + return extractClaim(token, Claims::getSubject); + } + + private T extractClaim(String token, Function claimsResolvers) { + final Claims claims = extractAllClaims(token); + return claimsResolvers.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } } diff --git a/src/main/java/com/fin/spr/services/security/MyUserDetailsService.java b/src/main/java/com/fin/spr/services/security/MyUserDetailsService.java new file mode 100644 index 0000000..6be7d5f --- /dev/null +++ b/src/main/java/com/fin/spr/services/security/MyUserDetailsService.java @@ -0,0 +1,22 @@ +package com.fin.spr.services.security; + +import com.fin.spr.repository.security.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class MyUserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService { + private final UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException { + Optional user = userRepository.findByLogin(login); + if (user.isPresent()) return new com.fin.spr.auth.UserDetails(user.get()); + else throw new UsernameNotFoundException(login); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3c80562..72990b1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -32,6 +32,6 @@ app: jwt: secret: mysuperSecretPasswordformysuperbestAppwithsuperloginandBisness123Good2wow3magic1 - short: 600 # 10 minutes - long: 2592000 # 30 days + short: 10m # 10 minutes + long: 30d # 30 days From cb313695d9fb38ecf75d605dcaeb8fc7b48e4961 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Tue, 26 Nov 2024 18:42:26 +0500 Subject: [PATCH 10/17] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20lo?= =?UTF-8?q?gin=20=D0=B1=D0=B5=D0=B7=20jwt=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20(logout=20=D0=BF=D0=BE=20jwt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/fin/spr/auth/UserController.java | 8 ++++++++ src/main/java/com/fin/spr/config/SecurityConfig.java | 2 +- .../fin/spr/services/security/AuthenticationService.java | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/fin/spr/auth/UserController.java b/src/main/java/com/fin/spr/auth/UserController.java index c631113..8b98adb 100644 --- a/src/main/java/com/fin/spr/auth/UserController.java +++ b/src/main/java/com/fin/spr/auth/UserController.java @@ -4,6 +4,8 @@ import com.fin.spr.controllers.payload.security.RegistrationPayload; import com.fin.spr.services.security.AuthenticationService; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -24,4 +26,10 @@ public JwtAuthenticationResponse register(@RequestBody RegistrationPayload regis public JwtAuthenticationResponse login(@RequestBody AuthenticationPayload authenticationPayload) { return authenticationService.login(authenticationPayload); } + + @PostMapping("/logout") + public ResponseEntity logout(Authentication authentication) { + authenticationService.logout(authentication); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/fin/spr/config/SecurityConfig.java b/src/main/java/com/fin/spr/config/SecurityConfig.java index 1940250..308aff4 100644 --- a/src/main/java/com/fin/spr/config/SecurityConfig.java +++ b/src/main/java/com/fin/spr/config/SecurityConfig.java @@ -47,7 +47,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti // Настройка доступа к конечным точкам .authorizeHttpRequests(request -> request // Можно указать конкретный путь, * - 1 уровень вложенности, ** - любое количество уровней вложенности - .requestMatchers("/api/v1/auth/register").permitAll() + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login").permitAll() .requestMatchers("/swagger-ui/**", "/swagger-resources/*", "/v3/api-docs/**").permitAll() .requestMatchers("/endpoint", "/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) diff --git a/src/main/java/com/fin/spr/services/security/AuthenticationService.java b/src/main/java/com/fin/spr/services/security/AuthenticationService.java index 1f83187..e3c2539 100644 --- a/src/main/java/com/fin/spr/services/security/AuthenticationService.java +++ b/src/main/java/com/fin/spr/services/security/AuthenticationService.java @@ -15,6 +15,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -77,5 +78,10 @@ public JwtAuthenticationResponse login(@NotNull AuthenticationPayload authentica return new JwtAuthenticationResponse(jwtToken); } + + public void logout(@NotNull Authentication authentication) { + var userDetails = (UserDetails) authentication.getPrincipal(); + + } } From de1369488d2fce6600eed8d02f525fe1534fd7b4 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Wed, 27 Nov 2024 01:40:35 +0500 Subject: [PATCH 11/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20logout=20=D0=B8=20=D1=81=D0=BC=D0=B5=D0=BD=D1=83=20?= =?UTF-8?q?=D0=BF=D0=B0=D1=80=D0=BE=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fin/spr/auth/JwtAuthenticationFilter.java | 9 ++++++++- .../java/com/fin/spr/auth/UserController.java | 15 ++++++++++---- .../security/ChangePasswordPayload.java | 7 +++++++ .../InvalidTwoFactorCodeException.java | 7 +++++++ .../exceptions/TokenNotFoundException.java | 15 ++++++++++++++ .../spr/exceptions/TokenRevokedException.java | 14 +++++++++++++ .../security/AuthenticationService.java | 19 ++++++++++++++++++ .../fin/spr/services/security/JwtService.java | 2 ++ .../spr/services/security/TokenService.java | 20 +++++++++++++++++++ 9 files changed, 103 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java create mode 100644 src/main/java/com/fin/spr/exceptions/InvalidTwoFactorCodeException.java create mode 100644 src/main/java/com/fin/spr/exceptions/TokenNotFoundException.java create mode 100644 src/main/java/com/fin/spr/exceptions/TokenRevokedException.java create mode 100644 src/main/java/com/fin/spr/services/security/TokenService.java diff --git a/src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java b/src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java index 267183c..16988c0 100644 --- a/src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java +++ b/src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java @@ -1,6 +1,8 @@ package com.fin.spr.auth; +import com.fin.spr.exceptions.TokenRevokedException; import com.fin.spr.services.security.JwtService; +import com.fin.spr.services.security.TokenService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -23,13 +25,15 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { public static final String BEARER_PREFIX = "Bearer "; private final JwtService jwtService; + private final TokenService tokenService; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) throws ServletException, IOException { + @NonNull FilterChain filterChain) + throws ServletException, IOException, TokenRevokedException { var authHeader = request.getHeader(HttpHeaders.AUTHORIZATION); if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) { @@ -38,6 +42,9 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, } var jwt = authHeader.substring(BEARER_PREFIX.length()); + + if (tokenService.isTokenRevoked(jwt)) throw new TokenRevokedException(jwt); + var userLogin = jwtService.extractUserLogin(jwt); var userDetails = userDetailsService.loadUserByUsername(userLogin); diff --git a/src/main/java/com/fin/spr/auth/UserController.java b/src/main/java/com/fin/spr/auth/UserController.java index 8b98adb..cd8ea71 100644 --- a/src/main/java/com/fin/spr/auth/UserController.java +++ b/src/main/java/com/fin/spr/auth/UserController.java @@ -1,15 +1,13 @@ package com.fin.spr.auth; import com.fin.spr.controllers.payload.security.AuthenticationPayload; +import com.fin.spr.controllers.payload.security.ChangePasswordPayload; import com.fin.spr.controllers.payload.security.RegistrationPayload; import com.fin.spr.services.security.AuthenticationService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -32,4 +30,13 @@ public ResponseEntity logout(Authentication authentication) { authenticationService.logout(authentication); return ResponseEntity.ok().build(); } + + @PatchMapping("change-password") + public ResponseEntity changePassword( + @RequestBody ChangePasswordPayload changePasswordRequest, + Authentication authentication + ) { + authenticationService.changePassword(changePasswordRequest, authentication); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java b/src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java new file mode 100644 index 0000000..32931d4 --- /dev/null +++ b/src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java @@ -0,0 +1,7 @@ +package com.fin.spr.controllers.payload.security; + +public record ChangePasswordPayload ( + String newPassword, + String twoFactorCode +){ +} diff --git a/src/main/java/com/fin/spr/exceptions/InvalidTwoFactorCodeException.java b/src/main/java/com/fin/spr/exceptions/InvalidTwoFactorCodeException.java new file mode 100644 index 0000000..e9b212d --- /dev/null +++ b/src/main/java/com/fin/spr/exceptions/InvalidTwoFactorCodeException.java @@ -0,0 +1,7 @@ +package com.fin.spr.exceptions; + +public class InvalidTwoFactorCodeException extends RuntimeException { + public InvalidTwoFactorCodeException() { + super("two-factor.code_invalid"); + } +} diff --git a/src/main/java/com/fin/spr/exceptions/TokenNotFoundException.java b/src/main/java/com/fin/spr/exceptions/TokenNotFoundException.java new file mode 100644 index 0000000..914e005 --- /dev/null +++ b/src/main/java/com/fin/spr/exceptions/TokenNotFoundException.java @@ -0,0 +1,15 @@ +package com.fin.spr.exceptions; + + +import lombok.Getter; + +@Getter +public class TokenNotFoundException extends RuntimeException +{ + private final String token; + + public TokenNotFoundException(String token) { + super("token.not_found"); + this.token=token; + } +} diff --git a/src/main/java/com/fin/spr/exceptions/TokenRevokedException.java b/src/main/java/com/fin/spr/exceptions/TokenRevokedException.java new file mode 100644 index 0000000..f448def --- /dev/null +++ b/src/main/java/com/fin/spr/exceptions/TokenRevokedException.java @@ -0,0 +1,14 @@ +package com.fin.spr.exceptions; + +import lombok.Getter; + +@Getter +public class TokenRevokedException extends RuntimeException { + + private final String token; + + public TokenRevokedException(String token) { + super("token.is_revoked"); + this.token = token; + } +} diff --git a/src/main/java/com/fin/spr/services/security/AuthenticationService.java b/src/main/java/com/fin/spr/services/security/AuthenticationService.java index e3c2539..4a54ab3 100644 --- a/src/main/java/com/fin/spr/services/security/AuthenticationService.java +++ b/src/main/java/com/fin/spr/services/security/AuthenticationService.java @@ -3,7 +3,9 @@ import com.fin.spr.auth.JwtAuthenticationResponse; import com.fin.spr.auth.UserDetails; import com.fin.spr.controllers.payload.security.AuthenticationPayload; +import com.fin.spr.controllers.payload.security.ChangePasswordPayload; import com.fin.spr.controllers.payload.security.RegistrationPayload; +import com.fin.spr.exceptions.InvalidTwoFactorCodeException; import com.fin.spr.exceptions.UserAlreadyRegisterException; import com.fin.spr.exceptions.UserNotFoundException; import com.fin.spr.models.security.Role; @@ -81,7 +83,24 @@ public JwtAuthenticationResponse login(@NotNull AuthenticationPayload authentica public void logout(@NotNull Authentication authentication) { var userDetails = (UserDetails) authentication.getPrincipal(); + var user = userDetails.getUser(); + var tokens = tokenRepository.findAllByUserAndRevoked(user, false); + tokens.forEach(token -> token.setRevoked(true)); + tokenRepository.saveAll(tokens); + } + + public void changePassword(@NotNull ChangePasswordPayload changePasswordPayload, + @NotNull Authentication authentication) { + if (!changePasswordPayload.twoFactorCode().equals("0000")) { + throw new InvalidTwoFactorCodeException(); + } + + var userDetails = (UserDetails) authentication.getPrincipal(); + var user = userDetails.getUser(); + + user.setHashedPassword(passwordEncoder.encode(changePasswordPayload.newPassword())); + userRepository.save(user); } } diff --git a/src/main/java/com/fin/spr/services/security/JwtService.java b/src/main/java/com/fin/spr/services/security/JwtService.java index a0eddc6..6ae24f8 100644 --- a/src/main/java/com/fin/spr/services/security/JwtService.java +++ b/src/main/java/com/fin/spr/services/security/JwtService.java @@ -66,4 +66,6 @@ private Claims extractAllClaims(String token) { .parseSignedClaims(token) .getPayload(); } + + } diff --git a/src/main/java/com/fin/spr/services/security/TokenService.java b/src/main/java/com/fin/spr/services/security/TokenService.java new file mode 100644 index 0000000..5449037 --- /dev/null +++ b/src/main/java/com/fin/spr/services/security/TokenService.java @@ -0,0 +1,20 @@ +package com.fin.spr.services.security; + +import com.fin.spr.exceptions.TokenNotFoundException; +import com.fin.spr.repository.security.TokenRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TokenService { + + private final TokenRepository tokenRepository; + + public boolean isTokenRevoked(String token) { + return tokenRepository.findByToken(token) + .orElseThrow(() -> new TokenNotFoundException(token)) + .isRevoked(); + } +} + From b69fd3772cb463988304cf8a93c048ef328d2b84 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Wed, 27 Nov 2024 01:41:11 +0500 Subject: [PATCH 12/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B5=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D1=82=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=B0=D0=B4=D0=BC=D0=B8=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/fin/spr/controllers/CategoryController.java | 4 ++++ src/main/java/com/fin/spr/services/CategoryService.java | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/com/fin/spr/controllers/CategoryController.java b/src/main/java/com/fin/spr/controllers/CategoryController.java index 55196c6..a61095f 100644 --- a/src/main/java/com/fin/spr/controllers/CategoryController.java +++ b/src/main/java/com/fin/spr/controllers/CategoryController.java @@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -74,6 +75,7 @@ public ResponseEntity getCategoryById(@PathVariable Integer id) { * Returns HTTP 409 if the category already exists. */ @PostMapping + @PreAuthorize("hasAuthority('ADMIN')") @Override public ResponseEntity createCategory(@RequestBody Category category) { try { @@ -92,6 +94,7 @@ public ResponseEntity createCategory(@RequestBody Category category) { * @return a ResponseEntity containing the updated category or an HTTP 404 status code if not found. */ @PutMapping("/{id}") + @PreAuthorize("hasAuthority('ADMIN')") @Override public ResponseEntity updateCategory(@PathVariable Integer id, @RequestBody Category category) { boolean updated = categoryService.updateCategory(id, category); @@ -106,6 +109,7 @@ public ResponseEntity updateCategory(@PathVariable Integer id, @Reques * Returns HTTP 204 if the category was deleted, or HTTP 404 if not found. */ @DeleteMapping("/{id}") + @PreAuthorize("hasAuthority('ADMIN')") @Override public ResponseEntity deleteCategory(@PathVariable Integer id) { boolean deleted = categoryService.deleteCategory(id); diff --git a/src/main/java/com/fin/spr/services/CategoryService.java b/src/main/java/com/fin/spr/services/CategoryService.java index db90ee9..3634ac1 100644 --- a/src/main/java/com/fin/spr/services/CategoryService.java +++ b/src/main/java/com/fin/spr/services/CategoryService.java @@ -7,6 +7,7 @@ import com.fin.spr.repository.history.CategoryHistory; import com.fin.spr.storage.InMemoryStorage; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; import java.util.List; @@ -61,6 +62,7 @@ public Optional getCategoryById(Integer id) { * * @param category the {@link Category} entity to be created */ + @Override public void createCategory(Category category) { categoryStorage.create(category.getId(), category); From 4633143fb0b776f27cf86f4bdd723582c57e4a20 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Wed, 27 Nov 2024 02:55:05 +0500 Subject: [PATCH 13/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E=20=D0=B2=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/changelog/db.changelog-test.yaml | 9 +++++ .../db/changelog/init-security-changelog.sql | 9 +++++ .../java/com/fin/spr/BaseIntegrationTest.java | 38 +++++++++++++++++-- .../CategoryControllerIntegrationTest.java | 23 +++++++---- .../EventControllerIntegrationTest.java | 29 ++++++++++---- .../LocationControllerIntegrationTest.java | 20 +++++++--- src/test/resources/application-test.yml | 1 + 7 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 src/main/resources/db/changelog/db.changelog-test.yaml create mode 100644 src/main/resources/db/changelog/init-security-changelog.sql diff --git a/src/main/resources/db/changelog/db.changelog-test.yaml b/src/main/resources/db/changelog/db.changelog-test.yaml new file mode 100644 index 0000000..eeb6c14 --- /dev/null +++ b/src/main/resources/db/changelog/db.changelog-test.yaml @@ -0,0 +1,9 @@ +databaseChangeLog: + - include: + file: db/changelog/kudago-changelog.sql + - include: + file: db/changelog/security-changelog.sql + - include: + file: db/changelog/security-tokens-changelog.sql + - include: + file: db/changelog/init-security-changelog.sql \ No newline at end of file diff --git a/src/main/resources/db/changelog/init-security-changelog.sql b/src/main/resources/db/changelog/init-security-changelog.sql new file mode 100644 index 0000000..233aa2b --- /dev/null +++ b/src/main/resources/db/changelog/init-security-changelog.sql @@ -0,0 +1,9 @@ +--liquibase formatted sql + +--changeset alexandergarifullin:1 +INSERT INTO security.t_users(c_name, c_login, c_hashed_password, c_role) +VALUES ('Test User', 'user', '$2a$10$kjWwIGpZ2EjGl5edbc9ceuNE5ceP3vdtiPqoAUgCdHtk4on97uvOC', 'USER'); + +--changeset alexandergarifullin:2 +INSERT INTO security.t_users(c_name, c_login, c_hashed_password, c_role) +VALUES ('Test Admin', 'admin', '$2a$10$kjWwIGpZ2EjGl5edbc9ceuNE5ceP3vdtiPqoAUgCdHtk4on97uvOC', 'ADMIN'); \ No newline at end of file diff --git a/src/test/java/com/fin/spr/BaseIntegrationTest.java b/src/test/java/com/fin/spr/BaseIntegrationTest.java index 93d1c58..4c67dd1 100644 --- a/src/test/java/com/fin/spr/BaseIntegrationTest.java +++ b/src/test/java/com/fin/spr/BaseIntegrationTest.java @@ -1,12 +1,15 @@ package com.fin.spr; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fin.spr.controllers.payload.security.AuthenticationPayload; +import com.fin.spr.services.security.AuthenticationService; import liquibase.Contexts; import liquibase.Liquibase; import liquibase.database.Database; import liquibase.database.jvm.JdbcConnection; import liquibase.exception.LiquibaseException; import liquibase.resource.ClassLoaderResourceAccessor; +import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -31,6 +34,24 @@ public abstract class BaseIntegrationTest { @Autowired protected ObjectMapper objectMapper; + @Autowired + protected AuthenticationService authenticationService; + + protected static String userBearerToken; + protected static String adminBearerToken; + + protected static final AuthenticationPayload userRequest = new AuthenticationPayload( + "user", + "password", + false + ); + + protected static final AuthenticationPayload adminRequest = new AuthenticationPayload( + "admin", + "password", + false + ); + void setUp() { postgreSQLContainer = new PostgreSQLContainer<>("postgres:17") .withDatabaseName("testdb") @@ -61,10 +82,21 @@ private void runLiquibaseMigrations(Connection connection) throws LiquibaseExcep database.setConnection(new JdbcConnection(connection)); // Указываем основной файл миграций - try (Liquibase liquibase = new Liquibase("db/changelog/db.changelog-master.yaml", - new ClassLoaderResourceAccessor(), - database)) { + String changeLogFile = System.getProperty("spring.liquibase.change-log", "db/changelog/db.changelog-master.yaml"); + + try (Liquibase liquibase = new Liquibase(changeLogFile, new ClassLoaderResourceAccessor(), database)) { liquibase.update(new Contexts()); } } + + @BeforeEach + public void getToken() { + if (userBearerToken == null) { + userBearerToken = "Bearer %s".formatted(authenticationService.login(userRequest).token()); + } + + if (adminBearerToken == null) { + adminBearerToken = "Bearer %s".formatted(authenticationService.login(adminRequest).token()); + } + } } diff --git a/src/test/java/com/fin/spr/controllers/CategoryControllerIntegrationTest.java b/src/test/java/com/fin/spr/controllers/CategoryControllerIntegrationTest.java index 6d3db7e..c6169c2 100644 --- a/src/test/java/com/fin/spr/controllers/CategoryControllerIntegrationTest.java +++ b/src/test/java/com/fin/spr/controllers/CategoryControllerIntegrationTest.java @@ -1,5 +1,6 @@ package com.fin.spr.controllers; +import com.fin.spr.BaseIntegrationTest; import com.fin.spr.exceptions.EntityAlreadyExistsException; import com.fin.spr.models.Category; import com.fin.spr.services.CategoryService; @@ -24,7 +25,7 @@ @SpringBootTest @AutoConfigureMockMvc -public class CategoryControllerIntegrationTest { +public class CategoryControllerIntegrationTest extends BaseIntegrationTest { @Autowired private MockMvc mockMvc; @@ -49,8 +50,8 @@ public void setup() { @Test public void testGetAllCategories() throws Exception { Mockito.when(categoryService.getAllCategories()).thenReturn(Arrays.asList(testCategory1, testCategory2)); - - mockMvc.perform(get(BASE_URL)) + mockMvc.perform(get(BASE_URL) + .header("Authorization", userBearerToken)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$[0].name").value("Museums")) @@ -61,7 +62,8 @@ public void testGetAllCategories() throws Exception { public void testGetCategoryById_Success() throws Exception { Mockito.when(categoryService.getCategoryById(1)).thenReturn(Optional.of(testCategory1)); - mockMvc.perform(get(BASE_URL + "/1")) + mockMvc.perform(get(BASE_URL + "/1") + .header("Authorization", userBearerToken)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.name").value("Museums")) @@ -72,7 +74,8 @@ public void testGetCategoryById_Success() throws Exception { public void testGetCategoryById_NotFound() throws Exception { Mockito.when(categoryService.getCategoryById(99)).thenReturn(Optional.empty()); - mockMvc.perform(get(BASE_URL + "/99")) + mockMvc.perform(get(BASE_URL + "/99") + .header("Authorization", userBearerToken)) .andExpect(status().isNotFound()); } @@ -81,6 +84,7 @@ public void testCreateCategory_Success() throws Exception { Mockito.doNothing().when(categoryService).createCategory(any(Category.class)); mockMvc.perform(post(BASE_URL) + .header("Authorization", adminBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(testCategory1))) .andExpect(status().isCreated()) @@ -93,6 +97,7 @@ public void testCreateCategory_Conflict() throws Exception { Mockito.doThrow(new EntityAlreadyExistsException("Category already exists")).when(categoryService).createCategory(any(Category.class)); mockMvc.perform(post(BASE_URL) + .header("Authorization", adminBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(testCategory1))) .andExpect(status().isConflict()); @@ -103,6 +108,7 @@ public void testUpdateCategory_Success() throws Exception { Mockito.when(categoryService.updateCategory(eq(1), any(Category.class))).thenReturn(true); mockMvc.perform(put(BASE_URL + "/1") + .header("Authorization", adminBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(testCategory1))) .andExpect(status().isOk()) @@ -115,6 +121,7 @@ public void testUpdateCategory_NotFound() throws Exception { Mockito.when(categoryService.updateCategory(eq(99), any(Category.class))).thenReturn(false); mockMvc.perform(put(BASE_URL + "/99") + .header("Authorization", adminBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(testCategory1))) .andExpect(status().isNotFound()); @@ -124,7 +131,8 @@ public void testUpdateCategory_NotFound() throws Exception { public void testDeleteCategory_Success() throws Exception { Mockito.when(categoryService.deleteCategory(1)).thenReturn(true); - mockMvc.perform(delete(BASE_URL + "/1")) + mockMvc.perform(delete(BASE_URL + "/1") + .header("Authorization", adminBearerToken)) .andExpect(status().isNoContent()); } @@ -132,7 +140,8 @@ public void testDeleteCategory_Success() throws Exception { public void testDeleteCategory_NotFound() throws Exception { Mockito.when(categoryService.deleteCategory(99)).thenReturn(false); - mockMvc.perform(delete(BASE_URL + "/99")) + mockMvc.perform(delete(BASE_URL + "/99") + .header("Authorization", adminBearerToken)) .andExpect(status().isNotFound()); } } diff --git a/src/test/java/com/fin/spr/controllers/EventControllerIntegrationTest.java b/src/test/java/com/fin/spr/controllers/EventControllerIntegrationTest.java index 03272f2..998dd72 100644 --- a/src/test/java/com/fin/spr/controllers/EventControllerIntegrationTest.java +++ b/src/test/java/com/fin/spr/controllers/EventControllerIntegrationTest.java @@ -72,7 +72,8 @@ public void getAllEvents_notEmpty() throws Exception { var createdEvent = eventService.createEvent(eventPayload.name(), eventPayload.startDate(), eventPayload.price(), eventPayload.free(), createdLocation.getId()); - var mvcResponse = mockMvc.perform(get(events_uri)) + var mvcResponse = mockMvc.perform(get(events_uri) + .header("Authorization", userBearerToken)) .andExpectAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON)) @@ -95,7 +96,8 @@ void getEventById_success() throws Exception { var createdEvent = eventService.createEvent(eventPayload.name(), eventPayload.startDate(), eventPayload.price(), eventPayload.free(), createdLocation.getId()); - var mvcResponse = mockMvc.perform(get(events_uri + "/{id}", createdEvent.getId())) + var mvcResponse = mockMvc.perform(get(events_uri + "/{id}", createdEvent.getId()) + .header("Authorization", userBearerToken)) .andExpectAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON)) @@ -112,7 +114,8 @@ void getEventById_success() throws Exception { @Test void getEventById_notFound() throws Exception { - mockMvc.perform(get(events_uri + "/88")) + mockMvc.perform(get(events_uri + "/88") + .header("Authorization", userBearerToken)) .andExpectAll( status().isNotFound(), content().contentType(MediaType.APPLICATION_PROBLEM_JSON) @@ -128,6 +131,7 @@ void createEvent_success() throws Exception { eventPayload.free(), createdLocation.getId()); var mvcResponse = mockMvc.perform(post(events_uri) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(eventPayload))) .andExpectAll( @@ -168,6 +172,7 @@ private static Stream invalidEventPayloads() { @MethodSource("invalidEventPayloads") void createEvent_badRequest(EventPayload badEventPayload) throws Exception { mockMvc.perform(post(events_uri) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(badEventPayload))) .andExpectAll( @@ -180,6 +185,7 @@ void createEvent_locationNotFound() throws Exception { eventPayload.free(), -1L); var mvcResponse = mockMvc.perform(post(events_uri) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(eventPayload))) .andExpectAll( @@ -200,6 +206,7 @@ void updateEvent_success() throws Exception { createdLocation.getId()); var mvcResponse = mockMvc.perform(put(events_uri + "/{id}", createdEvent.getId()) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(eventPayload))) .andExpectAll( @@ -225,6 +232,7 @@ void updateEvent_badRequest(EventPayload badEventPayload) throws Exception { eventPayload.price(), eventPayload.free(), createdLocation.getId()); mockMvc.perform(put(events_uri + "/{id}", createdEvent.getId()) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(badEventPayload))) .andExpectAll( @@ -234,6 +242,7 @@ void updateEvent_badRequest(EventPayload badEventPayload) throws Exception { @Test void updateEvent_notFound() throws Exception { var mvcResponse = mockMvc.perform(put(events_uri + "/8888") + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(eventPayload))) .andExpectAll( @@ -248,7 +257,8 @@ void deleteEvent_success() throws Exception { var createdEvent = eventService.createEvent(eventPayload.name(), eventPayload.startDate(), eventPayload.price(), eventPayload.free(), createdLocation.getId()); - mockMvc.perform(delete(events_uri + "/" + createdEvent.getId())) + mockMvc.perform(delete(events_uri + "/" + createdEvent.getId()) + .header("Authorization", userBearerToken)) .andExpect(status().isNoContent()); assertThatThrownBy(() -> eventService.getEventById(createdEvent.getId())) @@ -258,7 +268,8 @@ void deleteEvent_success() throws Exception { @Test void deleteEvent_notFound() throws Exception { - mockMvc.perform(delete(events_uri + "/88888")) + mockMvc.perform(delete(events_uri + "/88888") + .header("Authorization", userBearerToken)) .andExpectAll( status().isNotFound(), content().contentType(MediaType.APPLICATION_PROBLEM_JSON)); @@ -270,7 +281,8 @@ void getLocationWithEvents_success() throws Exception { var createdEvent = eventService.createEvent(eventPayload.name(), eventPayload.startDate(), eventPayload.price(), eventPayload.free(), createdLocation.getId()); - var mvcResponse = mockMvc.perform(get(locations_uri + "/" + createdLocation.getId())) + var mvcResponse = mockMvc.perform(get(locations_uri + "/" + createdLocation.getId()) + .header("Authorization", userBearerToken)) .andExpectAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON)) @@ -292,6 +304,7 @@ void searchEvent_withParams() throws Exception { eventPayload.price(), eventPayload.free(), createdLocation.getId()); var mvcResponse = mockMvc.perform(get(events_uri + "/filter") + .header("Authorization", userBearerToken) .param("name", createdEvent.getName()) .param("fromDate", createdEvent.getStartDate().minus(Duration.ofDays(1)).toString()) .param("toDate", createdEvent.getStartDate().plus(Duration.ofDays(1)).toString())) @@ -317,7 +330,8 @@ void searchEvent_withNullParams() throws Exception { var createdEvent = eventService.createEvent(eventPayload.name(), eventPayload.startDate(), eventPayload.price(), eventPayload.free(), createdLocation.getId()); - var mvcResponse = mockMvc.perform(get(events_uri + "/filter")) + var mvcResponse = mockMvc.perform(get(events_uri + "/filter") + .header("Authorization", userBearerToken)) .andExpectAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON)) @@ -341,6 +355,7 @@ void searchEvent_returnEmptyList() throws Exception { eventPayload.price(), eventPayload.free(), createdLocation.getId()); var mvcResponse = mockMvc.perform(get(events_uri + "/filter") + .header("Authorization", userBearerToken) .param("name", createdEvent.getName()) .param("fromDate", createdEvent.getStartDate().plus(Duration.ofDays(10)).toString())) .andExpectAll( diff --git a/src/test/java/com/fin/spr/controllers/LocationControllerIntegrationTest.java b/src/test/java/com/fin/spr/controllers/LocationControllerIntegrationTest.java index 8db3d71..6062fa3 100644 --- a/src/test/java/com/fin/spr/controllers/LocationControllerIntegrationTest.java +++ b/src/test/java/com/fin/spr/controllers/LocationControllerIntegrationTest.java @@ -45,7 +45,8 @@ void getAllLocations_notEmpty() throws Exception { LocationPayload payload = new LocationPayload("slug1", "Test Location"); var createdLocation = locationService.createLocation(payload.slug(), payload.name()); - var mvcResponse = mockMvc.perform(get(uri)) + var mvcResponse = mockMvc.perform(get(uri) + .header("Authorization", userBearerToken)) .andExpectAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON)) @@ -67,7 +68,8 @@ void getLocationById_success() throws Exception { LocationPayload payload = new LocationPayload("slug1", "Test Location"); var createdLocation = locationService.createLocation(payload.slug(), payload.name()); - var mvcResponse = mockMvc.perform(get(uri + "/" + createdLocation.getId())) + var mvcResponse = mockMvc.perform(get(uri + "/" + createdLocation.getId()) + .header("Authorization", userBearerToken)) .andExpectAll( status().isOk(), content().contentType(MediaType.APPLICATION_JSON)) @@ -81,7 +83,8 @@ void getLocationById_success() throws Exception { @Test void getLocationById_notFound() throws Exception { - mockMvc.perform(get(uri + "/88")) + mockMvc.perform(get(uri + "/88") + .header("Authorization", userBearerToken)) .andExpectAll( status().isNotFound(), content().contentType(MediaType.APPLICATION_PROBLEM_JSON) @@ -93,6 +96,7 @@ void createLocation_success() throws Exception{ LocationPayload payload = new LocationPayload("slug1", "Test Location"); var mvcResponse = mockMvc.perform(post(uri) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(payload))) .andExpectAll( @@ -129,6 +133,7 @@ private static Stream invalidLocationPayloads() { @MethodSource("invalidLocationPayloads") void createLocation_badRequest(LocationPayload locationPayload) throws Exception { mockMvc.perform(post(uri) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(locationPayload))) .andExpectAll( @@ -142,6 +147,7 @@ public void updateLocation_success() throws Exception{ LocationPayload newPayload = new LocationPayload("slug2", "New Location"); var mvcResponse = mockMvc.perform(put(uri + "/{id}", oldLocation.getId()) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(newPayload))) .andExpectAll( @@ -163,6 +169,7 @@ void updateLocation_badRequest(LocationPayload locationPayload) throws Exception var oldLocation = locationService.createLocation("slug1", "Old Location"); mockMvc.perform(put(uri + "/{id}", oldLocation.getId()) + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(locationPayload))) .andExpectAll( @@ -174,6 +181,7 @@ void updateLocation_notFound() throws Exception { LocationPayload newPayload = new LocationPayload("slug2", "New Location"); var mvcResponse = mockMvc.perform(put(uri + "/88") + .header("Authorization", userBearerToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(newPayload))) .andExpectAll( @@ -185,7 +193,8 @@ void updateLocation_notFound() throws Exception { void deleteLocation_success() throws Exception { var oldLocation = locationService.createLocation("slug1", "Old Location"); - mockMvc.perform(delete(uri + "/" + oldLocation.getId())) + mockMvc.perform(delete(uri + "/" + oldLocation.getId()) + .header("Authorization", userBearerToken)) .andExpect(status().isNoContent()); assertThatThrownBy(() -> locationService.getLocationById(oldLocation.getId())) @@ -195,7 +204,8 @@ void deleteLocation_success() throws Exception { @Test void deleteLocation_notFound() throws Exception { - mockMvc.perform(delete(uri + "/88")) + mockMvc.perform(delete(uri + "/88") + .header("Authorization", userBearerToken)) .andExpectAll( status().isNotFound(), content().contentType(MediaType.APPLICATION_PROBLEM_JSON)); diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index e378f53..4b8296b 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -11,4 +11,5 @@ spring: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect liquibase: + change-log: classpath:db/changelog/db.changelog-test.yaml enabled: true From 6f0e9e7f1f6296595e09b18ba3ec18ac215a39f8 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Wed, 27 Nov 2024 04:11:25 +0500 Subject: [PATCH 14/17] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D0=BE=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D0=B8,=20=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0,=20=D0=BB=D0=BE=D0=B3=D0=B0=D1=83=D1=82=D0=B0=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=B1=D1=80=D0=BE=D1=81=D0=B0=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D0=BE=D0=BB=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/AuthenticationPayload.java | 3 + .../security/ChangePasswordPayload.java | 3 + .../payload/security/RegistrationPayload.java | 3 + .../com/fin/spr/models/security/Token.java | 1 + .../com/fin/spr/models/security/User.java | 1 + .../auth/UserControllerIntegrationTest.java | 222 ++++++++++++++++++ 6 files changed, 233 insertions(+) create mode 100644 src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java diff --git a/src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java b/src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java index 661593a..a4819c3 100644 --- a/src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java +++ b/src/main/java/com/fin/spr/controllers/payload/security/AuthenticationPayload.java @@ -1,5 +1,8 @@ package com.fin.spr.controllers.payload.security; +import lombok.Builder; + +@Builder public record AuthenticationPayload ( String login, String password, diff --git a/src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java b/src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java index 32931d4..ab4075b 100644 --- a/src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java +++ b/src/main/java/com/fin/spr/controllers/payload/security/ChangePasswordPayload.java @@ -1,5 +1,8 @@ package com.fin.spr.controllers.payload.security; +import lombok.Builder; + +@Builder public record ChangePasswordPayload ( String newPassword, String twoFactorCode diff --git a/src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java b/src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java index 2b2f873..4459e3d 100644 --- a/src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java +++ b/src/main/java/com/fin/spr/controllers/payload/security/RegistrationPayload.java @@ -1,5 +1,8 @@ package com.fin.spr.controllers.payload.security; +import lombok.Builder; + +@Builder public record RegistrationPayload( String name, String login, diff --git a/src/main/java/com/fin/spr/models/security/Token.java b/src/main/java/com/fin/spr/models/security/Token.java index e457db2..8cc5773 100644 --- a/src/main/java/com/fin/spr/models/security/Token.java +++ b/src/main/java/com/fin/spr/models/security/Token.java @@ -8,6 +8,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(exclude = "user") @Entity @Table(name = "t_tokens", schema = "security") public class Token { diff --git a/src/main/java/com/fin/spr/models/security/User.java b/src/main/java/com/fin/spr/models/security/User.java index 8f0f321..1e95405 100644 --- a/src/main/java/com/fin/spr/models/security/User.java +++ b/src/main/java/com/fin/spr/models/security/User.java @@ -15,6 +15,7 @@ @Getter @NoArgsConstructor @AllArgsConstructor +@EqualsAndHashCode(exclude = "tokens") @Entity @Table(name = "t_users", schema = "security") public class User { diff --git a/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java b/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java new file mode 100644 index 0000000..3890793 --- /dev/null +++ b/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java @@ -0,0 +1,222 @@ +package com.fin.spr.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fin.spr.BaseIntegrationTest; +import com.fin.spr.controllers.payload.security.AuthenticationPayload; +import com.fin.spr.controllers.payload.security.ChangePasswordPayload; +import com.fin.spr.controllers.payload.security.RegistrationPayload; +import com.fin.spr.models.Event; +import com.fin.spr.repository.security.TokenRepository; +import com.fin.spr.repository.security.UserRepository; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; + +import java.io.UnsupportedEncodingException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class UserControllerIntegrationTest extends BaseIntegrationTest { + + private static final String uri = "/api/v1/auth"; + + @Autowired + private UserRepository userRepository; + + @Autowired + private TokenRepository tokenRepository; + + @AfterEach + void cleanDatabase() { + userRepository.deleteAll(); + tokenRepository.deleteAll(); + } + + @Test + public void register_success() throws Exception { + RegistrationPayload payload = RegistrationPayload.builder() + .login("register-login") + .name("register-name") + .password("register-password") + .build(); + + var jwtResponse = register(payload); + + var user = userRepository.findByLogin("register-login"); + var token = tokenRepository.findByToken(jwtResponse.token()); + + assertAll( + () -> assertThat(jwtResponse.token()).isNotEmpty(), + + () -> assertThat(token.isPresent()).isTrue(), + () -> assertThat(token.get().isRevoked()).isFalse(), + + () -> assertThat(user.isPresent()).isTrue() + ); + } + + @Test + public void login_success() throws Exception { + RegistrationPayload payloadToRegister = RegistrationPayload.builder() + .login("register-login") + .name("register-name") + .password("register-password") + .build(); + + var jwtResponse = register(payloadToRegister); + + var payloadToLogin = AuthenticationPayload.builder() + .login("register-login") + .password("register-password") + .rememberMe(true) + .build(); + + var response = mockMvc.perform(post(uri + "/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payloadToLogin))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var jwtFromLogin = objectMapper.readValue(response.getContentAsString(), JwtAuthenticationResponse.class); + + var oldToken = tokenRepository.findByToken(jwtResponse.token()); + var newToken = tokenRepository.findByToken(jwtFromLogin.token()); + + assertAll( + () -> assertThat(jwtFromLogin.token()).isNotNull(), + + () -> assertThat(jwtResponse.token()).isNotNull(), + () -> assertThat(jwtFromLogin.token()).isNotEqualTo(jwtResponse.token()), + + () -> assertThat(oldToken.isPresent()).isTrue(), + () -> assertThat(newToken.isPresent()).isTrue(), + + () -> assertThat(oldToken.get().isRevoked()).isTrue(), + () -> assertThat(newToken.get().isRevoked()).isFalse(), + + () -> assertThat(oldToken.get()).isNotEqualTo(newToken.get()) + ); + } + + @Test + public void logout_success() throws Exception { + RegistrationPayload payloadToRegister = RegistrationPayload.builder() + .login("register-login") + .name("register-name") + .password("register-password") + .build(); + + var jwtResponse = register(payloadToRegister); + + mockMvc.perform(post(uri + "/logout") + .header("Authorization", "Bearer %s".formatted(jwtResponse.token()))) + .andExpectAll( + status().isOk()) + .andReturn() + .getResponse(); + + var oldToken = tokenRepository.findByToken(jwtResponse.token()); + + assertAll( + () -> assertThat(oldToken.isPresent()).isTrue(), + () -> assertThat(oldToken.get().isRevoked()).isTrue() + ); + } + + @Test + public void changePassword_success() throws Exception { + RegistrationPayload payloadToRegister = RegistrationPayload.builder() + .login("register-login") + .name("register-name") + .password("register-password") + .build(); + + var jwtResponse = register(payloadToRegister); + + var oldUser = userRepository.findByLogin("register-login"); + + var changePasswordPayload = ChangePasswordPayload.builder() + .newPassword("new-password") + .twoFactorCode("0000") + .build(); + + mockMvc.perform(patch(uri + "/change-password") + .header("Authorization", "Bearer %s".formatted(jwtResponse.token())) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(changePasswordPayload))) + .andExpectAll( + status().isOk()) + .andReturn() + .getResponse(); + + var newUser = userRepository.findByLogin("register-login"); + + assertAll( + () -> assertThat(oldUser.isPresent()).isTrue(), + () -> assertThat(newUser.isPresent()).isTrue(), + + () -> assertThat(oldUser.get()).isNotEqualTo(newUser.get()), + () -> assertThat(oldUser.get().getHashedPassword()).isNotEqualTo(newUser.get().getHashedPassword()) + ); + + var payloadToLogin = AuthenticationPayload.builder() + .login("register-login") + .password("new-password") + .rememberMe(true) + .build(); + + var loginResponse = mockMvc.perform(post(uri + "/login") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payloadToLogin))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + var jwtFromLogin = objectMapper.readValue(loginResponse.getContentAsString(), JwtAuthenticationResponse.class); + + var loginUser = userRepository.findByLogin("register-login"); + var loginToken= tokenRepository.findByToken(jwtFromLogin.token()); + var oldToken= tokenRepository.findByToken(jwtResponse.token()); + + assertAll( + () -> assertThat(jwtFromLogin.token()).isNotEmpty(), + + () -> assertThat(loginToken.isPresent()).isTrue(), + () -> assertThat(oldToken.isPresent()).isTrue(), + + () -> assertThat(loginToken.get().isRevoked()).isFalse(), + () -> assertThat(oldToken.get().isRevoked()).isTrue(), + + () -> assertThat(loginToken.get()).isNotEqualTo(oldToken), + + () -> assertThat(loginUser.isPresent()).isTrue(), + () -> assertThat(loginUser.get()).isEqualTo(newUser.get()) + ); + } + + private JwtAuthenticationResponse register(RegistrationPayload payload) throws Exception { + var response = mockMvc.perform(post(uri + "/register") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(payload))) + .andExpectAll( + status().isOk(), + content().contentType(MediaType.APPLICATION_JSON)) + .andReturn() + .getResponse(); + + return objectMapper.readValue(response.getContentAsString(), JwtAuthenticationResponse.class); + } + +} \ No newline at end of file From 9d51fe72dce8cac2e3d577198cc8d568b37a6c97 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Wed, 27 Nov 2024 04:13:15 +0500 Subject: [PATCH 15/17] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=B8=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/fin/spr/config/SecurityConfig.java | 7 ------- src/main/java/com/fin/spr/models/security/User.java | 6 ------ .../com/fin/spr/auth/UserControllerIntegrationTest.java | 4 ---- 3 files changed, 17 deletions(-) diff --git a/src/main/java/com/fin/spr/config/SecurityConfig.java b/src/main/java/com/fin/spr/config/SecurityConfig.java index 308aff4..04032af 100644 --- a/src/main/java/com/fin/spr/config/SecurityConfig.java +++ b/src/main/java/com/fin/spr/config/SecurityConfig.java @@ -1,22 +1,15 @@ package com.fin.spr.config; import com.fin.spr.auth.JwtAuthenticationFilter; -import com.fin.spr.services.security.MyUserDetailsService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; diff --git a/src/main/java/com/fin/spr/models/security/User.java b/src/main/java/com/fin/spr/models/security/User.java index 1e95405..126c6f6 100644 --- a/src/main/java/com/fin/spr/models/security/User.java +++ b/src/main/java/com/fin/spr/models/security/User.java @@ -2,12 +2,6 @@ import jakarta.persistence.*; import lombok.*; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.util.Collection; -import java.util.Collections; import java.util.List; @Builder diff --git a/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java b/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java index 3890793..7bffa43 100644 --- a/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java +++ b/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java @@ -1,20 +1,16 @@ package com.fin.spr.auth; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fin.spr.BaseIntegrationTest; import com.fin.spr.controllers.payload.security.AuthenticationPayload; import com.fin.spr.controllers.payload.security.ChangePasswordPayload; import com.fin.spr.controllers.payload.security.RegistrationPayload; -import com.fin.spr.models.Event; import com.fin.spr.repository.security.TokenRepository; import com.fin.spr.repository.security.UserRepository; -import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; -import java.io.UnsupportedEncodingException; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; From 67366cd830e51bb2c7fc38b38cea3fdb5296a20d Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Wed, 27 Nov 2024 04:15:45 +0500 Subject: [PATCH 16/17] =?UTF-8?q?=D0=A3=D0=B4=D0=B0=D0=BB=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BB=D0=B8=D1=88=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D0=B0=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/fin/spr/config/SecurityConfig.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/fin/spr/config/SecurityConfig.java b/src/main/java/com/fin/spr/config/SecurityConfig.java index 04032af..9ab2377 100644 --- a/src/main/java/com/fin/spr/config/SecurityConfig.java +++ b/src/main/java/com/fin/spr/config/SecurityConfig.java @@ -37,10 +37,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti corsConfiguration.setAllowCredentials(true); return corsConfiguration; })) - // Настройка доступа к конечным точкам .authorizeHttpRequests(request -> request - // Можно указать конкретный путь, * - 1 уровень вложенности, ** - любое количество уровней вложенности - .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login").permitAll() + .requestMatchers("/api/v1/auth/register", "/api/v1/auth/login").permitAll() .requestMatchers("/swagger-ui/**", "/swagger-resources/*", "/v3/api-docs/**").permitAll() .requestMatchers("/endpoint", "/admin/**").hasRole("ADMIN") .anyRequest().authenticated()) From a31b19d7e12b9257236b9a16457d391687325306 Mon Sep 17 00:00:00 2001 From: AlexanderGarifullin Date: Thu, 28 Nov 2024 19:32:27 +0500 Subject: [PATCH 17/17] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D1=83?= =?UTF-8?q?=20=D0=B2=20AssertJ=20=D1=81=20isPresent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/UserControllerIntegrationTest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java b/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java index 7bffa43..8f0ccd3 100644 --- a/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java +++ b/src/test/java/com/fin/spr/auth/UserControllerIntegrationTest.java @@ -51,10 +51,10 @@ public void register_success() throws Exception { assertAll( () -> assertThat(jwtResponse.token()).isNotEmpty(), - () -> assertThat(token.isPresent()).isTrue(), + () -> assertThat(token).isPresent(), () -> assertThat(token.get().isRevoked()).isFalse(), - () -> assertThat(user.isPresent()).isTrue() + () -> assertThat(user).isPresent() ); } @@ -94,8 +94,8 @@ public void login_success() throws Exception { () -> assertThat(jwtResponse.token()).isNotNull(), () -> assertThat(jwtFromLogin.token()).isNotEqualTo(jwtResponse.token()), - () -> assertThat(oldToken.isPresent()).isTrue(), - () -> assertThat(newToken.isPresent()).isTrue(), + () -> assertThat(oldToken).isPresent(), + () -> assertThat(newToken).isPresent(), () -> assertThat(oldToken.get().isRevoked()).isTrue(), () -> assertThat(newToken.get().isRevoked()).isFalse(), @@ -124,7 +124,7 @@ public void logout_success() throws Exception { var oldToken = tokenRepository.findByToken(jwtResponse.token()); assertAll( - () -> assertThat(oldToken.isPresent()).isTrue(), + () -> assertThat(oldToken).isPresent(), () -> assertThat(oldToken.get().isRevoked()).isTrue() ); } @@ -158,8 +158,8 @@ public void changePassword_success() throws Exception { var newUser = userRepository.findByLogin("register-login"); assertAll( - () -> assertThat(oldUser.isPresent()).isTrue(), - () -> assertThat(newUser.isPresent()).isTrue(), + () -> assertThat(oldUser).isPresent(), + () -> assertThat(newUser).isPresent(), () -> assertThat(oldUser.get()).isNotEqualTo(newUser.get()), () -> assertThat(oldUser.get().getHashedPassword()).isNotEqualTo(newUser.get().getHashedPassword()) @@ -189,15 +189,15 @@ public void changePassword_success() throws Exception { assertAll( () -> assertThat(jwtFromLogin.token()).isNotEmpty(), - () -> assertThat(loginToken.isPresent()).isTrue(), - () -> assertThat(oldToken.isPresent()).isTrue(), + () -> assertThat(loginToken).isPresent(), + () -> assertThat(oldToken).isPresent(), () -> assertThat(loginToken.get().isRevoked()).isFalse(), () -> assertThat(oldToken.get().isRevoked()).isTrue(), () -> assertThat(loginToken.get()).isNotEqualTo(oldToken), - () -> assertThat(loginUser.isPresent()).isTrue(), + () -> assertThat(loginUser).isPresent(), () -> assertThat(loginUser.get()).isEqualTo(newUser.get()) ); }