4주차 발자국 | 인프런 워밍업 클럽 2기 - 백엔드

4주차 발자국 | 인프런 워밍업 클럽 2기 - 백엔드

인프런 워밍업 클럽 2기

입문자를 위한 Spring Boot with Kotlin(https://inf.run/bXQQQ) 강의를 듣고 작성하였습니다

Spring Security

Spring Security는 Spring Boot 애플리케이션에 보안 기능을 손쉽게 통합할 수 있는 프레임워크입니다.
인증(Authentication)과 권한(Authorization) 관리 기능을 제공하여 애플리케이션을 보호하는 데 사용되며, OAuth2, JWT 같은 다양한 보안 프로토콜도 지원합니다.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-security")
}
@Configuration
class SecurityConfiguration {
    @Bean
    fun filterChain(httpSecurity: HttpSecurity): SecurityFilterChain? {
        return httpSecurity
            .authorizeHttpRequests { authorizeHttpRequests ->
                authorizeHttpRequests
                    .requestMatchers(AntPathRequestMatcher("/**")).authenticated()
                    .anyRequest().permitAll()
            }.csrf { csrf ->
                csrf.disable()
            }.headers { headers ->
                headers.addHeaderWriter(XFrameOptionsHeaderWriter(XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN))
            }.formLogin { formLogin ->
                formLogin.defaultSuccessUrl("/")
            }.logout { logout ->
                logout.logoutRequestMatcher(AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/")
            }.build()
    }
}

 

/* 
 * NestJS에서는 strategy, Guard 를 사용하여 인증/인가처리를 구현합니다. 
 * strategy 에서는 JWT, oauth 등의 보안 프로토콜을 정의하고 적용할 수 있습니다.
 * Guard를 정의하고 개별 request 위에 데코레이터로 적용할 수 있습니다 (global 적용도 가능)
*/

 

password encode

https://velog.io/@glencode/Spring-Security-Crypto를-사용한-비밀번호-암호화

시큐리티에 관한 강의를 아직 듣지 않았을 때, 패스워드 암호화를 해야하는 요구사항이 있어서 적용해보았던 내용을 기록합니다.

dependencies {
  implementation("org.springframework.security:spring-security-crypto")
}
@Configuration
class AuthConfig {
    @Bean
     fun passwordEncoder(): PasswordEncoder {
        return BCryptPasswordEncoder()
    }
}

의존성을 추가하고 config 를 정의합니다

@Service
class UserService(
    private val userRepository: UserRepository,
    private val passwordEncoder: PasswordEncoder,
    // and so on..
) {...}
/*
 * dto.password는 사용자로부터 입력받은 password
 * user.password는 DB에 encode 하여 저장된 password
*/

// encode
val encodedPassword = passwordEncoder.encode(dto.password) 

// compare
if(!passwordEncoder.matches(dto.password, user.password)) {
            throw BadRequestException("비밀번호가 틀렸습니다.")
}

passwordEncoder 를 주입하고 위 매서드를 사용하여 활용할 수 있습니다.

 

/*
 * nodejs 에서는 bcrypt를 사용하여 구현할 수 있습니다.
 * const bcrypt = require('bcrypt');
*/

// encode
const encodedPassword = await bcrypt.hash(password, 10); // salt = 10

// compare
if (!(await bcrypt.compare(password, user.password))) {
    throw new BadRequestException("비밀번호가 틀렸습니다.")
  }

 

ControllerAdvice

@RestControllerAdvice는 @ControllerAdvice와 @ResponseBody의 기능을 결합한 어노테이션으로, REST API에서 예외를 처리할 때 주로 사용됩니다.
모든 컨트롤러에 대해 JSON이나 XML과 같은 형태로 일관된 응답을 반환할 수 있습니다.

@ControllerAdvice는 Spring MVC에서 예외 처리, 데이터 바인딩, 모델 객체의 변환 등을 전역적으로 관리할 수 있게 도와주는 어노테이션입니다.

서버 내부에서 입력 오류에 대한 경우를 전부 BadRequestException 으로 사용하고 message 를 다양하게 주고 있었는데, 실제로 에러가 발생했을 때 message가 오지 않아서 몹시 불편함을 느꼈습니다.

직접 입력한 메시지를 응답에 뿌려주기 위해 검색 후에 아래와 같은 GlobalExceptionHandler 를 구현하였습니다.

@RestControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(BadRequestException::class)
    fun handleBadRequest(ex: BadRequestException): ResponseEntity<Map<String, String>> {
        val errorResponse = mapOf(
            "timestamp" to LocalDateTime.now().toString(),
            "status" to HttpStatus.BAD_REQUEST.value().toString(),
            "error" to "Bad Request",
            "message" to (ex.message ?: "잘못된 요청입니다."),
        )
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse)
    }
}
# 에러 응답 예시
{
    "timestamp": "2024-10-23T04:22:59.959124",
    "status": "400",
    "error": "Bad Request",
    "message": "먼저 입실해주세요"
}

 

/* NestJS ExceptionFilter
 * 위와 동일한 작업을 하는 코드
*/

@Catch(BadRequestException)
export class GlobalExceptionFilter implements ExceptionFilter {
  catch(exception: BadRequestException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const message = exception.getResponse() as string;

    const errorResponse = {
      timestamp: new Date().toISOString(),
      status: HttpStatus.BAD_REQUEST,
      error: 'Bad Request',
      message: message || '잘못된 요청입니다.',
    };

    response.status(HttpStatus.BAD_REQUEST).json(errorResponse);
  }
}

 

작은 회고

처음의 결심과 다르게, 강의 일정을 따라가는게 쉽지않았습니다. (게으르기 때문일까요~,,) 그래도 계속 하다보니 아주 조금...? 스프링에 대해 알아가는 느낌이 들어 정말 재밌었습니다. 꾸준히 노력해서, 프레임워크 상관없이 능숙하게 작업할 수 있는 백엔드 개발자가 되겠습니다.

프로젝트에 아직 @TODO 가 많은데, 시작한 프로젝트는 돌아오는 주까지 열심히 해서 잘 마무리하고 싶습니다. 학부생 때가 HTML의 마지막이라, 타임리프 작업이 가장 오래걸렸습니다... (사실 아직도 끝나지 않았습니다)

원래 쓰던 프레임워크와 비교해가며 이해하고 공부하는데, 이게 도움이 되었는지 아니었는지는 아직 잘 모르겠습니다. 아마 더 많이 사용해보는 시간이 필요할 거 같습니다. 공부법이든, 코딩 습관이든, 나만의 Best Practice 를 찾아가는 과정은 참 어려운거 같아요.

이번에 워밍업클럽을 통해 (!오랜만에!) 완강도 하고, 프로젝트도 하고, 강사님과 컨택할 수 있는 시간도 가지게 되어 정말 좋았습니다.
+ 온라인 세션에서 받은 이력서 피드백이 정말 많은 도움이 되었습니다. 이 자리를 빌려 감사의 마음을 전해드립니다 🙂

다음번에도 이런 기회가 온다면 다른 강의로 신청해보려고 합니다.

이 과정을 기획하고 관리하신 인프런 운영자님과, a-z까지 한 과정에 깔끔하게 담아내신 강사님 정말 고생많으셨습니다! 수료식에서 보아요 ( •͈૦•͈ )

댓글을 작성해보세요.

채널톡 아이콘