2. 인증은 언제 어떻게 이루어지는가?
코드까지 전부 기록해 놓은 내용은 노션에서 확인할 수 있다.
해당 코드를 사용한 프로젝트는 깃허브에서 확인할 수 있다.
[인증]에 대해서 자세히
SecurityConfig.SecurityFilterChain
@Bean // 스프링 컨테이너에 의해 관리되는 빈 객체를 생성
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// .cors(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable) // CSRF(Cross-Site Request Forgery) 보호 기능을 비활성화
.formLogin(AbstractHttpConfigurer::disable) // 스프링 시큐리티의 기본 로그인 페이지를 비활성화
.httpBasic(AbstractHttpConfigurer::disable) // HTTP 기본 인증 비활성화
// HTTP 요청에 대한 보안 규칙을 설정
.authorizeHttpRequests(auth -> auth
.requestMatchers(JwtConstants.WHITELIST).permitAll() // 지정된 경로들은 인증 없이 접근 허용
.requestMatchers("/admin/**").hasRole("ADMIN") // "/admin/**" 경로는 'ADMIN' 역할을 가진 사용자만 접근 가능
.anyRequest().authenticated()) // 그 외 모든 요청은 인증을 요구
// JWT 필터 추가
.addFilterBefore(jwtVerifyFilter, UsernamePasswordAuthenticationFilter.class)
// 세션 관리 설정
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션을 생성하지 않고, 상태를 유지하지 않는 정책 설정
.build(); // HttpSecurity 객체를 사용하여 SecurityFilterChain을 생성
}
- 시큐리티 주요 설정이라고 볼 수 있는 SecurityFilterChain이다.
- 각 내용에 대한 설명은 주석으로 달아놨으며 Jwt관련 로직만 들여다보자.
JwtVerifyFilter.doFilterInternal
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String authHeader = request.getHeader(JwtConstants.JWT_HEADER);
checkAuthorizationHeader(authHeader); // header 형식 검사
String token = JwtUtil.getTokenFromHeader(authHeader);
if (redisUtil.isTokenBlacklisted(token)) { // 토큰 블랙리스트 검사
throw new CustomJwtException("로그인 세션이 만료되었습니다.");
}
Authentication authentication = JwtUtil.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (Exception e) {
handleAuthenticationError(response, e);
}
}
- JwtVerifyFilter 클래스의 doFilterInternal
: 시큐리티의 필터를 구현할 때 사용되는 메소드로, 실제 필터링 로직을 구현한다.
Authentication authentication = JwtUtil.getAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication);
토큰 검증이 성공적으로 이루어지면 위 코드가 Authenticaion 객체를 생성한 한 후 SecurityContextHolder에 저장하게 된다. - 이번엔 위의 JwtUtil.getAuthenticaion을 자세히 들여다보자
JwtUtil.getAuthentication
public class JwtUtil {
public static String secretKey = JwtConstants.key;
// 헤더에 "Bearer XXX" 형식으로 담겨온 토큰을 추출한다
public static String getTokenFromHeader(String header) {
return header.split(" ")[1];
}
public static String generateToken(Map<String, Object> valueMap, int validTime) {
SecretKey key = null;
try {
key = Keys.hmacShaKeyFor(JwtUtil.secretKey.getBytes(StandardCharsets.UTF_8));
} catch(Exception e){
throw new RuntimeException(e.getMessage());
}
return Jwts.builder()
.setHeader(Map.of("typ","JWT"))
.setClaims(valueMap)
.setIssuedAt(Date.from(ZonedDateTime.now().toInstant()))
.setExpiration(Date.from(ZonedDateTime.now().plusMinutes(validTime).toInstant()))
.signWith(key)
.compact();
}
public static Authentication getAuthentication(String token) {
Map<String, Object> claims = validateToken(token);
String email = (String) claims.get("email");
String name = (String) claims.get("name");
String role = (String) claims.get("role");
UserRole userRole = UserRole.valueOf(role);
UserDocument userDocument = UserDocument.builder().email(email).name(name).userRole(userRole).build();
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority(userDocument.getUserRole().getValue()));
PrincipalDetail principalDetail = new PrincipalDetail(userDocument, authorities);
return new UsernamePasswordAuthenticationToken(principalDetail, "", authorities);
}
public static Map<String, Object> validateToken(String token) {
Map<String, Object> claim = null;
try {
SecretKey key = Keys.hmacShaKeyFor(JwtUtil.secretKey.getBytes(StandardCharsets.UTF_8));
claim = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token) // 파싱 및 검증, 실패 시 에러
.getBody();
} catch(ExpiredJwtException expiredJwtException){
throw new CustomExpiredJwtException("토큰이 만료되었습니다", expiredJwtException);
} catch(Exception e){
throw new CustomJwtException("Error");
}
return claim;
}
// 토큰이 만료되었는지 판단하는 메서드
public static boolean isExpired(String token) {
try {
validateToken(token);
} catch (Exception e) {
return (e instanceof CustomExpiredJwtException);
}
return false;
}
// 토큰의 남은 만료시간 계산
public static long tokenRemainTime(long expTime) {
long remainMs = expTime - System.currentTimeMillis();
return remainMs / (1000 * 60);
}
}
- getTokenFromHeader : Header에서 토큰 추출
- generateToken : 사용자 정보를 포함하는 JWT 토큰을 생성
→ kakaoAuthService에서 담당하기 때문에 없어져야 하는 메소드 (즉, 사용되지 않음. 리팩터링 대상) - getAuthentication : JWT 토큰에서 인증 정보를 추출하고, 이를 기반으로 Authentication 객체를 생성
- validateToken : JWT 토큰을 검증하고, Claim을 추출
- Claim
- 토큰 내 포함된 정보를 의미.
- kakaoAuthService에서 생성한 클레임은 JWT에 포함되어 전송
- JWT가 클라이언트에서 서버로 전달될 때, JwtUtil 클래스에서 검증 및 추출되는 클레임은 kakaoAuthService에서 처음 생성한 클레임과 동일
- Claim
'Back-End > SpringBoot' 카테고리의 다른 글
[SpringSecurity 뜯어보기] oAuth2 Resource 라이브러리와 Custom JWT 간의 호환 (1) | 2025.01.15 |
---|---|
[SpringSecurity 뜯어보기] oAuth2 라이브러리를 통한 인증객체 주입 (1) | 2024.12.28 |
3. JWT + oAuth2 카카오 로그인 삽질일기 (2) | 2024.06.29 |
1. JWT + oAuth2 카카오 로그인 삽질일기 (0) | 2024.06.28 |
0. JWT + oAuth2 카카오 로그인 삽질일기 (3) | 2024.06.07 |