@Test
@DisplayName("로그인 실패 - password 틀림")
void signinFailForWrongPassword() throws Exception {
  // given
  String wrongPassword = "wrongPassword";
  SigninDto.Request signinDto = SigninDto.Request.builder()
      .username("testUser")
      .password("testPassword")
      .build();

  // then
  mockMvc.perform(post("/signin")
          .contentType(MediaType.APPLICATION_JSON)
          .content("{\"username\":\"" + signinDto.getUsername()
              + "\",\"password\":\"" + wrongPassword + "\"}"))
      .andExpect(status().isBadRequest());
}

 

  • 로그인 실패 케이스 (비밀번호 틀림) 중 when이 필요 없는 것 같아 지웠다.
    • 어차피 다른 비밀번호를 넣으면 알아서 에러가 발생할 것이기 때문에 지정해 줬던 when을 지웠더랬다.
  • 그랬더니 ? NestedServletException 이 발생 됐다.
    • 그리고 가장 위엔 NullPointerException: Cannot invoke "zerobase.maintenance.domain.Account.getUsername()" because "loginSuccessful" is null
    • "loginSuccessful 은 로그인에 대한 정볼르 담고 있는 객체인데, 그게 없단다.
  • 비밀번호가 틀렸을 경우의 에러를 띄우고 싶었는데 로그인이 안돼서 객체가 없다는 에러를 띄운 것이다.
  • 그럼 다시 when을 집어 넣자.
@Test
@DisplayName("로그인 실패 - password 틀림")
void signinFailForWrongPassword() throws Exception {
  // given
  String wrongPassword = "wrongPassword";
  SigninDto.Request signinDto = SigninDto.Request.builder()
      .username("testUser")
      .password("testPassword")
      .build();

  // when
  when(signinService.signin(signinDto.getUsername(), wrongPassword))
      .thenThrow(new AccountException(ErrorCode.WRONG_PASSWORD));

  // then
  mockMvc.perform(post("/signin")
          .contentType(MediaType.APPLICATION_JSON)
          .content("{\"username\":\"" + signinDto.getUsername()
              + "\",\"password\":\"" + wrongPassword + "\"}"))
      .andExpect(status().isBadRequest());
}
  • when으로 siginDto.getUsername(), wrongPasswrod를 넣었을 경우 에러 발생으로 만든 후

정상적으로 테스트 성공

구현 목적

 

JwtAuthenticationFilter는 Spring Security의 필터 중 하나로, 클라이언트의 요청이나 서버의 응답을 가로채어서 JWT 토큰을 확인하고 인증을 처리하는 역할을 합니다. 클라이언트가 요청을 보낼 때 해당 필터를 거쳐 JWT 토큰을 검증하고, 서버가 응답을 할 때도 해당 필터를 통해 JWT 토큰을 생성하고 응답에 포함시킬 수 있습니다. 이를 통해 보안 및 권한 검사를 수행하고, 사용자 인증 및 권한 부여를 처리할 수 있습니다.

 

public class JwtAuthenticationFilter extends OncePerRequestFilter

 

OncePerRequestFilter

 

OncePerRequestFilter는 Spring Security에서 제공하는 필터 중 하나로, 각 요청당 한 번만 실행되도록 보장하는 필터입니다. 이 필터는 서블릿 필터의 추상 클래스인 GenericFilterBean을 확장하며, doFilterInternal 메서드를 구현하여 필터링 로직을 정의합니다.

OncePerRequestFilter는 보통 Spring Security에서 인증, 권한 부여, 요청 로깅 등과 같은 작업을 수행하는 데 사용됩니다. 요청이 필터 체인을 따라 흐를 때 한 번만 실행되어야 하는 경우에 이 필터를 사용하는 것이 유용합니다.


doFilterInternal (OncePerRequestFilter 의 구현 메서드)
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
    FilterChain filterChain) throws ServletException, IOException

 

OncePerRequestFilter 클래스는 GenericFilterBean을 확장하고 있으며, doFilterInternal 메서드를 오버라이드하여 필터링 로직을 정의합니다. 따라서 개발자는 OncePerRequestFilter를 상속받아 doFilterInternal 메서드를 구현하여 원하는 필터 동작을 지정할 수 있습니다. doFilterInternal 메서드는 각 요청에 대해 한 번 호출되며, 이를 통해 요청을 필터링하고 원하는 처리를 수행할 수 있습니다.

 

서블릿 컨테이너로부터 요청을 받거나 응답을 보내고, 필터체인 내에서 다음 필터로 요청을 전달하는 역할을 수행 합니다.

 

FilterChain

 

필터 체인은 웹 애플리케이션에서 요청을 받고 응답을 보내는 과정 중에 여러 필터가 연속적으로 적용되는 메커니즘을 말합니다. 각각의 필터는 요청을 가로채어 원하는 작업을 수행한 뒤, 다음 필터로 요청을 전달하거나 응답을 수정하여 다음 단계로 전달합니다. 필터 체인은 보안, 로깅, 인코딩 변환 등과 같은 다양한 작업을 수행하기 위해 사용됩니다.

 

각각의 체인은 필터를 의미하고, 해당 필터에선 HTTP 의 요청에 대한 특정 작업을 수행합니다. 해당 작업이 끝나면 다음 체인으로 연결을 시켜줘 작업을 이행할 수 있게 합니다.


doFilterIntenal 에서 사용 될 resolveTokenFromRequest 메서드

private String resolveTokenFromRequest(HttpServletRequest request) {
  String token = request.getHeader(TOKEN_HEADER);

  if (!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) {
    return token.substring(TOKEN_PREFIX.length());
  }

  return null;
}

 

1. 토큰은 HTTP 에서 HEADER 에 저장 되기 때문에 어떤 키를 기준으로 토큰을 주고 받을지 정해주기 위해

TOKEN_HEADER 설정. ("Authorization")

 

2. 인증 타입을 나태낼 TOKEN_PREFIX. JWT는 Bearer를 사용한다.

("Bearer ") 이후에 토큰값이 올 수 있게 Bearer 이후 한칸을 띈다.

 

이후 token 값을 가져와 반환해준다. (없으면 null)


다시 doFilterIntenal 
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
    FilterChain filterChain) throws ServletException, IOException {
  String token = this.resolveTokenFromRequest(request);

  if (StringUtils.hasText(token) && this.tokenProvider.validateToken(token)) {
    Authentication auth = this.tokenProvider.getAuthentication(token);
    SecurityContextHolder.getContext().setAuthentication(auth);
  }

  filterChain.doFilter(request, response);
}

 

1. resolveTokenFromRequest를 이용해 토큰 값을 가져온다.

2. 토큰이 존재하고, 유효 하다면 (tokenProvider 의 따로 구현해 놓은 validateToken을 이용해 검증, 토큰의 만료 시간 등을 검증 해준다.) 로직 진행.

 

조건문을 통과한 다음 실행될 로직에 필요한 getAuthentication 메서드 (TokenProvider)
public Authentication getAuthentication(String jwt) {
  UserDetails userDetails =
      this.authenticationService.loadUserByUsername(this.getUsername(jwt));
  return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}

 

Authentication 더 알아보기

2024.02.29 - [Java/Spring] - Authentication 인증 객체에 대하여

 

UserDetails를 가져오기 위해 사용 된 loadUserByUsername 메서드

(userDetailsService를 구현한 AuthenticationService 에서 Override 구현 돼 있다.)

public class AuthenticationService implements UserDetailsService {

  private final AccountRepository accountRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    return this.accountRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("해당 유저는 존재하지 않습니다."));
  }
}

 

 

getAuthentication 메서드의 UsernamePasswordAuthenticationToken 설명

- 지금은 credentials가 비어있는데 이는 사용자의 자격 증명이 이미 JWT 토큰 안에 포함 되어 있기 때문이다.

 

다시 doFilterIntenal
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
    FilterChain filterChain) throws ServletException, IOException {
  String token = this.resolveTokenFromRequest(request);

  if (StringUtils.hasText(token) && this.tokenProvider.validateToken(token)) {
    Authentication auth = this.tokenProvider.getAuthentication(token);
    SecurityContextHolder.getContext().setAuthentication(auth);
  }

  filterChain.doFilter(request, response);
}

그렇게 가져온 Authentication을 여기서는 변수명 auth로 초기화 시켰고, 이를 SecurityContextHolder에 넣어준다.

 

그렇게 검증 된 SecurityContextHolder 가 존재하게 되고 검증이 되지 않았다면 다음 문구인,

filtetChain.doFilter(request, response)를 통해 다음 체인을 진행하게 된다.

작성 예정

signWith 노란줄 !

강의를 보고 열심히 jwt를 만들고 있었는데 위와 같이 노란줄이 뜬다.

잘 사용하지 않는단다. (deprecated)

왜 ??

 

GPT 한테 물어보니 보안상의 이유로 signWith(SignatureAlgorithm, Key) 요 메서드는 잘 사용 되지 않는 단다.

보니까 그럼 다른 게, Key key 요렇게 Key를 넣으란다.

그래서
private static final Key secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);

 

요래 전역변수를 만들어 놓고,

.signWith(secretKey)

요래 바꿔서 사용 한다.

 

Key 란 ? 

 

Key는 Java Cryptography Architecture (JCA)에서 사용되는 인터페이스입니다. 이 인터페이스는 비밀 키, 공개 키 및 시크릿 키와 같은 암호화 및 서명에 사용되는 키를 표현합니다.

Keys.secretKeyFor(SignatureAlgorithm.HS512)는 주어진 알고리즘 (여기서는 HS512)에 따라 무작위 비밀 키를 생성하는 메서드입니다. 이 메서드는 암호화 및 서명 작업에 사용되는 키를 생성합니다. 여기서 HS512는 HMAC-SHA512 알고리즘을 나타냅니다. HMAC은 해시 기반 메시지 인증 코드로, 메시지 무결성을 보호하기 위해 사용됩니다.

따라서 Keys.secretKeyFor(SignatureAlgorithm.HS512)는 HMAC-SHA512 알고리즘에 대한 비밀 키를 생성하고 이를 Key 인터페이스로 반환합니다. 이것은 일반적으로 JWT의 서명 작업에 사용되는 비밀 키입니다.

 

그러니까

 

signWith(SignatureAlgorithm.HS512, key) 와 같은 방식의 메서드는 어찌됐건 key를 숨겨둬야 하는데 (지금 실습에선 yml에 jwt/secretkey 에 base64 를 활용한 비밀키를 넣어뒀다) key가 노출 될 위험이 있으니 더 안전하고 간단한  signWith(Key) 메서드를 사용 하라는 것 같다.

 

 

자동으로 랜덤한 비밀키를 생성해준다고 하니 그게 더 나을 것 같다.

var now = new Date();

 

- 할당 된 값에 따라 변수의 타입이 동적으로 결정 된다. 그니까 지금은 now 가 Date가 되는 것.

 

장점:

  1. 간결성: var를 사용하면 변수를 선언할 때 타입을 명시적으로 지정할 필요가 없으므로 코드가 더 간결해집니다.
  2. 유연성: var를 사용하면 한 번에 여러 유형의 값을 변수에 할당할 수 있습니다.
  3. 동적 타입: JavaScript는 동적으로 타입이 결정되는 언어이므로, var를 사용하여 변수를 선언하면 해당 변수의 타입이 동적으로 결정됩니다.
  4. 함수 스코프: var로 선언된 변수는 함수 스코프를 가지므로, 함수 내에서만 유효한 변수로 사용됩니다.

단점:

  1. 타입 오류 식별의 어려움: var를 사용하면 변수의 타입이 명시적으로 선언되지 않기 때문에 코드를 읽거나 유지보수할 때 변수의 타입을 추론하기가 어려울 수 있습니다. 이는 타입 오류를 식별하는 데 어려움을 초래할 수 있습니다.
  2. 전역 변수 오염 가능성: var로 선언된 변수는 함수 스코프를 가지며, 전역 변수로 사용될 수 있습니다. 이는 의도하지 않은 전역 변수 오염이 발생할 수 있으며, 디버깅이 어려워질 수 있습니다.
  3. 호이스팅: var로 선언된 변수는 호이스팅(hoisting)에 영향을 받습니다. 즉, 변수가 선언된 위치와 상관없이 해당 스코프의 최상단으로 끌어올려지므로, 코드의 의도와 다르게 동작할 수 있습니다.

결국 유연한 사용을 위해 var 키워드를 사용할 수 있지만, 잘 못 쓰게 되면 디버깅 등에 애먹을 수 있기 때문에 사용함에 있어 신중 해야한다.

(가독성만 따지다 더 큰 어려움을 불러일으킬 수도 있겠다..)

'Java > 개념 정리' 카테고리의 다른 글

동기적 메서드와 비동기적 메서드  (0) 2024.05.16
스레드의 차단(blocked)  (0) 2024.05.16
JWT(Json Web Token)의 구조  (0) 2024.02.22
Heap 이란?  (1) 2023.10.22
연결리스트(LinkedList)  (0) 2023.10.21
JWT의 비밀키 생성에 왜 base64 방식이 사용 되나 ? 

 

JWT(Json Web Token)는 비밀 키를 사용하여 서명(signature)을 생성합니다. 이 서명은 JWT의 무결성을 보장하고 JWT가 변경되지 않았음을 검증하는 데 사용됩니다. 비밀 키는 이 서명을 생성하고 검증하는 데 사용되는 중요한 보안 요소입니다.

일반적으로 비밀 키는 base64로 인코딩됩니다. 그 이유는 다음과 같습니다:

  1. Binary Data Representation: 비밀 키는 바이너리 데이터로서, 문자열로 직접 표현할 수 없습니다. 따라서 바이너리 데이터를 텍스트로 표현할 수 있는 base64 인코딩을 사용하여 키를 문자열로 변환합니다.
  2. ASCII 문자 사용: base64는 ASCII 문자 집합을 사용하여 데이터를 표현하므로, 키를 표현하는 데 사용하기 적합합니다.
  3. 문자열 처리 편의성: base64로 인코딩된 문자열은 일반적인 문자열 처리 도구를 사용하여 쉽게 다룰 수 있습니다.
  4. 안정성과 이식성: base64는 데이터를 안전하게 전송하고 저장하는 데 사용되는 표준 방법입니다. 이식성이 뛰어나며, 다양한 시스템과 프로그래밍 언어에서 사용할 수 있습니다.

따라서 JWT를 구현할 때 비밀 키를 base64로 인코딩하여 사용하는 것은 일반적인 관행입니다. 이는 안전한 보안 키를 문자열로 표현하고 사용하기 위한 표준적인 방법입니다.

 

base64 란? 

 

base64는 이진 데이터를 텍스트로 인코딩하기 위한 인코딩 방식 중 하나입니다. 이진 데이터는 일반적으로 바이너리 형태로 저장되어 있는데, 이를 텍스트 형태로 전환할 때 사용됩니다. 이 방식은 이진 데이터를 ASCII 문자 집합에 속하는 문자들로만 이루어진 텍스트 형태로 변환하여 텍스트 기반 시스템에서도 사용할 수 있도록 해줍니다.

base64 인코딩은 다음과 같은 특징을 갖습니다:

  1. 문자 집합: base64 인코딩은 64개의 문자로 이루어진 집합을 사용합니다. 이 문자 집합에는 대문자(A-Z), 소문자(a-z), 숫자(0-9), 그리고 두 개의 추가 문자(+와 /)가 포함되어 있습니다. 이렇게 총 64개의 문자로 이루어진 집합을 사용하기 때문에 base64라고 불립니다.
  2. 3바이트 묶음: base64 인코딩은 입력 데이터를 3바이트씩 묶어서 처리합니다. 각각의 3바이트는 총 24비트를 나타내는데, 이 비트들을 6비트씩 나누어서 base64 문자 집합에 매핑됩니다.
  3. 패딩: 입력 데이터의 바이트 수가 3의 배수가 아닌 경우, 패딩 문자인 '='가 추가됩니다. 이는 base64 인코딩된 데이터의 길이가 항상 4의 배수가 되도록 보장합니다.
  4. 텍스트로의 변환: base64 인코딩은 이진 데이터를 텍스트 형태로 변환합니다. 이진 데이터는 주로 바이너리 파일이나 이미지 파일과 같은 형태로 저장되어 있으며, 이를 텍스트 형태로 변환하여 전송하거나 저장할 수 있게 합니다.

base64는 이진 데이터를 텍스트로 변환할 때 주로 사용되며, 이메일과 웹에서 이미지 파일이나 바이너리 파일을 전송하거나 저장할 때 자주 활용됩니다. 또한 암호학에서는 주로 인증 정보나 디지털 서명과 같은 데이터를 안전하게 전송하거나 저장할 때 base64 인코딩을 사용합니다.

+ Recent posts