
JWT가 무엇이고 왜 사용하는지, 어떻게 사용하는지 알아보자.
세션을 이용한 로그인은 다음과 같은 순서로 진행된다.
세션 방식은 서버가 여러 개일 경우 문제가 발생한다. 로드밸런싱을 통해 A,B,C 서버가 있다고 가정하면 서버 마다 세션이 있을 것이다. 만약 A서버에서 로그인 요청을 보내 A서버의 세션에 유저 정보가 담겨있을 때 다음 요청에서 로드밸런싱을 통해 B서버로 요청을 보내게 된다면 B서버에서는 세션ID 검증에 실패할 것이다.
이를 해결하기 위해서는 여러가지 방법이 있는데 대표적인 방법으로 Redis와 같은 메모리 공유 서버에 세션 값과 유저 정보를 저장하는 방법이 있다.
정보보안의 목적은 정보가 생성되어 소멸되기까지 그 처리 및 유통의 생명 주기 전반에 걸쳐 기밀성(Confidentiality), 무결성(Integrity), 가용성(Availability)을 확보하는 데 있다.

위의 문제를 해결하기 위해서 문서를 암호화 시키는 방법을 사용한다. 어떠한 key가 있어야 문서를 열 수 있는데 이러한 방법을 사용해도 몇 가지 문제가 발생한다.
key를 등록하면 B도 key를 가지고 있어야한다. -> 어떻게 전달하지?key가 없어서 열 수는 없지만 다른 문서로 바꾸어서 B로 보낼 수 있다. -> B는 이 문서가 어디서 왔는지 알아야 한다.위 문제를 RSA 암호화 알고리즘을 사용하여 해결할 수 있다.
RSA는 공개키 암호화 시스템 중 하나이다. 간단하게 설명하면 공개키 + 개인키로 복호화 할 수 있다.

jwt 공식 사이트 : https://jwt.io/
jwt는 세션 기반 인증 방식의 문제점과 위에서 설명한 문제점들을 해결할 수 있다.
jwt는 header, payload, signature 로 이루어져 있다. 실제 jwt을 보면 .으로 구분되어 있는데 순서대로 header, payload, signature이다.
header,payload,secret key 정보가 담겨있다.signature와 header+payload+서버의 secret key를 HS256 방식으로 암호화한 값이 같은지 검증전체 코드 : https://github.com/yryryr96/jwt
jwt 토큰을 쉽게 만들기위해 라이브러리 설정을 해준다.
implementation group: 'com.auth0', name: 'java-jwt', version: '4.4.0' 를 build.gradle에 추가해준다.
폼 로그인과, 소셜 로그인을 구현하며 시큐리티의 로그인 과정을 설명했기에 중복되는 내용은 생략한다.
시큐리티는 /login로 username,password를 전송하면 UsernamePasswordAuthenticationFilter가 동작한다.
로그인을 시도하면 attemptAuthentication 메서드에서 유저 정보를 검증한다.
// /login 요청 시 로그인 시도를 위해 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
System.out.println("로그인 시도중 attemptAuthentication()");
ObjectMapper objectMapper = new ObjectMapper();
LoginRequestDto loginRequestDto = null;
try {
loginRequestDto = objectMapper.readValue(request.getInputStream(), LoginRequestDto.class);
} catch (IOException e) {
e.printStackTrace();
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginRequestDto.getUsername(), loginRequestDto.getPassword());
// PrincipalDetailsService의 loadUserByUsername() 함수 실행 -> authentication 반환
Authentication authentication
= authenticationManager.authenticate(authenticationToken);
// authentication 객체가 session 영역에 저장된다. -> 로그인 성공
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
System.out.println(principalDetails.getUser());
System.out.println(principalDetails.getUser().getUsername());
return authentication;
}
성공적으로 로그인이 완료됐다면 successfulAuthentication 메서드가 실행되는데 여기서 jwt 토큰을 만들고 response의 헤더에 담아서 클라이언트에게 반환하는 로직을 작성한다.
// attemptAuthentication 실행 후 인증이 정상적으로 되었으면 successfulAuthentication 함수가 실행된다.
// JWT 토큰을 만들어 request 요청한 사용자에게 JWT 토큰을 response 해주면 된다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
System.out.println("successfulAuthentication() = " + "인증이 완료되었다는 뜻");
PrincipalDetails principalDetails = (PrincipalDetails) authResult.getPrincipal();
// Hash 암호 방식
String jwtToken = JWT.create()
.withSubject("cos토큰")
.withExpiresAt(new Date(System.currentTimeMillis() + (EXPIRATION_TIME)))
.withClaim("id", principalDetails.getUser().getId())
.withClaim("username", principalDetails.getUser().getUsername())
.sign(Algorithm.HMAC512(SECRET));
response.addHeader(HEADER_STRING, TOKEN_PREFIX+jwtToken);
}
여기까지가 유저 정보를 검증하고 클라이언트에게 jwt 토큰을 반환하는 필터를 만드는 과정이었다.
다음으로는 클라이언트가 헤더에 jwt 토큰을 담아서 요청했을 때 토큰에 있는 유저 정보를 추출하여 인증하는 로직을 작성해보자.
시큐리티는 권한,인증이 필요한 요청이 들어오면 BasicAuthenticationFilter가 무조건 동작한다.
// 시큐리티 필터중 BasicAuthenticationFilter 가 있다.
// 권한이나 인증이 필요한 특정 주소를 요청했을 때 위 필터를 무조건 타게 되어있다.
// 만약 권한이나 인증이 필요한 주소가 아니라면 BasicAuthenticationFilter를 타지 않는다.
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {
private UserRepository userRepository;
public JwtAuthorizationFilter(AuthenticationManager authenticationManager, UserRepository userRepository) {
super(authenticationManager);
this.userRepository = userRepository;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("권한이나 인증이 요청됨");
String jwtHeader = request.getHeader("Authorization");
System.out.println("jwtHeader = " + jwtHeader);
// header가 있는지 확인
if (jwtHeader == null || !jwtHeader.startsWith("Bearer")) {
chain.doFilter(request, response);
return;
}
//JWT토큰을 검증해서 정상적인 사용자인지 확인
String jwtToken = request.getHeader(HEADER_STRING).replace(TOKEN_PREFIX,"");
String username =
JWT.require(Algorithm.HMAC512(SECRET)).build().verify(jwtToken).getClaim("username").asString();
// 서명이 정상적으로 됨
if (username != null) {
User findUser = userRepository.findByUsername(username);
PrincipalDetails principalDetails = new PrincipalDetails(findUser);
//JWT 토큰 서명을 통해 서명이 정상이면 Authentication 객체를 만든다.
Authentication authentication =
new UsernamePasswordAuthenticationToken(principalDetails,null, principalDetails.getAuthorities());
// 강제로 시큐리티 세션에 접근하여 Authentication 객체 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}