먼저 Jwt 란 뭔지 간단하게 알아보자
Jwt(Json Web Token)
Jwt은 주로 사용자 인증 및 정보 전달에 사용하는 토큰 기반 인증 방식이다. 토큰 자체에 사용자 정보를 포함하고 있기 때문에 서버 측 세션 저장소가 필요하지 않다는 장점이 있다.
- 헤더 : 어떤 알고리즘을 사용해서 서명했는지에 대한 내용
- 페이로드 : 인증 또는 인가에 필요한 사용자 정보를 담고 있는 부분
- 서명 : 토큰의 신뢰성을 보장하기 위한 부분. 헤더와 페이로드의 데이터 무결성을 확인하기 위해 서버는 헤더와 페이로드를 조합하고, 서버의 시크릿키로 서명하여 이 서명을 생성한다.
인코딩되어있는 Jwt 토큰을 디코딩해보면 위처럼 헤더, 페이로드, 서명값을 얻을 수 있다.
Jwt 기본 정보 세팅을 해보자
jwt 폴더 안에 먼저 Jwt의 기본 정보들을 넣어주자.
public interface JwtVO {
public static final String SECRET = "메타코딩"; // HS256 (대칭키) 서버의 키
public static final int EXPIRATION_TIME = 1000 * 60 * 60 * 24 * 7; // 만료시간은 일주일
public static final String TOKEN_PREFIX = "Bearer ";
public static final String HEADER = "Authorization";
}
- SECRET : 토큰을 암호화할 때 필요한 중요한 시크릿키이다. 원래 절대 노출되면 안된다.
- EXPIRATION_TIME : Jwt 토큰의 만료시간을 정해준다. 보통 일주일로 지정한다.
- TOKEN_PREFIX : Jwt 토큰이 Authorization 헤더에 포함될 때 앞에 붙는 Bearer 접두사이다. 전처리 시 떼어줘야하기 때문에 PREFIX로 정해주었다.
- HEADER : 클라이언트가 Jwt를 서버로 보낼 때 사용하는 HTTP 헤더 필드 이름이다. 일반적으로 Authorization 헤더에 Jwt를 포함해 보내도록 한다.
Jwt 필터 세팅을 해보자
이제 인증 필터와 인가 필터를 구현해야한다. 전체적인 흐름을 그림으로 정리해보았다.
JwtAuthenticationFilter
=> 인증을 수행하는 필터이다.
- 기본값으로 /login 에서 동작한다. (원하는 대로 로그인 api를 변경할 수 있다.)
<진행 순서>
- 클라이언트로부터 username, password를 받는다.
- 파싱 후 LoginReqDto로 반환하고 강제로 로그인해 인증용 토큰을 생성한다.
- UserDetailsService의 loadUserByUsername을 호출한다.
- 호출 후 로그인이 성공하면 LoginUser 객체 생성 후 시큐리티 세션(SecurityContext) 에 담는다. 로그인이 실패하면 실패 에러를 날린다.
(우리가 JWT를 쓴다 하더라도, 컨트롤러 진입을 하면 시큐리티의 권한체크, 인증체크 도움을 받을 수 있으므로 임시 세션을 만든다.) - JWT 토큰을 생성하고 response 헤더에 JWT 토큰을 담아 브라우저에게 응답한다.
브라우저는 JWT 토큰 만료 시간 전까지 계속 헤더에 JWT 토큰을 담고 있고, 권한이 필요한 페이지에서 JwtAuthorizationFilter 를 호출해 사용한다.
JwtAuthorizationFilter
=> 인가를 수행하는 필터이다.
- 권한이 필요한 모든 주소에서 동작한다.
<진행 순서>
- 헤더에 든 JWT 토큰을 가져와 검증한다.
- 검증이 성공하면 Authentication(LoginUser) 객체를 생성 하고 강제 로그인을 진행해 스프링 시큐리티의 SecutiryContext에 담는다.
- 이때 이 객체는 인증이 완료되었는지랑 권한 체크용으로만 사용한다.
- 검증이 완료되면 체인을 타고 계속 진행한다.
위 필터들을 SecutiryConfig에 등록해줘야 한다.
Jwt 필터에 필요한 토큰 생성 로직과 검증 로직을 구현 해보자
- 토큰 생성하기
인증에 성공하면 LoginUser가 임시 세션에 담기는데, 이 LoginUser를 사용해서 토큰을 생성할 것이다.
// 토큰 생성
public static String create(LoginUser loginUser) {
String jwtToken = JWT.create()
.withSubject("bank")
.withExpiresAt(new Date(System.currentTimeMillis() + JwtVO.EXPIRATION_TIME))
.withClaim("id", loginUser.getUser().getId())
.withClaim("role", loginUser.getUser().getRole()+"") // 문자열 캐스팅
.sign(Algorithm.HMAC512(JwtVO.SECRET));
return JwtVO.TOKEN_PREFIX+jwtToken;
}
설정 내용은 JwtVO 안에 담긴 기본 정보 세팅 파일들을 사용해주었다.
- 토큰 검증하기
인가를 진행할 때 토큰을 검증해야한다. 토큰을 받고 리턴되는 LoginUser 객체를 강제로 시큐리티 임시 세션에 직접 주입할 것이다. (강제 로그인)
// 토큰 검증
public static LoginUser verify(String token) {
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(JwtVO.SECRET)).build().verify(token);
Long id = decodedJWT.getClaim("id").asLong();
String role = decodedJWT.getClaim("role").asString();
User user = User.builder().id(id).role(UserEnum.valueOf(role)).build();
LoginUser loginUser = new LoginUser(user);
return loginUser;
}
이제 Jwt 필터를 SecurityConfig에 적용해주자
만들어둔 Jwt 필터를 그냥 사용할 순 없다. SecurityConfig에 적용해줘야한다.
// JWT 필터 등록이 필요함.
// AbstractHttpConfigurer<본인 클래스명, HttpSecurity>
public class CustomSecurityFilterManager extends AbstractHttpConfigurer<CustomSecurityFilterManager, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); // AuthenticationManager 에 접근 가능.
builder.addFilter(new JwtAuthenticationFilter(authenticationManager));
builder.addFilter(new JwtAuthorizationFilter(authenticationManager));
super.configure(builder);
}
public HttpSecurity build(){
return getBuilder();
}
}
참고로 SecurityFilterChain 클래스에서 인증과 인가 권한 예외를 가로채서 원하는 에러 형태로 예쁘게 만들어줄 수 있다.
백엔드에서 대부분의 에러를 잘 처리해줘야 하기 때문에 잘 기억해두자.
// Exception 가로채기
// 인증 실패
http.exceptionHandling(e -> e.authenticationEntryPoint((request, response, authException) -> {
CustomResponseUtil.fail(response, "로그인을 진행해 주세요.", HttpStatus.UNAUTHORIZED);
}));
// 권한 실패
http.exceptionHandling(e -> e.accessDeniedHandler((request, response, accessDeniedException) -> {
CustomResponseUtil.fail(response, "권한이 없습니다.", HttpStatus.FORBIDDEN);
}));
+) 나머지 코드는 bank-junit 레포 글로벌 에러처리 참고하기
'스터디 기록 > SpringBoot 심화 스터디' 카테고리의 다른 글
Long 타입 비교와 동적 쿼리 그리고 Join (0) | 2024.11.27 |
---|---|
서비스 테스트와 컨트롤러 테스트 (0) | 2024.11.19 |
SpringSecurity 세팅 (0) | 2024.11.06 |
나만의 Exception 만들기 & 유효성검사와 AOP (0) | 2024.11.06 |