Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ dependencies {
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'

// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.GeneralSuccessCode;
import com.example.umc10th.global.security.entity.AuthMember;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

@RestController
Expand All @@ -17,7 +19,18 @@ public class MemberController {

private final MemberService memberService;

// 1) 회원가입
// 1) 로그인
@PostMapping("/auth/login")
public ApiResponse<MemberResDTO.Login> login(
@RequestBody @Valid MemberReqDTO.Login dto
) {
return ApiResponse.onSuccess(
MemberSuccessCode.MEMBER_LOGIN_SUCCESS,
memberService.login(dto)
);
}

// 2) 회원가입
@PostMapping("/auth/signup")
public ApiResponse<MemberResDTO.SignUp> signUp(
@RequestBody @Valid MemberReqDTO.SignUp dto
Expand All @@ -43,10 +56,12 @@ public ApiResponse<MemberResDTO.Home> getHome(

// 3) 마이페이지 조회
@GetMapping("/members/me")
public ApiResponse<MemberResDTO.MyPage> getMyPage() {
public ApiResponse<MemberResDTO.MyPage> getMyPage(
@AuthenticationPrincipal AuthMember member
) {
return ApiResponse.onSuccess(
MemberSuccessCode.MEMBER_GET_INFO_SUCCESS,
memberService.getMyPage()
memberService.getMyPage(member)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,37 @@

import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.enums.Gender;
import com.example.umc10th.global.enums.Address;
import com.example.umc10th.global.security.dto.OAuthDTO;

import java.time.LocalDate;

public class MemberConverter {

private MemberConverter() {
}

public static MemberResDTO.Login toLogin(String accessToken) {
return new MemberResDTO.Login(accessToken);
}

public static Member toMember(OAuthDTO dto) {
return Member.builder()
.name(dto.getName())
.nickname(dto.getName())
.email(dto.getSocialEmail())
.password("")
.socialUid(dto.getSocialUid())
.socialType(dto.getSocialType())
.gender(Gender.NONE)
.birth(LocalDate.of(1900, 1, 1))
.address(Address.NONE)
.detailAddress("")
.point(0)
.build();
}

public static MemberResDTO.SignUp toSignUpResponse(
Long memberId,
String nickname,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@

public class MemberReqDTO {

// 로그인
public record Login(
@NotBlank(message = "이메일은 필수입니다.") @Email(message = "이메일 형식이 올바르지 않습니다.") String email,
@NotBlank(message = "비밀번호는 필수입니다.") String password
) {}

// 회원가입
public record SignUp(
@NotBlank(message = "이름은 필수입니다.") String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

public class MemberResDTO {

// 1) 회원가입 응답
// 1) 로그인 응답
public record Login(
String accessToken
) {}

// 2) 회원가입 응답
public record SignUp(
Long memberId,
String nickname,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "존재하지 않는 회원입니다."),
MEMBER_EMAIL_DUPLICATED(HttpStatus.CONFLICT, "MEMBER409_1", "이미 사용 중인 이메일입니다."),
MEMBER_INVALID_REQUEST(HttpStatus.BAD_REQUEST, "MEMBER400_1", "잘못된 회원 요청입니다.");
MEMBER_INVALID_REQUEST(HttpStatus.BAD_REQUEST, "MEMBER400_1", "잘못된 회원 요청입니다."),
MEMBER_PASSWORD_INVALID(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "비밀번호가 올바르지 않습니다."),
NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, "MEMBER400_2", "지원하지 않는 소셜 로그인입니다.");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ public enum MemberSuccessCode implements BaseSuccessCode {
HttpStatus.OK,
"MEMBER200_2",
"성공적으로 회원가입이 완료되었습니다."
),

// 로그인 성공
MEMBER_LOGIN_SUCCESS(
HttpStatus.OK,
"MEMBER200_3",
"성공적으로 로그인이 완료되었습니다."
);


Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.umc10th.domain.member.repository;

import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.enums.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;
Expand All @@ -9,4 +10,5 @@
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
boolean existsByEmail(String email);
Optional<Member> findBySocialTypeAndSocialUid(SocialType socialType, String socialUid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import com.example.umc10th.domain.mission.repository.MemberMissionRepository;
import com.example.umc10th.domain.mission.repository.MissionRepository;
import com.example.umc10th.global.enums.Address;
import com.example.umc10th.global.security.entity.AuthMember;
import com.example.umc10th.global.security.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.data.domain.Page;
Expand All @@ -48,11 +50,22 @@ public class MemberService {
private final MemberMissionRepository memberMissionRepository;
private final MissionRepository missionRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;

public MemberResDTO.MyPage getMyPage() {
Member member = memberRepository.findById(CURRENT_MEMBER_ID)
public MemberResDTO.Login login(MemberReqDTO.Login dto) {
Member member = memberRepository.findByEmail(dto.email())
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
return MemberConverter.toMyPageResponse(member);

if (!passwordEncoder.matches(dto.password(), member.getPassword())) {
throw new MemberException(MemberErrorCode.MEMBER_PASSWORD_INVALID);
}

String token = jwtUtil.createAccessToken(new AuthMember(member));
return new MemberResDTO.Login(token);
}

public MemberResDTO.MyPage getMyPage(AuthMember authMember) {
return MemberConverter.toMyPageResponse(authMember.getMember());
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package com.example.umc10th.global.config;

import com.example.umc10th.global.security.filter.JwtAuthFilter;
import com.example.umc10th.global.security.handler.CustomAccessDeniedHandler;
import com.example.umc10th.global.security.handler.CustomAuthenticationEntryPoint;
import com.example.umc10th.global.security.handler.OAuthSuccessHandler;
import com.example.umc10th.global.security.service.CustomOAuthService;
import com.example.umc10th.global.security.service.CustomUserDetailsService;
import com.example.umc10th.global.security.util.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand All @@ -10,38 +16,83 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;
private final CustomOAuthService customOAuthService;

private final String[] allowUris = {
// Swagger 허용
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
// 회원가입, 로그인 허용
"/api/v1/auth/**"
"/api/v1/auth/**",

// OAuth 허용
"/oauth/**"
};

@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtil, customUserDetailsService);
}

@Bean
public OAuthSuccessHandler oAuthSuccessHandler() {
return new OAuthSuccessHandler(jwtUtil);
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
// URI 허용 여부
.authorizeHttpRequests(requests -> requests
// Public API 허용 여부
.requestMatchers(allowUris).permitAll()
// 그 이외 API는 인증 필요
.anyRequest().authenticated()
)
.formLogin(form -> form
.defaultSuccessUrl("/swagger-ui/index.html", true)
.permitAll()
// 폼 로그인
.formLogin(AbstractHttpConfigurer::disable)
// 세션 (OAuth2 흐름에서 state 검증을 위해 세션 필요 - 비활성화하면 콜백 실패)
// .sessionManagement(AbstractHttpConfigurer::disable)
// JWT 필터
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
// OAuth
.oauth2Login(oauth -> oauth

// 인증 엔트리 포인트
.authorizationEndpoint(auth -> auth
.baseUri("/oauth/authorize")
)

// 콜백 주소
.redirectionEndpoint(redirect -> redirect
.baseUri("/oauth/callback/**")
)

// 인증 완료 후 정보 활용
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuthService)
)

// 성공 시 JWT 토큰 발행 핸들러
.successHandler(oAuthSuccessHandler())
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
// 에러 상황 핸들러
// 예외 상황 핸들러
.exceptionHandling(exception -> exception
.accessDeniedHandler(customAccessDeniedHandler())
.authenticationEntryPoint(customAuthenticationEntryPoint())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.umc10th.global.security.dto;

import com.example.umc10th.domain.member.enums.SocialType;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class KakaoDTO implements OAuthDTO {

private final String id;
private final String email;
private final String name;

@Override
public SocialType getSocialType() {
return SocialType.KAKAO;
}

@Override
public String getSocialUid() {
return id;
}

@Override
public String getSocialEmail() {
return email;
}

@Override
public String getName() {
return name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.umc10th.global.security.dto;

import com.example.umc10th.domain.member.enums.SocialType;

public interface OAuthDTO {
SocialType getSocialType();
String getSocialUid();
String getSocialEmail();
String getName();
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ public Collection<? extends GrantedAuthority> getAuthorities() {

@Override
public @Nullable String getPassword() {
return member.getPassword();
return null;
}

@Override
public String getUsername() {
return member.getEmail();
return member.getSocialUid();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.umc10th.global.security.entity;

import com.example.umc10th.domain.member.entity.Member;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

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

@RequiredArgsConstructor
public class OAuthMember implements OAuth2User {

@Getter
private final Member member;
private final Map<String, Object> attributes;

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}

@Override
public String getName() {
return member.getSocialUid();
}
}
Loading