[코드로 배우는 스프링 부트 웹 프로젝트] 스프링 부트(Spring Boot) 04

hidihyeonee·2025년 2월 21일
0

2025.02.21 작성

OS : Window
개발환경: IntelliJ IDEA
개발언어: Java
프레임워크: Spring Boot


Spring Initializr는 Spring Boot 프로젝트를 쉽게 생성할 수 있도록 도와주는 도구로,
Spring 공식 사이트(https://start.spring.io)에서 제공하는 서비스.


logging.level.org.springframework.security.web=trace
logging.level.org.zerock=debug

package org.zerock.club.config;

import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@Log4j2
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

이 코드는 Spring Security에서 비밀번호를 안전하게 저장하기 위한 BCrypt 해싱(암호화) 인코더를 빈(Bean)으로 등록하는 것.

BCryptPasswordEncoder()

  • BCryptPasswordEncoder는 비밀번호를 해싱(암호화)하는 가장 많이 사용되는 방식 중 하나.
    내부적으로 Salt(랜덤 값)를 포함하여 해싱하기 때문에, 같은 비밀번호라도 저장될 때마다 다른 해시 값이 생성됨.
    → 보안성이 높음.

PasswordTests

package org.zerock.club.security;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;

@SpringBootTest
public class PasswordTests {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Test
    public void testEncode() {
        String password = "1111";

        String enPw = passwordEncoder.encode(password);

        System.out.println("enPw = " + enPw);

        boolean matchResult = passwordEncoder.matches(password, enPw);

        System.out.println("matchResult = " + matchResult);
    }
}


→ 같은 비밀번호라도 다르게 저장됨 (Salt 때문)

로그인 없이 접속 가능

@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests((auth) -> {
            auth.requestMatchers("/sample/all").permitAll(); // 로그인 없이 접속 가능
        });

        http.formLogin(formLogin -> {
            
        });

        return http.build();
    }

ClubMemberRole.enum

package org.zerock.club.entity;

public enum ClubMemberRole {
    USER, MANAGER, ADMIN;
}

insertDummies() --> 더미데이터 넣기

@Test
    public void insertDummies() {
        IntStream.rangeClosed(1, 100).forEach( i -> {
            ClubMember clubMember = ClubMember.builder()
                    .email("user"+i+"zerock.org")
                    .name("사용자" + i)
                    .fromSocial(false)
                    .password(passwordEncoder.encode("1111"))
                    .build();

            clubMember.addMemberRole(ClubMemberRole.USER);

            if(i > 80) {
                clubMember.addMemberRole(ClubMemberRole.MANAGER);
            }

            if(i > 90) {
                clubMember.addMemberRole(ClubMemberRole.ADMIN);
            }

            repository.save(clubMember);
        });
    }

오타 조심하자

@Test
    public void testRead() {
        Optional<ClubMember> result = repository.findByEmail("user1@zerock.org", false);
        ClubMember clubMember = result.get();
        System.out.println(clubMember);
    }
  • 골뱅이 빼먹어서 회원 찾을 때 오류 남

Spring Security에서 로그인 성공 후 특정 로직을 처리하기 위해 작성된 Custom AuthenticationSuccessHandler

package org.zerock.club.security.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.zerock.club.security.dto.ClubAuthMemberDTO;

import java.io.IOException;

@Log4j2
@RequiredArgsConstructor
public class ClubLoginSuccessHandler implements AuthenticationSuccessHandler {
    private final PasswordEncoder passwordEncoder;
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("----------------------------");
        log.info("onAuthenticationSuccess");

        ClubAuthMemberDTO authMember = (ClubAuthMemberDTO) authentication.getPrincipal();
        boolean fromSocial = authMember.isFromSocial();
        boolean passwordResult = passwordEncoder.matches("1111", authMember.getPassword());
        log.info("fromSocial: " + fromSocial);
        log.info("passwordResult: " + passwordResult);
        log.info("password:"+authMember.getPassword());
        if (fromSocial && passwordResult) {
            redirectStrategy.sendRedirect(request, response, "/member/modify?from=social");
        }
    }
}
  • 클래스 이름은 ClubLoginSuccessHandler.
  • 주요 역할은 사용자가 소셜 로그인으로 처음 로그인한 경우 비밀번호를 변경하도록 유도하는 것.

1. 로그인 성공 후 호출

  • onAuthenticationSuccess() 메서드는 Spring Security에서 로그인에 성공했을 때 호출되는 메서드입니다.

2. 사용자 정보 가져오기

ClubAuthMemberDTO authMember = (ClubAuthMemberDTO) authentication.getPrincipal();
  • 로그인한 사용자의 정보를 ClubAuthMemberDTO 타입으로 가져옴.
  • ClubAuthMemberDTO는 Spring Security의 UserDetails를 구현한 사용자 정보 객체.

3. 소셜 로그인 여부 확인

boolean fromSocial = authMember.isFromSocial();
  • isFromSocial() 메서드를 통해 사용자가 소셜 로그인으로 접속했는지 확인.

4. 초기 비밀번호 확인

boolean passwordResult = passwordEncoder.matches("1111", authMember.getPassword());
  • 비밀번호가 "1111"로 설정되어 있는지 확인.
  • 여기서 "1111"은 소셜 로그인 사용자의 초기 비밀번호로 설정된 값.

5. 비밀번호 변경 페이지로 리다이렉트.

if (fromSocial && passwordResult) {
    redirectStrategy.sendRedirect(request, response, "/member/modify?from=social");
}
  • 소셜 로그인(fromSocial)이면서 비밀번호가 "1111"인 경우, 비밀번호 변경 페이지(/member/modify?from=social)로 리다이렉트.

6. 로그 출력

  • Log4j2를 사용해 로그인 성공 관련 정보를 로그로 남깁니다.

왜 이런 로직을 사용하는가?

  • 보안 상 초기 비밀번호를 유지하지 않도록 하기 위해, 소셜 로그인 사용자가 처음 로그인한 경우 비밀번호를 반드시 변경하도록 유도.
  • 소셜 로그인 사용자의 비밀번호가 기본값("1111")일 때만 비밀번호 변경 페이지로 보내는 방식.

UserDetails

Spring Security에서 사용자의 인증 및 권한 정보를 담는 인터페이스.

  • Spring Security는 인증 과정에서 UserDetails 타입의 객체를 사용해 사용자의 정보(아이디, 비밀번호, 권한 등)를 확인.
  • 예를 들어, 데이터베이스에서 사용자 정보를 가져와 인증하고, 권한(Role)을 확인하여 접근 제어를 할 때 사용.

ClubAuthMemberDTO

package org.zerock.club.security.dto;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Collection;
import java.util.Map;

@Getter
@Setter
@ToString
@Log4j2
public class ClubAuthMemberDTO extends User implements OAuth2User {
    public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.email = username;
        this.fromSocial = fromSocial;
        this.password = password;
    }
    public ClubAuthMemberDTO(String username, String password, boolean fromSocial, Collection<? extends GrantedAuthority> authorities, Map<String, Object> attr) {
        super(username, password, authorities);
        this.email = username;
        this.fromSocial = fromSocial;
        this.password = password;
        this.attr = attr;
    }

    private String email;
    private String name;
    private String password;
    private boolean fromSocial;
    private Map<String, Object> attr;

    @Override
    public Map<String, Object> getAttributes() {
        return this.attr;
    }
}
  • Spring Security에서 사용자 인증 및 권한 관리를 위해 확장된 DTO(Data Transfer Object)
  • 특히, 소셜 로그인(OAuth2) 및 일반 로그인을 모두 지원하기 위해 다음 두 가지 클래스를 상속받고 있음.
    • User: Spring Security에서 기본 제공하는 사용자 정보 클래스 (UserDetails를 구현)
    • OAuth2User: OAuth2 로그인 시 사용자 정보를 담는 인터페이스
      • OAuth2User를 구현하여 OAuth2 소셜 로그인(Google, Facebook 등)을 사용할 수 있음.

사용자 정보 조회

Optional<ClubMember> result = clubMemberRepository.findByEmail(username, false);
  • username(이메일)을 기준으로 DB에서 사용자 정보를 조회합니다.
  • findByEmail() 메서드의 두 번째 인자 false는 소셜 로그인이 아닌 일반 로그인을 의미합니다.
  • Optional 타입으로 받아서 존재하지 않는 경우 예외를 던집니다.

Optional

  • Optional은 NullPointerException 방지와 가독성 향상을 위해 사용.
  • 값이 없을 수 있음을 명확하게 표현하여 코드를 읽는 사람이 이해하기 쉬움.
  • Optional을 사용하면 안전한 null 처리를 강제할 수 있음.
  • 필드 또는 컬렉션에는 사용하지 말고, 메서드 반환값이나 지역변수에만 사용해야 함.
  • .get() 사용은 지양하고, orElse(), orElseThrow() 등 안전한 메서드를 사용함.
profile
벨로그 쫌 재밌네?

0개의 댓글