랑아
article thumbnail
Published 2023. 6. 11. 17:01
Spring Security - JWT 인증 3 Spring

Spring Security에서의 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'
}

...

애플리케이션 실행

  • 애플리케이션을 실행한 후, http://localhost:8080으로 접속하면 Spring Security에서 제공하는 로그인 화면이 보여야 함
  • 로그인 화면에서 Username과 Password를 입력해야 함
    • Username : user
    • Password : intelliJ 로그에 출력되는 Using generated security password: ... ... 부분을 복사 및 붙여넣기 함
      • intelliJ 로그에 출력되는 Password는 애플리케이션을 실행할 때마다 바뀜
  • 정상적으로 로그인에 성공했다면 Whitelabel Error Page가 출력됨
    • 로그인 인증에는 성공했지만 기본적으로 Controller 같은 엔드포인트가 없어서 발생하는 404 에러로 Spring Security에 문제가 있어서 발생한 에러는 아님

SecurityConfiguration 추가

  • Spring Security를 이용한 보안 강화를 위해 최소한의 보안 구성을 함
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;
    }
}

  • H2 웹 콘솔의 화면 자체가 내부적으로 태그를 사용하고 있기 때문에 개발 환경에서는 H2 웹 콘솔을 정상적으로 사용할 수 있도록 (1)과 같이 .frameOptions().sameOrigin() 을 추가함
    • .frameOptions().sameOrigin() 을 호출하면 동일 출처로부터 들어오는 request만 페이지 렌더링을 허용함
  • (2)와 같이 CSRF(Cross-Site Request Forgery) 공격에 대한 Spring Security에 대한 설정을 비활성화함
    • 로컬 환경에서는 CSRF 공격에 대한 설정이 필요하지 않음
    • 만약, csrf().disable() 설정하지 않는다면 403 에러로 인해 정상적인 접속이 불가능함
  • (3)에서는 CORS 설정을 추가함
    • .cors(withDefaults()) 일 경우, corsConfigurationSource라는 이름으로 등록된 Bean을 이용함
    • ORS를 처리하는 가장 쉬운 방법은 CorsFilter를 사용하는 것인데 CorsConfigurationSource Bean을 제공함으로써 CorsFilter를 적용할 수 있음
  • CSR(Client Side Rendering) 방식에서 주로 사용하는 JSON 포맷으로 Username과 Password를 전송하는 방식을 사용할 것이므로 (4)와 같이 폼 로그인 방식을 비활성화함
  • HTTP Basic 인증은 request를 전송할 때마다 Username/Password 정보를 HTTP Header에 실어서 인증을 하는 방식으로 현재 사용하지 않을 것으므로 (5)와 같이 HTTP Basic 인증 방식을 비활성화함
    • 폼 로그인과 HTTP Basic 인증을 disable하면 해당 인증과 관련된 Security Filter(UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter 등)가 비활성화됨
  • (6)에서는 JWT를 적용하기 전이므로 우선은 모든 HTTP request 요청에 대해서 접근을 허용하도록 설정함
    • JWT 적용 후, URL 별로 적절한 권한을 적용할 예정
  • (7)에서 PasswordEncoder Bean 객체를 생성함
  • (8)에서는 CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책을 설정함
    • (8-1)에서 setAllowedOrigins()을 통해 모든 출처(Origin)에 대해 스크립트 기반의 HTTP 통신을 허용하도록 설정하며, 이 설정은 운영 서버 환경에서 요구사항에 맞게 변경이 가능함
    • (8-2)에서는 setAllowedMethods()를 통해 파라미터로 지정한 HTTP Method에 대한 HTTP 통신을 허용함
    • (8-3)에서는 CorsConfigurationSource 인터페이스의 구현 클래스인 UrlBasedCorsConfigurationSource 클래스의 객체를 생성함
    • (8-4)에서는 모든 URL에 앞에서 구성한 CORS 정책(CorsConfiguration)을 적용함

회원 가입 로직 수정

  • Spring Security 적용으로 회원 등록 시, 회원의 인증과 관련된 정보(패스워드. 사용자 권한)를 추가하기 위함
  1. Dto 클래스에 패스워드 필드 추가
    • 실제 서비스에서는 사용자가 회원 가입 시, 패스워드를 한 번만 입력하는 것이 아니라 사용자가 입력한 패스워드가 맞는지 재확인하기 위해 패스워드 입력 확인 필드가 추가로 존재하는 경우가 대부분이고, 입력한 두 패스워드가 일치하는지를 검증하는 로직이 필요함
    • 패스워드의 생성 규칙(대/소문자, 패스워드 길이, 특수 문자 포함 여부 등)에 대한 유효성 검증도 실시함
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;
    }

    ...
}

  1. 엔티티 클래스에 패스워드 필드 추가
@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) 추가
    @Column(length = 100, nullable = false)
    private String password;

    ...
    // (2) 추가
    @ElementCollection(fetch = FetchType.EAGER)
    private List<String> roles = new ArrayList<>();

    ...
}
  • (1)과 같이 Member 엔티티 클래스에 패스워드 필드를 추가
    • password는 암호화되어 저장되기 때문에 열의 길이는 100으로 지정하였으며, 패스워드 입력 규칙에 따라서 password 길이는 달라질 수 있음
  • (2)에서는 @ElementCollection 애너테이션을 이용해 사용자 등록 시, 사용자의 권한을 등록하기 위한 권한 테이블을 생성함

  1. 사용자 등록 시, 패스워드와 사용자 권한 저장
@Transactional
@Service
public class MemberService {
    private final MemberRepository memberRepository;
    private final ApplicationEventPublisher publisher;

    // (1) 추가
    private final PasswordEncoder passwordEncoder;
    private final CustomAuthorityUtils authorityUtils;

    // (2) 생성자 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) 추가: Password 암호화
        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;
    }

    ...
}
  • (1)과 (2)에서는 PasswordEncoder와 CustomAuthorityUtils 클래스를 DI 받도록 필드를 추가함
  • (3)에서는 패스워드를 단방향 암호화함
  • (4)에서는 등록하는 사용자의 권한 정보를 생성함

'Spring' 카테고리의 다른 글

Spring Security - JWT 인증 5  (0) 2023.06.12
Spring Security - JWT 인증 4  (0) 2023.06.11
Spring Security - JWT 인증 2  (0) 2023.06.11
Spring Security - JWT 인증 1  (1) 2023.06.10
Spring Security 기본 8  (0) 2023.06.10
profile

랑아

@RangA

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!