JWT 적용
JWT 의존 라이브러리 추가
- build.gradle
...
...
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.mapstruct:mapstruct:1.5.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'com.google.code.gson:gson'
implementation 'org.springframework.boot:spring-boot-starter-security' // (1)
// (2) JWT 기능을 위한 jjwt 라이브러리
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
...
...
- SecurityConfiguration 추가 JWT를 적용해 보기 전에 Spring Security를 이용한 보안 강화를 위해 최소한의 보안 구성을 먼저 하겠습니다.
ackage com.codestates.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import static org.springframework.security.config.Customizer.withDefaults;
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin() // (1)
.and()
.csrf().disable() // (2)
.cors(withDefaults()) // (3)
.formLogin().disable() // (4)
.httpBasic().disable() // (5)
.authorizeHttpRequests(authorize -> authorize
.anyRequest().permitAll() // (6)
);
return http.build();
}
// (7)
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// (8)
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); // (8-1)
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE")); // (8-2)
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // (8-3)
source.registerCorsConfiguration("/**", configuration); // (8-4)
return source;
}
}
(1)에서는 H2 웹 콘솔을 정상적으로 사용하기 위해 .frameOptions().sameOrigin()을 추가합니다.
- .frameOptions().sameOrigin()을 호출하면 동일 출처로부터 들어오는 request만 페이지 렌더링을 허용합니다.
(2)에서는 CSRF(Cross-Site Request Forgery) 공격에 대한 Spring Security 설정을 비활성화합니다.
- 우리는 로컬 환경에서 Spring Security에 대한 학습을 진행하므로 CSRF 공격에 대한 설정이 필요하지 않습니다.
(3)에서는 CORS 설정을 추가합니다. .cors(withDefaults()) 일 경우, corsConfigurationSource라는 이름으로 등록된 Bean을 이용합니다.
- CORS를 처리하는 가장 쉬운 방법은 CorsFilter를 사용하는 것인데 CorsConfigurationSource Bean을 제공함으로써 CorsFilter를 적용할 수 있습니다.
(4)에서는 폼 로그인 방식을 비활성화합니다.
- HTTP Basic 인증 방식은 우리 코스에서 사용하지 않으므로 HTTP Basic 인증 방식을 비활성화합니다.
(7)에서는 PasswordEncoder Bean 객체를 생성합니다.
(8)에서는 CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책을 설정합니다.
- setAllowedOrigins()을 통해 모든 출처(Origin)에 대해 스크립트 기반의 HTTP 통신을 허용하도록 설정합니다.
- setAllowedMethods()를 통해 파라미터로 지정한 HTTP Method에 대한 HTTP 통신을 허용합니다.
- CorsConfigurationSource 인터페이스의 구현 클래스인 UrlBasedCorsConfigurationSource 클래스의 객체를 생성하여 모든 URL에 CORS 정책을 적용합니다.
회원 가입 로직 수정
- MemberDto.Post 패스워드 필드 추가
public class MemberDto {
@Getter
@AllArgsConstructor
public static class Post {
@NotBlank
@Email
private String email;
// (1) 패스워드 필드 추가
@NotBlank
private String password;
@NotBlank(message = "이름은 공백이 아니어야 합니다.")
private String name;
@Pattern(regexp = "^010-\\d{3,4}-\\d{4}$",
message = "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.")
private String phone;
}
...
...
}
- Member 엔티티 클래스에 패스워드 필드 추가
@NoArgsConstructor
@Getter
@Setter
@Entity
public class Member extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(nullable = false, updatable = false, unique = true)
private String email;
// (1) 추가 Member 엔티티 클래스에도 패스워드 필드를 추가
@Column(length = 100, nullable = false)
private String password;
...
...
// (2) 추가 @ElementCollection 애너테이션을 이용해 사용자 등록 시, 사용자의 권한을 등록하기 위한 권한 테이블을 생성
@ElementCollection(fetch = FetchType.EAGER)
private List<String> roles = new ArrayList<>();
...
...
}
- 사용자 등록 시, 패스워드와 사용자 권한 저장
import com.codestates.auth.utils.CustomAuthorityUtils;
...
...
@Transactional
@Service
public class MemberService {
private final MemberRepository memberRepository;
private final ApplicationEventPublisher publisher;
// (1) 추가 PasswordEncoder와 CustomAuthorityUtils 클래스를 DI 받도록 필드를 추가
private final PasswordEncoder passwordEncoder;
private final CustomAuthorityUtils authorityUtils;
// (2) 추가 PasswordEncoder와 CustomAuthorityUtils 클래스를 DI 받도록 필드를 추가
public MemberService(MemberRepository memberRepository,
ApplicationEventPublisher publisher,
PasswordEncoder passwordEncoder,
CustomAuthorityUtils authorityUtils) {
this.memberRepository = memberRepository;
this.publisher = publisher;
this.passwordEncoder = passwordEncoder;
this.authorityUtils = authorityUtils;
}
public Member createMember(Member member) {
verifyExistsEmail(member.getEmail());
// (3) 추가: 생성자 DI용 파라미터 추가 패스워드를 단방향 암호화
String encryptedPassword = passwordEncoder.encode(member.getPassword());
member.setPassword(encryptedPassword);
// (4) 추가: DB에 User Role 저장 사용자의 권한 정보를 생성
List<String> roles = authorityUtils.createRoles(member.getEmail());
member.setRoles(roles);
Member savedMember = memberRepository.save(member);
publisher.publishEvent(new MemberRegistrationApplicationEvent(this, savedMember));
return savedMember;
}
...
...
}
JWT 자격 증명을 위한 로그인 인증 구현
- 사용자의 로그인 인증 성공 후, JWT가 클라이언트에게 전달되는 과정
- 클라이언트가 서버 측에 로그인 인증 요청(Username/Password를 서버 측에 전송)
- 로그인 인증을 담당하는 Security Filter(JwtAuthenticationFilter)가 클라이언트의 로그인 인증 정보 수신
- Security Filter가 수신한 로그인 인증 정보를 AuthenticationManager에게 전달해 인증 처리를 위임
- AuthenticationManager가 Custom UserDetailsService(MemberDetailsService)에게 사용자의 UserDetails 조회를 위임
- Custom UserDetailsService(MemberDetailsService)가 사용자의 크리덴셜을 DB에서 조회한 후, AuthenticationManager에게 사용자의 UserDetails를 전달
- AuthenticationManager가 로그인 인증 정보와 UserDetails의 정보를 비교해 인증 처리
- JWT 생성 후, 클라이언트의 응답으로 전달
1번부터 7번 과정 중에서 우리는 JwtAuthenticationFilter 구현(2번 ~ 3번, 7번), MemberDetailsService(5번)을 구현합니다.
4번과 6번은 Spring Security의 AuthenticationManager가 대신 처리해 주므로 신경 쓸 필요가 없습니다.