Skip to content
This repository has been archived by the owner on Dec 27, 2024. It is now read-only.

Commit

Permalink
dev/security/ Добавил работу с пользователями и security (#5)
Browse files Browse the repository at this point in the history
* dev/security/ Добавил зависимости для security и jwt

* dev/security/ Добавил responses и payloads для security

* dev/security/ Добавил контроллер для security

* dev/security/ Добавил ошибки для security

* dev/security/ Удалил роль admin

* dev/security/ Добавил сервис для UserController

* dev/security/ Добавил сервис для jwt

* dev/security/ Добавил бины для security

* dev/security/ Добавил UserDetails

* dev/security/ Добавил spring-boot-starter-validation

* dev/security/ Добавил ошибки для токена jwt

* dev/security/ Добавил фильтр с jwt

* dev/security/ Добавил TokenService

* dev/security/ Добавил security filter

* dev/security/ Добавил бины для security

* dev/security/ Добавил jwt.yml в gitignore

* dev/security/ Добавил jwt.yml в проект

* dev/security/ Проверка gitignore

* dev/security/ попытка добавить файл из gitignore

* dev/security/ Исправил код по checkstyle

* dev/security/ Поменял русскую букву c на английскую

* dev/security/ Поменял русскую букву c на английскую в БД и удалил лишнее ограничение not null

* dev/security/ Для login теперь нужен jwt токен

* dev/security/ Удалил поле c_time в таблицах бд

* dev/security/ Разрешил /auth/login всем пользователям

* dev/security/ Исправил классы нет под checkStyle

* dev/security/ Написал ControllerAdvice для security

* dev/security/ Написал unit тест для UserController

* dev/security/ Исправил SecurityExceptionHandler под checkstyle

* dev/security/ Написал unit тест для TokenService

* dev/security/ Написал unit тест для MyUserDetailsService

* dev/security/ Написал unit тест для AuthenticationService

* dev/security/ Добавил базовый интеграционный тест

* dev/security/ Добавил позитивные интеграционные тесты для UserController

* dev/security/ Пытаюсь исправить ci

* dev/security/ Пытаюсь исправить ci 2

* dev/security/ Пытаюсь исправить ci 3

* dev/security/ Пытаюсь исправить ci 4

* dev/security/ Пытаюсь исправить ci 5

* dev/security/ Написал интеграционные тесты на негативные сценарии
  • Loading branch information
AlexanderGarifullin authored Dec 13, 2024
1 parent 509721a commit 4227352
Show file tree
Hide file tree
Showing 45 changed files with 1,514 additions and 32 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
run: chmod +x gradlew

- name: Build project
env:
JWT_SECRET: ${{ secrets.JWT_SECRET }}
JWT_SHORT: ${{ secrets.JWT_SHORT }}
JWT_LONG: ${{ secrets.JWT_LONG }}
run: ./gradlew bootJar

test:
Expand All @@ -50,6 +54,10 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Verify test resources
run: ls src/test/resources


- name: Run tests
run: ./gradlew test

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,7 @@ out/

### VS Code ###
.vscode/

### yml ###
src/main/resources/jwt.yml
src/main/resources/test.yml
7 changes: 7 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ checkstyle {
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 +39,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'

// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.testcontainers:junit-jupiter:1.20.1'
Expand Down
5 changes: 4 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ services:
depends_on:
- db
environment:
- SPRING_PROFILES_ACTIVE=prod
- POSTGRES_CONTAINER_NAME=db
- POSTGRES_PORT=5432
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=123
- POSTGRES_DB=CodeforcesLocalDB
- SPRING_PROFILES_ACTIVE=prod
- JWT_SECRET=mysuperSecretPasswordformysuperbestAppwithsuperloginandBisness123Good2wow3magic1
- JWT_SHORT=10m
- JWT_LONG=30d
volumes:
db_data:
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.cf.cfteam.advicers.security;

import com.cf.cfteam.exceptions.security.InvalidTwoFactorCodeException;
import com.cf.cfteam.exceptions.security.TokenRevokedException;
import com.cf.cfteam.exceptions.security.UserAlreadyRegisterException;
import com.cf.cfteam.exceptions.security.UserNotFoundException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class SecurityExceptionHandler {

private static final String LOGIN = "login";
private static final String TOKEN = "token";
private static final String UNEXPECTED_ERROR = "unexpected.error";

@ExceptionHandler(UserAlreadyRegisterException.class)
public ResponseEntity<Object> handleUserAlreadyRegisterException(UserAlreadyRegisterException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, Map.of(LOGIN, ex.getLogin()));
}

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(LOGIN, ex.getLogin()));
}

@ExceptionHandler(TokenRevokedException.class)
public ResponseEntity<Object> handleTokenRevokedException(TokenRevokedException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.UNAUTHORIZED, Map.of(TOKEN, ex.getToken()));
}

@ExceptionHandler(InvalidTwoFactorCodeException.class)
public ResponseEntity<Object> handleInvalidTwoFactorCodeException(InvalidTwoFactorCodeException ex) {
return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, null);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleGenericException(Exception ex) {
return buildErrorResponse(UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR, null);
}

private ResponseEntity<Object> buildErrorResponse(String message, HttpStatus status, Map<String, Object> details) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("timestamp", LocalDateTime.now());
errorResponse.put("status", status.value());
errorResponse.put("error", status.getReasonPhrase());
errorResponse.put("message", message);

if (details != null) {
errorResponse.put("details", details);
}

return new ResponseEntity<>(errorResponse, status);
}
}
67 changes: 67 additions & 0 deletions src/main/java/com/cf/cfteam/auth/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.cf.cfteam.auth;

import com.cf.cfteam.exceptions.security.TokenRevokedException;
import com.cf.cfteam.services.security.JwtService;
import com.cf.cfteam.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);
}
}
44 changes: 44 additions & 0 deletions src/main/java/com/cf/cfteam/config/AppConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.cf.cfteam.config;

import com.cf.cfteam.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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
@RequiredArgsConstructor
public class AppConfig {

private final MyUserDetailsService myUserDetailsService;

@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/cf/cfteam/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.cf.cfteam.config;

import com.cf.cfteam.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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;


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", "PATCH", "DELETE", "OPTIONS"));
corsConfiguration.setAllowedHeaders(List.of("*"));
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}))
.authorizeHttpRequests(request -> request
.requestMatchers("/auth/register", "/auth/login").permitAll()
// .requestMatchers("/**").hasRole("User")
.anyRequest().authenticated())
.sessionManagement(sessionManagementConfigurer ->
sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.cf.cfteam.controllers.security;

import com.cf.cfteam.services.security.AuthenticationService;
import com.cf.cfteam.transfer.payloads.security.AuthenticationPayload;
import com.cf.cfteam.transfer.payloads.security.ChangePasswordPayload;
import com.cf.cfteam.transfer.payloads.security.RegistrationPayload;
import com.cf.cfteam.transfer.responses.security.JwtAuthenticationResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

@RestController
@RequiredArgsConstructor
@RequestMapping("/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,
Authentication authentication) {
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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.cf.cfteam.exceptions.security;

public class InvalidTwoFactorCodeException extends RuntimeException {
public InvalidTwoFactorCodeException() {
super("two-factor.code_invalid");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.cf.cfteam.exceptions.security;

import lombok.Getter;

@Getter
public class TokenNotFoundException extends RuntimeException {
private final String token;

public TokenNotFoundException(String token) {
super("token.not_found");
this.token = token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.cf.cfteam.exceptions.security;

import lombok.Getter;

@Getter
public class TokenRevokedException extends RuntimeException {

private final String token;

public TokenRevokedException(String token) {
super("token.is_revoked");
this.token = token;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.cf.cfteam.exceptions.security;

import lombok.Getter;

@Getter
public class UserAlreadyRegisterException extends RuntimeException {

private final String login;

public UserAlreadyRegisterException(String login) {
super("login.already_register");
this.login = login;
}
}
Loading

0 comments on commit 4227352

Please sign in to comment.