Skip to content
This repository was archived by the owner on Dec 27, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6bc20c9
Добавил spring security dependency
AlexanderGarifullin Nov 25, 2024
49ab3b0
Добавил в БД таблицу User и классы для работы с этой таблицей
AlexanderGarifullin Nov 25, 2024
ca157f1
Добавил логику для регистрации пользователей
AlexanderGarifullin Nov 25, 2024
9b9056e
Добавил библиотеки для jwt токена
AlexanderGarifullin Nov 26, 2024
8405313
Добавил Token в бд
AlexanderGarifullin Nov 26, 2024
9dede45
Сделал возращение jwt токена в контроллере
AlexanderGarifullin Nov 26, 2024
36180ab
Добавил сервис для генерации jwt токена
AlexanderGarifullin Nov 26, 2024
9d85bff
Добавил login пользователей
AlexanderGarifullin Nov 26, 2024
c606e3f
Добавил login по JWT
AlexanderGarifullin Nov 26, 2024
cb31369
Сделал login без jwt токена (logout по jwt)
AlexanderGarifullin Nov 26, 2024
de13694
Добавил logout и смену пароля
AlexanderGarifullin Nov 26, 2024
b69fd37
Добавил ендпоиты для админов
AlexanderGarifullin Nov 26, 2024
4633143
Добавил авторизацию в тесты
AlexanderGarifullin Nov 26, 2024
6f0e9e7
Добавил интеграционные тесты для регистрации, логина, логаута и сброс…
AlexanderGarifullin Nov 26, 2024
9d51fe7
Удалил лишние импорты
AlexanderGarifullin Nov 26, 2024
67366cd
Удалил лишние комментарии
AlexanderGarifullin Nov 26, 2024
a31b19d
Исправил проверку в AssertJ с isPresent
AlexanderGarifullin Nov 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -37,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'
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/com/fin/spr/auth/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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;
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 TokenService tokenService;

private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException, TokenRevokedException {
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());

if (tokenService.isTokenRevoked(jwt)) throw new TokenRevokedException(jwt);

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);
}
}
6 changes: 6 additions & 0 deletions src/main/java/com/fin/spr/auth/JwtAuthenticationResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.fin.spr.auth;

public record JwtAuthenticationResponse(
String token
) {
}
42 changes: 42 additions & 0 deletions src/main/java/com/fin/spr/auth/UserController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/auth")
public class UserController {
private final AuthenticationService authenticationService;

@PostMapping("/register")
public JwtAuthenticationResponse register(@RequestBody RegistrationPayload registrationRequest) {
return authenticationService.register(registrationRequest);
}

@PostMapping("/login")
public JwtAuthenticationResponse login(@RequestBody AuthenticationPayload authenticationPayload) {
return authenticationService.login(authenticationPayload);
}

@PostMapping("/logout")
public ResponseEntity<Void> logout(Authentication authentication) {
authenticationService.logout(authentication);
return ResponseEntity.ok().build();
}

@PatchMapping("change-password")
public ResponseEntity<Void> changePassword(
@RequestBody ChangePasswordPayload changePasswordRequest,
Authentication authentication
) {
authenticationService.changePassword(changePasswordRequest, authentication);
return ResponseEntity.ok().build();
}
}
32 changes: 32 additions & 0 deletions src/main/java/com/fin/spr/auth/UserDetails.java
Original file line number Diff line number Diff line change
@@ -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<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(user.getRole().name()));
}

@Override
public String getPassword() {
return user.getHashedPassword();
}

@Override
public String getUsername() {
return user.getLogin();
}
}
36 changes: 36 additions & 0 deletions src/main/java/com/fin/spr/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
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;

/**
* The {@code AppConfig} class provides the configuration for the application.
* 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}.
*
Expand All @@ -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();
}
}
52 changes: 52 additions & 0 deletions src/main/java/com/fin/spr/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.fin.spr.config;

import com.fin.spr.auth.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
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.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 {

private final JwtAuthenticationFilter jwtAuthFilter;
private final AuthenticationProvider authenticationProvider;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.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
.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())
.sessionManagement(sessionManagementConfigurer ->
sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/fin/spr/controllers/CategoryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,6 +75,7 @@ public ResponseEntity<Category> getCategoryById(@PathVariable Integer id) {
* Returns HTTP 409 if the category already exists.
*/
@PostMapping
@PreAuthorize("hasAuthority('ADMIN')")
@Override
public ResponseEntity<Category> createCategory(@RequestBody Category category) {
try {
Expand All @@ -92,6 +94,7 @@ public ResponseEntity<Category> 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<Category> updateCategory(@PathVariable Integer id, @RequestBody Category category) {
boolean updated = categoryService.updateCategory(id, category);
Expand All @@ -106,6 +109,7 @@ public ResponseEntity<Category> 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<Void> deleteCategory(@PathVariable Integer id) {
boolean deleted = categoryService.deleteCategory(id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.fin.spr.controllers.payload.security;

import lombok.Builder;

@Builder
public record AuthenticationPayload (
String login,
String password,
boolean rememberMe
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.fin.spr.controllers.payload.security;

import lombok.Builder;

@Builder
public record ChangePasswordPayload (
String newPassword,
String twoFactorCode
){
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.fin.spr.controllers.payload.security;

import lombok.Builder;

@Builder
public record RegistrationPayload(
String name,
String login,
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.fin.spr.exceptions;

public class InvalidTwoFactorCodeException extends RuntimeException {
public InvalidTwoFactorCodeException() {
super("two-factor.code_invalid");
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/fin/spr/exceptions/TokenNotFoundException.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
14 changes: 14 additions & 0 deletions src/main/java/com/fin/spr/exceptions/TokenRevokedException.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading