728x90
🧩 전반적인 구조 한눈에 보기
[사용자 로그인 요청(OAuth)] → [SecurityConfig - 설정]
↓
Spring이 OAuth2UserService를 호출
↓
[MemberService] → OAuth2 플랫폼에서 사용자 정보 받아옴
↓
[MemberRepository] 통해 DB에서 사용자 찾기 or 회원가입
↓
[Member] 엔티티로 DB 저장
↓
[MemberDetails]로 스프링 시큐리티에 사용자 정보 전달 (인증객체로 사용)
이 흐름을 기준으로 생각하면 코드이해가 더 잘된다!!!
✅ 1. ApiController - API 테스트용 컨트롤러
@RestController
public class ApiController {
@GetMapping("/user/test")
public String userTest() {
return "User Test!";
}
@GetMapping("/admin/test")
public String adminTest() {
return "Admin Test!";
}
}
✔️ 역할
- OAuth 인증이 완료된 사용자가 실제 접근하게 될 리소스를 테스트하기 위한 컨트롤러.
- /user/test → USER 또는 ADMIN 권한이 있어야 접근 가능
- /admin/test → ADMIN 권한만 있어야 접근 가능 (이건 SecurityConfig에서 지정)
✅ 2. SecurityConfig - 보안 설정 클래스
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.oauth2Login(Customizer.withDefaults())
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/login", "/sign-up").anonymous()
.requestMatchers("user/**").hasAnyAuthority("USER", "ADMIN")
.requestMatchers("admin/**").hasAnyAuthority("ADMIN")
.anyRequest().authenticated();
})
.build();
}
}
✔️ 핵심 설명
- csrf.disable() : API 서버로 사용하거나 OAuth2에서는 비활성화하는 경우가 많음.
- formLogin.disable() : 일반적인 로그인 폼을 사용하지 않겠다는 의미. 우리는 소셜 로그인(OAuth2)을 쓰니까.
- oauth2Login(Customizer.withDefaults()) : 기본 설정으로 소셜 로그인 사용하겠다는 의미.
- authorizeHttpRequests() :
- anonymous() : 인증 안 된 사람만 접근 가능.
- hasAnyAuthority(...) : 해당 권한을 가진 사용자만 접근 가능 (여기선 문자열 권한 명시함).
- authenticated() : 로그인한 사용자만 접근 가능.
✅ 3. MemberService - 소셜 로그인 사용자 처리 로직(OAuth2 사용자 처리 서비스)
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
String provider = userRequest.getClientRegistration().getRegistrationId();
OAuth2User oAuth2User = super.loadUser(userRequest);
Map<String, Object> attributes = oAuth2User.getAttributes();
String findName = attributes.get("name").toString();
String email = attributes.get("email").toString();
Optional<Member> memberOptional = memberRepository.findByEmail(email);
Member member = memberOptional.orElseGet(() -> {
Member saved = Member.builder()
.name(findName)
.email(email)
.provider(provider)
.build();
return memberRepository.save(saved);
});
return MemberDetails.builder()
.name(member.getName())
.role(member.getRole())
.attributes(attributes)
.build();
}
}
❓ 이건 뭐냐면?
→ OAuth 로그인 후, 사용자 정보를 가져오고, 그 사용자가 DB에 있는지 확인하거나 저장하고,
그걸 바탕으로 인증에 사용할 MemberDetails 객체를 만들어주는 클래스
🎯 핵심 포인트:
- 스프링이 자동으로 이걸 호출함
- DefaultOAuth2UserService를 상속해서 loadUser()를 오버라이딩하면 우리가 원하는 로직으로 "로그인 후 처리" 가능
🛠 역할 순서대로 설명:
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
이 함수는 OAuth 로그인 완료 후 자동으로 호출됨
- super.loadUser(userRequest) → 구글/네이버에서 사용자 정보 받아옴
- 사용자 이메일을 기준으로 DB에 있는지 확인
- 없다면 Member 객체 만들어서 저장
- 인증 정보로 사용할 MemberDetails 객체 만들어서 리턴 → Spring Security가 내부에서 인증에 사용
🧩 MemberService 흐름 예시:
구글로 로그인 → 사용자 정보 받음 (name, email 등)
→ 우리 DB에 저장된 이메일이 있는지 확인
→ 있으면 그대로 사용
→ 없으면 새로 회원가입 (DB 저장)
→ Spring Security에 인증 정보(MemberDetails) 전달
✔️ 코드 분석 시작
@Service
@Transactional
@RequiredArgsConstructor
public class MemberService extends DefaultOAuth2UserService {
✔️ 기본 설명
- DefaultOAuth2UserService를 상속 → Spring Security가 소셜 로그인 사용자 정보를 가져올 때 사용하는 기본 구현체를 오버라이딩.
- 여기서 유저 정보 받아서 DB에 저장하거나 기존 유저 찾는 로직을 커스터마이징함.
✔️ 주요 메서드 설명
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
- 이 메서드는 소셜 로그인 후 사용자 정보를 받아올 때 호출됨.
- OAuth2UserRequest에는 어떤 제공자인지, 엑세스 토큰 등이 담겨 있음.
OAuth2User oAuth2User = super.loadUser(userRequest);
- 소셜 제공자로부터 유저 정보를 불러옴. (name, email 등)
Map<String, Object> attributes = oAuth2User.getAttributes();
- 사용자 정보 Map. 구글일 경우 이름: name, 이메일: email 등이 들어있음.
Optional<Member> memberOptional = memberRepository.findByEmail(email);
- 이미 DB에 존재하는 이메일인지 확인.
Member member = memberOptional.orElseGet(() -> {
Member saved = Member.builder()
.name(findName)
.email(email)
.provider(provider)
.build();
return memberRepository.save(saved);
});
- 존재하지 않으면 새로 회원 가입 → DB 저장.
return MemberDetails.builder()
.name(member.getName())
.role(member.getRole())
.attributes(attributes)
.build();
- 인증 객체로 사용할 MemberDetails 객체 반환. (Spring Security가 내부적으로 이 객체를 이용해 인가 처리를 함)
✅ 4. MemberDetails - OAuth2 사용자 정보 구현체
@Getter
public class MemberDetails implements OAuth2User {
private String name;
private Map<String, Object> attributes;
@Setter
private String role;
@Builder
public MemberDetails(String name, String role, Map<String, Object> attributes) {
this.name = name;
this.role = role;
this.attributes = attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role));
}
}
❓ 이건 뭐냐면?
→ Spring Security에서 "로그인한 사용자"를 나타내는 인증 객체
🛠 역할:
- 로그인한 사용자의 권한 정보 (ROLE_USER, ROLE_ADMIN 등)를 담고 있음
- Spring Security는 이 객체로 "인가(authorization)" 처리를 함
- Spring Security에서 인증된 사용자 객체로 사용됨
- 권한 (role) 을 통해 인가 처리 가능
🔐 중요한 부분:
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role));
}
- 사용자 권한을 SimpleGrantedAuthority로 감싸서 리턴해줌 → Spring Security는 여기서 권한 읽음
- 예: role = "ADMIN" → new SimpleGrantedAuthority("ADMIN")
📌 왜 우리가 직접 만들어야 해?
→ Spring Security의 기본 OAuth2User는 우리가 원하는 필드(name, role 등)를 다루기 어려움
→ 우리가 원하는 필드를 가진 MemberDetails 객체로 커스터마이징
✔️ 코드 분석 시작
public class MemberDetails implements OAuth2User {
- OAuth2User 인터페이스를 구현 → Spring Security에서 OAuth 인증 정보를 이 클래스를 통해 처리함.
✔️ 핵심 필드
private String name;
private String role;
private Map<String, Object> attributes;
✔️ 주요 메서드
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role));
}
- 현재 로그인한 사용자의 권한을 반환함 (ROLE_USER, ROLE_ADMIN 등)
- SimpleGrantedAuthority를 통해 권한 객체 생성.
✅ 5. Member - 회원 도메인 엔티티
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String nickname;
private String role = "USER";
private String email;
private String provider;
private LocalDateTime signedAt = LocalDateTime.now();
@Builder
public Member(String name, String nickname, String email, String provider) {
this.name = name;
this.nickname = nickname;
this.email = email;
this.provider = provider;
}
}
❓ 이건 뭐냐면?
→ 데이터베이스의 member 테이블과 연결된 JPA 엔티티 클래스
🛠 역할:
- 사용자의 정보를 담는 객체 (name, email, role, provider 등)
- 이 객체가 그대로 DB의 행(Row) 하나가 됨
🧱 예시:
Member member = Member.builder()
.name("철수")
.email("chulsoo@gmail.com")
.provider("google")
.build();
이렇게 하면 DB에 이런 식으로 저장될 수 있어:
id | name | provider | role | signed_at | |
1 | 철수 | chulsoo@gmail.com | USER | 2025-04-16 14:30:00 |
🔗 왜 만들어?
- DB와 데이터를 주고받으려면 “엔티티”가 필요하기 때문에
- OAuth 로그인으로 받은 정보(email 등)를 이걸로 저장하고 불러오려고 만든 거임!
✔️ 필드 설명
- @Id, @GeneratedValue : 기본 키 자동 생성
- email, provider : OAuth 로그인 시 사용자 식별 정보
- role : 권한 (기본 "USER"로 초기화됨)
- signedAt : 가입 시간
- @Builder 생성자 : 코드에서 객체 만들 때 Member.builder()...build() 형식 사용 가능
6. MemberRepository - DB 접근 인터페이스
❓ 이건 뭐냐면?
→ Member 엔티티를 DB에서 조회, 저장, 삭제 등을 할 수 있도록 도와주는 DAO (Data Access Object)
🛠 역할:
- DB와 직접 연결되는 “통로”
- 예: 이메일로 사용자 찾기, 회원정보 저장하기
Optional<Member> optional = memberRepository.findByEmail("chulsoo@gmail.com");
💡왜 Optional로 반환해?
→ 사용자 존재할 수도 있고, 안 할 수도 있으니까.
→ null 방지하고 안전하게 ifPresent 혹은 orElseGet 등으로 처리 가능
✔️ 코드 분석 시작
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
}
✔️ 역할
- JPA 기반의 DB CRUD 처리 자동화.
- 회원 조회/저장 등의 DB 작업 처리
- findByEmail : 이메일로 유저 조회 가능하게 만드는 메서드 (JPA 쿼리 메서드)
- findByEmail() : 이메일 기준 회원 조회
🔁 전체 흐름 요약
- 사용자가 Google 로그인 버튼 클릭
- Spring Security가 OAuth2 인증 요청 → Google 로그인
- 인증 후, 사용자 정보가 loadUser()로 전달됨
- DB에서 이메일로 기존 유저 있는지 확인
- 없으면 새로 저장 (회원가입)
- MemberDetails 객체 생성 → Spring Security 내부 인증 완료
- 인증된 사용자만 user/test, admin/test 접근 가능
📌 참고 사항
- 컨트롤러에서 현재 로그인한 사용자 정보를 얻고 싶다면:
@GetMapping("/profile")
public String profile(@AuthenticationPrincipal OAuth2User user) {
return user.getAttribute("email");
}
- 실제 서비스에선 권한을 ENUM으로 관리하는 것이 안전하고 유지보수에 좋음.
728x90
'프로그래밍 > Spring' 카테고리의 다른 글
💭Spring Security 정리 - 4월 15일 (1) | 2025.04.15 |
---|---|
JPA 핵심 메서드 정리 (0) | 2025.04.14 |
entityManager.persist()란? (0) | 2025.04.14 |
Hibernate실습, JPQL 활용2 - 4월 14일 (0) | 2025.04.14 |
Hibernate실습, JPQL 활용 - 4월 14일 (0) | 2025.04.14 |