Skip to content
Open
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 @@ -39,6 +39,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 @@ -2,13 +2,15 @@

import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import umc.domain.member.dto.MemberReqDTO;
import umc.domain.member.dto.MemberResDTO;
import umc.domain.member.exception.code.MemberSuccessCode;
import umc.domain.member.service.MemberService;
import umc.global.apiPayload.ApiResponse;
import umc.global.apiPayload.code.BaseSuccessCode;
import umc.global.security.entity.AuthMember;

@RestController
@RequiredArgsConstructor
Expand All @@ -18,10 +20,11 @@ public class MemberController {
private final MemberService memberService;

@GetMapping("/users/me")
public ApiResponse<MemberResDTO.GetInfo> getInfo(){
Long memberId = 1L; // TODO: memberId from access token
public ApiResponse<MemberResDTO.GetInfo> getInfo(
@AuthenticationPrincipal AuthMember member
){
BaseSuccessCode code = MemberSuccessCode.MEMBER_VIEW;
MemberResDTO.GetInfo response = memberService.getInfo(memberId);
MemberResDTO.GetInfo response = memberService.getInfo(member.getMember().getId());
return ApiResponse.onSuccess(code, response);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,12 @@ public ApiResponse<MemberResDTO.CreateMember> createMember(
BaseSuccessCode code = MemberSuccessCode.MEMBER_CREATED;
return ApiResponse.onSuccess(code, memberService.createMember(request));
}

@PostMapping("/login")
public ApiResponse<MemberResDTO.Login> login(
@RequestBody @Valid MemberReqDTO.Login request
) {
BaseSuccessCode code = MemberSuccessCode.MEMBER_LOGIN;
return ApiResponse.onSuccess(code, memberService.login(request));
}
}
23 changes: 23 additions & 0 deletions src/main/java/umc/domain/member/converter/MemberConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import org.springframework.data.domain.Page;
import umc.domain.member.dto.MemberResDTO;
import umc.domain.member.entity.Member;
import umc.domain.member.enums.Gender;
import umc.domain.member.exception.code.MemberErrorCode;
import umc.domain.mission.entity.Mission;
import umc.domain.store.entity.Region;
import umc.domain.store.entity.Store;
import umc.global.security.dto.OAuthDTO;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -69,4 +72,24 @@ public static MemberResDTO.CreateMember toCreateMember(
.build();
}

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

// nullable = false 거나 NullPointerException이 발생 가능한 attribute에는 더미
public static Member toMember(OAuthDTO dto) {
return Member.builder()
.name(dto.getName())
.email(dto.getSocialEmail())
.password("")
.gender(Gender.NONE)
.birth(LocalDate.of(2000, 1, 1))
.address("NONE")
.socialProvider(dto.getSocialProvider())
.socialId(dto.getSocialId())
.currentPoint(0L)
.build();
}
}
5 changes: 5 additions & 0 deletions src/main/java/umc/domain/member/dto/MemberReqDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public record Agree(
@NotNull Boolean marketing
) {}
}

public record Login(
@Email @NotBlank String email,
@NotBlank String paassword
) {}
}
5 changes: 5 additions & 0 deletions src/main/java/umc/domain/member/dto/MemberResDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,9 @@ public record HomeMission(
public record CreateMember(
Long memberId
) {}

@Builder
public record Login(
String accessToken
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@
@AllArgsConstructor
public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 사용자를 찾을 수 없습니다."),
EMAIL_ALREADY_EXIST(HttpStatus.CONFLICT, "MEMBER409_1", "해당 이메일은 이미 사용 중 입니다."),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND,
"MEMBER404_1",
"해당 사용자를 찾을 수 없습니다."),
EMAIL_ALREADY_EXIST(HttpStatus.CONFLICT,
"MEMBER409_1",
"해당 이메일은 이미 사용 중 입니다."),
LOGIN_FAILED(HttpStatus.UNAUTHORIZED,
"MEMBER401_1",
"이메일 또는 비밀번호가 일치하지 않습니다."),
NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST,
"MEMBER400_1",
"지원하지 않는 소셜 로그인 제공자입니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public enum MemberSuccessCode implements BaseSuccessCode {
MEMBER_CREATED(HttpStatus.OK,
"MEMBER200_2",
"회원을 성공적으로 생성했습니다."),
MEMBER_LOGIN(HttpStatus.OK,
"MEMBER200_3",
"로그인에 성공했습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.data.jpa.repository.JpaRepository;
import umc.domain.member.entity.Member;
import umc.domain.member.enums.SocialProvider;

import java.util.Optional;

Expand All @@ -10,4 +11,6 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);

boolean existsByEmail(String email);

Optional<Member> findBySocialProviderAndSocialId(SocialProvider provider, String socialId);
}
17 changes: 17 additions & 0 deletions src/main/java/umc/domain/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import umc.domain.term.exception.TermException;
import umc.domain.term.exception.code.TermErrorCode;
import umc.domain.term.repository.TermRepository;
import umc.global.security.entity.AuthMember;
import umc.global.security.util.JwtUtil;

import java.util.List;
import java.util.Map;
Expand All @@ -55,6 +57,7 @@ public class MemberService {
private final TermRepository termRepository;
private final MemberPreferredCategoryRepository memberPreferredCategoryRepository;
private final TermAgreementRepository termAgreementRepository;
private final JwtUtil jwtUtil;

@Transactional(readOnly = true)
public MemberResDTO.GetInfo getInfo(Long memberId) {
Expand Down Expand Up @@ -199,4 +202,18 @@ private void validateTermExist(
throw new TermException(TermErrorCode.TERM_MASTER_DATA_NOT_FOUND);
}
}

@Transactional
public MemberResDTO.Login login(MemberReqDTO.Login request){
Member member = memberRepository.findByEmail(request.email())
.orElseThrow(() -> new MemberException(MemberErrorCode.LOGIN_FAILED));

if(!passwordEncoder.matches(request.paassword(), member.getPassword())){
throw new MemberException(MemberErrorCode.LOGIN_FAILED);
}

String accessToken = jwtUtil.createAccessToken(new AuthMember(member));

return MemberConverter.toLogin(accessToken);
}
}
55 changes: 48 additions & 7 deletions src/main/java/umc/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package umc.global.config;

import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
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;
import umc.global.security.exception.CustomAccessDenied;
import umc.global.security.exception.CustomEntryPoint;
import umc.global.security.exception.SecurityErrorResponseWriter;
import umc.global.security.filter.JwtAuthFilter;
import umc.global.security.handler.OAuthSuccessHandler;
import umc.global.security.service.CustomOAuthService;
import umc.global.security.service.CustomUserDetailsService;
import umc.global.security.util.JwtUtil;


@EnableWebSecurity
Expand All @@ -20,40 +29,66 @@ public class SecurityConfig {

private final CustomAccessDenied customAccessDenied;
private final CustomEntryPoint customEntryPoint;
private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;
private final SecurityErrorResponseWriter securityErrorResponseWriter;
private final OAuthSuccessHandler oAuthSuccessHandler;

private final String[] allowUris = {
// Swagger 허용
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",

"/public/**"
"/public/**",
"/oauth/**"
};

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuthService customOAuthService) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(requests -> requests
// public API 허용
.requestMatchers(allowUris).permitAll()
// 그 이외 API는 인증 필요
.anyRequest().authenticated()
)
// 폼 로그인
.formLogin(form -> form
.defaultSuccessUrl("/swagger-ui/index.html", true)
.permitAll()
)
// JWT 필터
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
// 세션
.sessionManagement(
AbstractHttpConfigurer::disable
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
// 예외 상황 핸들러
.exceptionHandling(exception -> exception
.defaultAuthenticationEntryPointFor( // 폼 로그인을 위한 임시 설정
customEntryPoint,
request -> request.getRequestURI().startsWith("/api")
)
.accessDeniedHandler(customAccessDenied)
// .authenticationEntryPoint(customEntryPoint) // 전역 설정
.authenticationEntryPoint(customEntryPoint) // 전역 설정
)
// OAuth
.oauth2Login(oauth -> oauth
.authorizationEndpoint(auth -> auth
.baseUri("/oauth/authorize")
)
.redirectionEndpoint(redirect -> redirect
.baseUri("/oauth/callback/**")
)
// 인증 완료 후 정보 활용
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuthService)
)
// 성공 시 JWT 토큰 발행할 핸들러
.successHandler(oAuthSuccessHandler)
)
;

Expand All @@ -64,4 +99,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

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

}
34 changes: 34 additions & 0 deletions src/main/java/umc/global/security/dto/KakaoDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package umc.global.security.dto;

import lombok.RequiredArgsConstructor;
import umc.domain.member.enums.SocialProvider;

@RequiredArgsConstructor
public class KakaoDTO implements OAuthDTO{

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

@Override
public SocialProvider getSocialProvider(){
return SocialProvider.KAKAO;
}

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

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

@Override
public String getName(){
return name;
}


}
10 changes: 10 additions & 0 deletions src/main/java/umc/global/security/dto/OAuthDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package umc.global.security.dto;

import umc.domain.member.enums.SocialProvider;

public interface OAuthDTO {
SocialProvider getSocialProvider();
String getSocialId();
String getSocialEmail();
String getName();
}
2 changes: 1 addition & 1 deletion src/main/java/umc/global/security/entity/AuthMember.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ public Collection<? extends GrantedAuthority> getAuthorities(){

@Override
public String getUsername(){
return member.getEmail();
return member.getEmail();
}
}
Loading