토큰기반 인증 절차

- 클라이언트가 서버 측에 로그인 요청
- 로그인 인증을 담당하는 Security Filter가 클라이언트 로그인 인증 정보 수신
- Security Filter가 수신한 로그인 인증 정보를 AuthenticationManager 에게 전달해 인증 처리를 위임
- AuthenticationManager 가 CustomUserDetatilsService에게 사용자의 UserDetatils 조회를 위임
- Custom UserDetailsService가 사용자의 크리덴셜을 DB에서 조회한 후, AuthenticationManager에게 사용자의 UserDetails를 전달
- AuthenticationManager가 로그인 인증 정보와 UserDetails의 정보를 비교해 인증 처리
- JWT 생성 후, 클라이언트의 응답으로 전달
JWT 생성
public class JwtTokenizer {
public String encodeBase64SecretKey(String secretkey) { // Secret Key의 byte[]를 Base64형식의 문자열로 인코딩
return Encoders.BASE64.encode(secretkey.getBytes(StandardCharsets.UTF_8));
}
public String generateAccessToken(Map<String, object> claims, // 인증된 사용자에게 JWT를 최초로 발급해주기 위한 JWT 생성
String subject,
Data expiration,
String base64EncodedSecretKey) {
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey); // Key 객체를 얻어옴
return Jwts.builder()
.setClaims(claims) // JWT에 포함 시킬 Custom Claims를 추가
.setSubject(subject) // JWT에 대한 제목 추가
.setIssuedAt(Calendar.getInstance().getTime()) // JWT 발행 일자 설정
.setExpiration(expiration) // JWT 만료일시 지정
.signWith(key) // 서명을 위한 Key 객체 설정
.compact(); // JWT 생성 및 직렬화
}
public String generateRefreshToken(String subject, Date expiration, String base64EncodedSecretKey) { // Access Token이 만료될 경우 Access Token 새로 생성
Key key = getKeyFromBase64EncodedKey(base64EncodedSecretKey);
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(Calendar.getInstance().getTime())
.setExpiration(expiration)
.signWith(key)
.compact();
}
...
...
private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) { // JWT의 서명에 사용될 Secret Key 생성
byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey); // Base64 형식으로 인코딩된 Secret Key를 디코딩 한 후 반환
Key key = Keys.hmacShaKeyFor(keyBytes); // HMAC 알고리즘을 적용한 Key 객체 생성
return key;
}
}
JWT 적용을 위한 사전 작업
[ SecurityConfiguration 추가 ]
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // H2 개발활경 설정
.and()
.csrf().disable() // csrf 공격에 대한 설정 비활성화
.cors(withDefaults()) // CORS 설정 추가
.formLogin().disable() // 폼 로그인 방식 비활성화
.httpBasic().disable() // Http Basic 인증 비화성화
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll() // HTTP request 요청에 대한 접급 허용
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() { // PasswordEncoder Bean 객체 생성
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
CorsConfigurationSource corsConfigurationSource() { // CORS 정책 설정
CorsConfiguration configuration = CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // 모든 출처에 대해 스크립트 기반의 HTTP 통신 허용
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PATCH", "DELETE")); // 파라미터로 지정한 HTTP Method에 대한 HTTP 통신 허용
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigyrationSource(); // CorsCofigurationSource 인터페이스 구현 클래스인 UrlBasedCorsConfigyrationSource 객체 생성
source.registerCorsConfiguration("\/**", configuration); // CORS 정책 적용
return source;
}
}
로그인 인증 정보 역직렬화를 위한 LoginDTO 클래스 생성
LoginDto
@Getter
public class LoginDto {
private String username;
private String password;
}
JwtTokenizer
// spring bean 등록
@Component
public class JwtTokenizer {
@Getter
@Value("${jwt.key}")
private String secretKey; // JWT 생성 및 검증 시 사용되는 Secret Key
@Getter
@Value("${jwt.access-token-expiration-minutes}")
private int accessTokenExpirationMinutes; // Access Token에 대한 만료 시간 정보
@Getter
@Value("${jwt.refresh-token-expiration-minutes}")
private int refreshTokenExpirationMinutes; // Refresh Token에 대한 만료 시간 정보
...
...
// JWT의 만료 일시를 지정하기 위한 메서드로 JWT 생성시 사용
public Date getTokenExpiration(int expirationMinutes) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, expirationMinutes);
Date expiration = calendar.getTime();
return expiration;
}
private Key getKeyFromBase64EncodedKey(String base64EncodedSecretKey) {
byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
Key key = Keys.hmacShaKeyFor(keyBytes);
return key;
}
}
application.yml
...
...
jwt:
key: ${JWT_SECRET_KEY} // 민감한 정보는 시스템 환경 변수에 로드
access-token-expritation-minutes: 30
refresh-token-expritation-minutes: 420
Custom Security Filter
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { // Username/Password 기반의 인증을 처리하기 위해 UsernamePasswordAuthenticationFilter 상속
private final AuthenticationManager authenticationManager;
private final JwtTokenizer jwtTokenizer;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenizer jwtTokenizer) {
this.authenticationManager = authenticationManager; // 로그인 인증 정보를 전달 받아 UserDetailsService와 인터랙션 한 뒤 인증 여부를 판단
this.jwtTokenizer = jwtTokenizer; // 클라이언트가 인증에 성공할 경우, JWT를 생성 및 발급
}
// 메서드 내부에서 인증을 시도하는 로직 구현
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
ObjectMapper objectMapper = new ObjectMapper(); // DTO 클래스로 역직렬화 하기 위해 ObjectMapper 인스턴스를 생성
LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class); // 역직렬화
// UsernamePasswordAuthenticationToken 생성
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());
return authenticationManager.authenticate(authenticationToken); // UsernamePasswordAuthenticationToken을 전달하면서 인증처리
}
// 클라이언트의 인증 정보를 이용해 인증에 성공할 경우 호출
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) {
Member member = (Member) authResult.getPrincipal(); // Member 엔티티 클래스의 객체를 얻음
String accessToken = delegateAccessToken(member); // Access Token 생성
String refreshToken = delegateRefreshToken(member); // Refresh Token 생성
response.setHeader("Authorization", "Bearer " + accessToken); // respone header에 Access Token 추가
response.setHeader("Refresh", refreshToken); // response header에 Refresh Token 추가
}
// Access Token 생성
private String delegateAccessToken(Member member) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", member.getEmail());
claims.put("roles", member.getRoles());
String subject = member.getEmail();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
String accessToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);
return accessToken;
}
// Refresh Token 생성
private String delegateRefreshToken(Member member) {
String subject = member.getEmail();
Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);
return refreshToken;
}
}
SecurityConfiguration
@Configuration
public class SecurityConfiguration {
private final JwtTokenizer jwtTokenizer;
public SecurityConfiguration(JwtTokenizer jwtTokenizer) {
this.jwtTokenizer = jwtTokenizer;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors(withDefaults())
.formLogin().disable()
.httpBasic().disable()
.apply(new CustomFilterConfigurer()) // Custom Configurer를 추가해 커스터마이징된 Configuration 추가
.and()
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll()
);
return http.build();
}
...
...
// 구현된 JwtAuthenticationFilter를 등록하는 역할
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception { // Configuration 커스터마이징
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); // AuthenticationManager 객체를 얻어옴
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer); // AuthenticationManager와 JwtTokenizer를 DI
jwtAuthenticationFilter.setFilterProcessesUrl("/v11/auth/login"); // request URL인 /login을 /v11/auth/login으로 변경
builder.addFilter(jwtAuthenticationFilter); // JwtAuthenticationFilter를 Spring Security Filter Chain에 추가
}
}
}