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

Commit 4227352

Browse files
dev/security/ Добавил работу с пользователями и security (#5)
* 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/ Написал интеграционные тесты на негативные сценарии
1 parent 509721a commit 4227352

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1514
-32
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ jobs:
3030
run: chmod +x gradlew
3131

3232
- name: Build project
33+
env:
34+
JWT_SECRET: ${{ secrets.JWT_SECRET }}
35+
JWT_SHORT: ${{ secrets.JWT_SHORT }}
36+
JWT_LONG: ${{ secrets.JWT_LONG }}
3337
run: ./gradlew bootJar
3438

3539
test:
@@ -50,6 +54,10 @@ jobs:
5054
- name: Grant execute permission for gradlew
5155
run: chmod +x gradlew
5256

57+
- name: Verify test resources
58+
run: ls src/test/resources
59+
60+
5361
- name: Run tests
5462
run: ./gradlew test
5563

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,7 @@ out/
3535

3636
### VS Code ###
3737
.vscode/
38+
39+
### yml ###
40+
src/main/resources/jwt.yml
41+
src/main/resources/test.yml

build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ checkstyle {
2727
dependencies {
2828
// spring
2929
implementation 'org.springframework.boot:spring-boot-starter-web'
30+
implementation 'org.springframework.boot:spring-boot-starter-validation'
31+
implementation 'org.springframework.boot:spring-boot-starter-security'
3032

3133
// db
3234
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
@@ -37,6 +39,11 @@ dependencies {
3739
implementation 'org.projectlombok:lombok'
3840
annotationProcessor 'org.projectlombok:lombok'
3941

42+
// jwt
43+
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
44+
implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
45+
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'
46+
4047
// test
4148
testImplementation 'org.springframework.boot:spring-boot-starter-test'
4249
testImplementation 'org.testcontainers:junit-jupiter:1.20.1'

docker/docker-compose.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ services:
1717
depends_on:
1818
- db
1919
environment:
20+
- SPRING_PROFILES_ACTIVE=prod
2021
- POSTGRES_CONTAINER_NAME=db
2122
- POSTGRES_PORT=5432
2223
- POSTGRES_USER=postgres
2324
- POSTGRES_PASSWORD=123
2425
- POSTGRES_DB=CodeforcesLocalDB
25-
- SPRING_PROFILES_ACTIVE=prod
26+
- JWT_SECRET=mysuperSecretPasswordformysuperbestAppwithsuperloginandBisness123Good2wow3magic1
27+
- JWT_SHORT=10m
28+
- JWT_LONG=30d
2629
volumes:
2730
db_data:
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.cf.cfteam.advicers.security;
2+
3+
import com.cf.cfteam.exceptions.security.InvalidTwoFactorCodeException;
4+
import com.cf.cfteam.exceptions.security.TokenRevokedException;
5+
import com.cf.cfteam.exceptions.security.UserAlreadyRegisterException;
6+
import com.cf.cfteam.exceptions.security.UserNotFoundException;
7+
import org.springframework.http.HttpStatus;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.ControllerAdvice;
10+
import org.springframework.web.bind.annotation.ExceptionHandler;
11+
12+
import java.time.LocalDateTime;
13+
import java.util.HashMap;
14+
import java.util.Map;
15+
16+
@ControllerAdvice
17+
public class SecurityExceptionHandler {
18+
19+
private static final String LOGIN = "login";
20+
private static final String TOKEN = "token";
21+
private static final String UNEXPECTED_ERROR = "unexpected.error";
22+
23+
@ExceptionHandler(UserAlreadyRegisterException.class)
24+
public ResponseEntity<Object> handleUserAlreadyRegisterException(UserAlreadyRegisterException ex) {
25+
return buildErrorResponse(ex.getMessage(), HttpStatus.CONFLICT, Map.of(LOGIN, ex.getLogin()));
26+
}
27+
28+
@ExceptionHandler(UserNotFoundException.class)
29+
public ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex) {
30+
return buildErrorResponse(ex.getMessage(), HttpStatus.NOT_FOUND, Map.of(LOGIN, ex.getLogin()));
31+
}
32+
33+
@ExceptionHandler(TokenRevokedException.class)
34+
public ResponseEntity<Object> handleTokenRevokedException(TokenRevokedException ex) {
35+
return buildErrorResponse(ex.getMessage(), HttpStatus.UNAUTHORIZED, Map.of(TOKEN, ex.getToken()));
36+
}
37+
38+
@ExceptionHandler(InvalidTwoFactorCodeException.class)
39+
public ResponseEntity<Object> handleInvalidTwoFactorCodeException(InvalidTwoFactorCodeException ex) {
40+
return buildErrorResponse(ex.getMessage(), HttpStatus.BAD_REQUEST, null);
41+
}
42+
43+
@ExceptionHandler(Exception.class)
44+
public ResponseEntity<Object> handleGenericException(Exception ex) {
45+
return buildErrorResponse(UNEXPECTED_ERROR, HttpStatus.INTERNAL_SERVER_ERROR, null);
46+
}
47+
48+
private ResponseEntity<Object> buildErrorResponse(String message, HttpStatus status, Map<String, Object> details) {
49+
Map<String, Object> errorResponse = new HashMap<>();
50+
errorResponse.put("timestamp", LocalDateTime.now());
51+
errorResponse.put("status", status.value());
52+
errorResponse.put("error", status.getReasonPhrase());
53+
errorResponse.put("message", message);
54+
55+
if (details != null) {
56+
errorResponse.put("details", details);
57+
}
58+
59+
return new ResponseEntity<>(errorResponse, status);
60+
}
61+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.cf.cfteam.auth;
2+
3+
import com.cf.cfteam.exceptions.security.TokenRevokedException;
4+
import com.cf.cfteam.services.security.JwtService;
5+
import com.cf.cfteam.services.security.TokenService;
6+
import jakarta.servlet.FilterChain;
7+
import jakarta.servlet.ServletException;
8+
import jakarta.servlet.http.HttpServletRequest;
9+
import jakarta.servlet.http.HttpServletResponse;
10+
import lombok.NonNull;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.http.HttpHeaders;
13+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
14+
import org.springframework.security.core.context.SecurityContextHolder;
15+
16+
17+
import org.springframework.security.core.userdetails.UserDetailsService;
18+
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
19+
import org.springframework.stereotype.Component;
20+
import org.springframework.web.filter.OncePerRequestFilter;
21+
22+
import java.io.IOException;
23+
24+
@Component
25+
@RequiredArgsConstructor
26+
public class JwtAuthenticationFilter extends OncePerRequestFilter {
27+
public static final String BEARER_PREFIX = "Bearer ";
28+
29+
private final JwtService jwtService;
30+
private final TokenService tokenService;
31+
32+
private final UserDetailsService userDetailsService;
33+
34+
@Override
35+
protected void doFilterInternal(@NonNull HttpServletRequest request,
36+
@NonNull HttpServletResponse response,
37+
@NonNull FilterChain filterChain)
38+
throws ServletException, IOException, TokenRevokedException {
39+
var authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
40+
41+
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
42+
filterChain.doFilter(request, response);
43+
return;
44+
}
45+
46+
var jwt = authHeader.substring(BEARER_PREFIX.length());
47+
48+
if (tokenService.isTokenRevoked(jwt)) throw new TokenRevokedException(jwt);
49+
50+
var userLogin = jwtService.extractUserLogin(jwt);
51+
52+
var userDetails = userDetailsService.loadUserByUsername(userLogin);
53+
54+
UsernamePasswordAuthenticationToken authToken =
55+
new UsernamePasswordAuthenticationToken(userDetails,
56+
userDetails.getPassword(),
57+
userDetails.getAuthorities());
58+
59+
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
60+
61+
if (SecurityContextHolder.getContext().getAuthentication() == null) {
62+
SecurityContextHolder.getContext().setAuthentication(authToken);
63+
}
64+
65+
filterChain.doFilter(request, response);
66+
}
67+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.cf.cfteam.config;
2+
3+
import com.cf.cfteam.services.security.MyUserDetailsService;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.security.authentication.AuthenticationManager;
8+
import org.springframework.security.authentication.AuthenticationProvider;
9+
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
10+
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
11+
import org.springframework.security.core.userdetails.UserDetailsService;
12+
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
13+
import org.springframework.security.crypto.password.PasswordEncoder;
14+
15+
16+
@Configuration
17+
@RequiredArgsConstructor
18+
public class AppConfig {
19+
20+
private final MyUserDetailsService myUserDetailsService;
21+
22+
@Bean
23+
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
24+
return config.getAuthenticationManager();
25+
}
26+
27+
@Bean
28+
public AuthenticationProvider authenticationProvider() {
29+
var authProvider = new DaoAuthenticationProvider();
30+
authProvider.setUserDetailsService(userDetailsService());
31+
authProvider.setPasswordEncoder(passwordEncoder());
32+
return authProvider;
33+
}
34+
35+
@Bean
36+
public UserDetailsService userDetailsService() {
37+
return myUserDetailsService;
38+
}
39+
40+
@Bean
41+
public PasswordEncoder passwordEncoder() {
42+
return new BCryptPasswordEncoder();
43+
}
44+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.cf.cfteam.config;
2+
3+
import com.cf.cfteam.auth.JwtAuthenticationFilter;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.context.annotation.Bean;
6+
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.security.authentication.AuthenticationProvider;
8+
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
9+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
10+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
11+
import org.springframework.security.config.http.SessionCreationPolicy;
12+
import org.springframework.security.web.SecurityFilterChain;
13+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
14+
import org.springframework.web.cors.CorsConfiguration;
15+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
16+
17+
18+
import java.util.List;
19+
20+
@Configuration
21+
@EnableWebSecurity
22+
@EnableMethodSecurity
23+
@RequiredArgsConstructor
24+
public class SecurityConfig {
25+
26+
private final JwtAuthenticationFilter jwtAuthFilter;
27+
private final AuthenticationProvider authenticationProvider;
28+
29+
@Bean
30+
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
31+
http
32+
.csrf(AbstractHttpConfigurer::disable)
33+
.cors(cors -> cors.configurationSource(request -> {
34+
var corsConfiguration = new CorsConfiguration();
35+
corsConfiguration.setAllowedOriginPatterns(List.of("*"));
36+
corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
37+
corsConfiguration.setAllowedHeaders(List.of("*"));
38+
corsConfiguration.setAllowCredentials(true);
39+
return corsConfiguration;
40+
}))
41+
.authorizeHttpRequests(request -> request
42+
.requestMatchers("/auth/register", "/auth/login").permitAll()
43+
// .requestMatchers("/**").hasRole("User")
44+
.anyRequest().authenticated())
45+
.sessionManagement(sessionManagementConfigurer ->
46+
sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
47+
.authenticationProvider(authenticationProvider)
48+
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
49+
50+
return http.build();
51+
}
52+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.cf.cfteam.controllers.security;
2+
3+
import com.cf.cfteam.services.security.AuthenticationService;
4+
import com.cf.cfteam.transfer.payloads.security.AuthenticationPayload;
5+
import com.cf.cfteam.transfer.payloads.security.ChangePasswordPayload;
6+
import com.cf.cfteam.transfer.payloads.security.RegistrationPayload;
7+
import com.cf.cfteam.transfer.responses.security.JwtAuthenticationResponse;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.http.ResponseEntity;
10+
import org.springframework.security.core.Authentication;
11+
import org.springframework.web.bind.annotation.*;
12+
13+
@RestController
14+
@RequiredArgsConstructor
15+
@RequestMapping("/auth")
16+
public class UserController {
17+
18+
private final AuthenticationService authenticationService;
19+
20+
@PostMapping("/register")
21+
public JwtAuthenticationResponse register(@RequestBody RegistrationPayload registrationRequest) {
22+
return authenticationService.register(registrationRequest);
23+
}
24+
25+
@PostMapping("/login")
26+
public JwtAuthenticationResponse login(@RequestBody AuthenticationPayload authenticationPayload,
27+
Authentication authentication) {
28+
return authenticationService.login(authenticationPayload);
29+
}
30+
31+
@PostMapping("/logout")
32+
public ResponseEntity<Void> logout(Authentication authentication) {
33+
authenticationService.logout(authentication);
34+
return ResponseEntity.ok().build();
35+
}
36+
37+
@PatchMapping("change-password")
38+
public ResponseEntity<Void> changePassword(
39+
@RequestBody ChangePasswordPayload changePasswordRequest,
40+
Authentication authentication
41+
) {
42+
authenticationService.changePassword(changePasswordRequest, authentication);
43+
return ResponseEntity.ok().build();
44+
}
45+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.cf.cfteam.exceptions.security;
2+
3+
public class InvalidTwoFactorCodeException extends RuntimeException {
4+
public InvalidTwoFactorCodeException() {
5+
super("two-factor.code_invalid");
6+
}
7+
}

0 commit comments

Comments
 (0)