Skip to content

Commit

Permalink
인증/인가 추가 (#64)
Browse files Browse the repository at this point in the history
* feat(#15): gateway 모듈에서 검증된 토큰 헤더에 추가 후 요청 전달
* feat(#15): User 모듈에 spring security 및 커스텀 필터(헤더 파싱) 추가
  • Loading branch information
kyeonkim authored Oct 7, 2024
1 parent 90d8e7d commit 07b9672
Show file tree
Hide file tree
Showing 16 changed files with 365 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
Expand All @@ -29,18 +30,22 @@ public class AuthService {
private final UserService userService;
private final JwtProperties jwtProperties;
private final SecretKey secretKey;
private final PasswordEncoder passwordEncoder;

public AuthService(UserService userService, JwtProperties jwtProperties) {
public AuthService(UserService userService, JwtProperties jwtProperties,
PasswordEncoder passwordEncoder) {
this.userService = userService;
this.jwtProperties = jwtProperties;
this.secretKey = createSecretKey();
this.passwordEncoder = passwordEncoder;
}

public AuthResponse.SignIn signIn(AuthRequest.SignIn request) {
UserDto userData = userService.getUserByUsername(request.getUsername());

// TODO(경민): 암호화 된 비밀번호로 비교해야함
if (userData == null || !userData.getPassword().equals(request.getPassword())) {
if (userData == null ||
!passwordEncoder.matches(request.getPassword(), userData.getPassword())
) {
throw new AuthException(AuthErrorCode.SIGN_IN_FAIL);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
Expand All @@ -30,4 +32,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
return http.build();
}

@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ void setUp() {
@Test
void test_로그인_성공() {
// Arrange
AuthRequest.SignIn request = new AuthRequest.SignIn("testuser", "correctpassword");
UserDto userDto = new UserDto(1L, "testuser", "correctpassword", "ROLE_ADMIN");
AuthRequest.SignIn request = new AuthRequest.SignIn("testuser", "password123");
UserDto userDto = new UserDto(1L, "testuser",
"$2a$10$YZ2cP0PF11iqNqNrwk4pUOKQnAGxqLtGxO1F6XZomixg73EYQoduC", "ROLE_ADMIN");

// Mock the user service to return the userDto when called
when(userService.getUserByUsername("testuser")).thenReturn(userDto);
Expand Down
50 changes: 27 additions & 23 deletions service/gateway/server/build.gradle
Original file line number Diff line number Diff line change
@@ -1,49 +1,53 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.sparta.gateway'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

ext {
set('springCloudVersion', "2023.0.3")
set('springCloudVersion', "2023.0.3")
}

dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation project(':common:domain')
implementation project(':service:auth:auth_dto')

implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.sparta.gateway.server.application;

import com.sparta.auth.auth_dto.jwt.JwtClaim;

public interface AuthService {

JwtClaim verifyToken(String token);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.sparta.gateway.server.infrastructure.configuration;

import feign.Logger;
import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;

public class AuthFeignConfig {

@Bean
public Decoder feignDecoder() {
ObjectFactory<HttpMessageConverters> messageConverters = HttpMessageConverters::new;
return new SpringDecoder(messageConverters);
}

@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.sparta.gateway.server.infrastructure.feign;

import com.sparta.auth.auth_dto.jwt.JwtClaim;
import com.sparta.gateway.server.application.AuthService;
import com.sparta.gateway.server.infrastructure.configuration.AuthFeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;

@FeignClient(name = "auth", configuration = AuthFeignConfig.class)
public interface AuthClient extends AuthService {

@GetMapping("/internal/auth/verify")
JwtClaim verifyToken(@RequestHeader("Authorization") String token);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.sparta.gateway.server.infrastructure.filter;

import static com.sparta.common.domain.jwt.JwtGlobalConstant.AUTHORIZATION;
import static com.sparta.common.domain.jwt.JwtGlobalConstant.BEARER_PREFIX;
import static com.sparta.common.domain.jwt.JwtGlobalConstant.X_USER_CLAIMS;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.auth.auth_dto.jwt.JwtClaim;
import com.sparta.gateway.server.application.AuthService;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Slf4j
@Component
public class JwtAuthenticationFilter implements GlobalFilter {

private final AuthService authService;

public JwtAuthenticationFilter(@Lazy AuthService authService) {
this.authService = authService;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();

if (path.startsWith("/api/auth/") || path.startsWith("/api/users/sign-up")) {
return chain.filter(exchange);
}

Optional<String> token = this.extractToken(exchange);

if (token.isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}

try {
JwtClaim claims = authService.verifyToken(token.get());
this.addUserClaimsToHeaders(exchange, claims);
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

private void addUserClaimsToHeaders(ServerWebExchange exchange, JwtClaim claims) {
if (claims != null) {
try {
ObjectMapper objectMapper = new ObjectMapper();
String jsonClaims = objectMapper.writeValueAsString(claims);
exchange.getRequest().mutate()
.header(X_USER_CLAIMS, URLEncoder.encode(jsonClaims, StandardCharsets.UTF_8))
.build();
} catch (JsonProcessingException e) {
log.error("Error processing JSON: {}", e.getMessage());
}
}
}

private Optional<String> extractToken(ServerWebExchange exchange) {
String header = exchange.getRequest().getHeaders().getFirst(AUTHORIZATION);
if (header != null && header.startsWith(BEARER_PREFIX)) {
return Optional.of(header.substring(BEARER_PREFIX.length()));
}
return Optional.empty();
}

}
3 changes: 3 additions & 0 deletions service/user/server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@ ext {
dependencies {
implementation project(':common:domain')
implementation project(':service:user:user_dto')
implementation project(':service:auth:auth_dto')

implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
import static com.sparta.user.exception.UserErrorCode.USER_CONFLICT;
import static com.sparta.user.exception.UserErrorCode.USER_NOT_FOUND;

import com.sparta.user.user_dto.infrastructure.UserDto;
import com.sparta.user.domain.model.User;
import com.sparta.user.domain.repository.UserRepository;
import com.sparta.user.exception.UserException;
import com.sparta.user.presentation.request.UserRequest;
import com.sparta.user.user_dto.infrastructure.UserDto;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -17,6 +18,7 @@
public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
public void createUser(UserRequest.Create request) {
Expand All @@ -26,8 +28,7 @@ public void createUser(UserRequest.Create request) {
user -> {
throw new UserException(USER_CONFLICT);
});
// TODO: 비밀번호 암호화 필요(암호화 시 security 라이브러리가 필요한데 설치하면 테스트 불편함. 이후에 추가) - 경민
userRepository.save(User.create(request, request.getPassword()));
userRepository.save(User.create(request, passwordEncoder.encode(request.getPassword())));
}

public UserDto getUserByUsername(String username) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.sparta.user.infrastructure.configuration;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.user.infrastructure.filter.SecurityContextFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.annotation.web.configurers.RequestCacheConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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;

@EnableMethodSecurity
@EnableWebSecurity
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain httpSecurity(HttpSecurity http, ObjectMapper objectMapper)
throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement((s) -> s.sessionCreationPolicy(
SessionCreationPolicy.STATELESS))
.rememberMe(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.requestCache(RequestCacheConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/users/sign-up").permitAll()
.requestMatchers("/internal/users/**").permitAll()
.anyRequest().authenticated())
.addFilterAfter(new SecurityContextFilter(objectMapper),
UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}

}
Loading

0 comments on commit 07b9672

Please sign in to comment.