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
13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ 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'

// WebAuthn
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.security:spring-security-webauthn'

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

tasks.named('test') {
Expand Down
25 changes: 19 additions & 6 deletions src/main/java/umc/domain/member/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package umc.domain.member.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -20,6 +21,7 @@
import umc.global.apiPayload.ApiResponse;
import umc.global.apiPayload.code.BaseSuccessCode;
import umc.global.apiPayload.code.GeneralSuccessCode;
import umc.global.security.entity.AuthMember;

@RestController
@RequiredArgsConstructor
Expand All @@ -31,12 +33,12 @@ public class MemberController {
private final MemberCommandService memberCommandService;

// 마이페이지
@GetMapping("/{memberId}/mypage")
@GetMapping("/users/me")
@Operation(summary = "마이페이지 조회")
public ApiResponse<MemberResDTO.GetInfo> getInfo(
@PathVariable(name = "memberId") Long memberId
@AuthenticationPrincipal AuthMember member
) {
MemberResDTO.GetInfo getInfo = memberQueryService.getInfo(memberId);
MemberResDTO.GetInfo getInfo = memberQueryService.getInfo(member);

return ApiResponse.onSuccess(MemberSuccessCode.OK, getInfo);
}
Expand All @@ -45,12 +47,12 @@ public ApiResponse<MemberResDTO.GetInfo> getInfo(
// 회원가입
@PostMapping("/signup")
@Operation(summary = "회원가입")
public ApiResponse<String> join(@RequestBody @Valid MemberReqDTO.JoinDTO request) {
public ApiResponse<MemberResDTO.JoinResultDTO> join(@RequestBody @Valid MemberReqDTO.JoinDTO request) {

Member joins = memberCommandService.joinMember(request);
MemberResDTO.JoinResultDTO joins = memberCommandService.joinMember(request);

BaseSuccessCode code = MemberSuccessCode.MEMBER_JOINED;
return ApiResponse.onSuccess(code, joins.getEmail());
return ApiResponse.onSuccess(code, joins);
}


Expand All @@ -66,4 +68,15 @@ public ApiResponse<MemberResDTO.HomeResponseDTO> getHomeInfo(

return ApiResponse.onSuccess(MemberSuccessCode.OK, result);
}

// 로그인
@PostMapping("/login")
@Operation(summary = "로그인")
public ApiResponse<MemberResDTO.LoginResultDTO> login(
@RequestBody @Valid MemberReqDTO.LoginDTO request
) {
MemberResDTO.LoginResultDTO result = memberCommandService.login(request);

return ApiResponse.onSuccess(MemberSuccessCode.OK, result);
}
}
27 changes: 27 additions & 0 deletions src/main/java/umc/domain/member/converter/MemberConverter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package umc.domain.member.converter;

import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -8,7 +9,10 @@
import umc.domain.member.dto.MemberReqDTO;
import umc.domain.member.dto.MemberResDTO;
import umc.domain.member.entity.Member;
import umc.domain.member.enums.Gender;
import umc.domain.member.enums.MemberStatus;
import umc.domain.mission.entity.Mission;
import umc.global.security.dto.OAuthDTO;

public class MemberConverter {

Expand Down Expand Up @@ -76,4 +80,27 @@ public static Member toMember(MemberReqDTO.JoinDTO request, String encodedPasswo
.address(request.getAddress())
.build();
}
public static MemberResDTO.JoinResultDTO toJoinDTO(Member member) {
return MemberResDTO.JoinResultDTO.builder()
.memberId(member.getId())
.createdAt(LocalDateTime.now())
.build();
}

public static Member toMember(OAuthDTO oAuthDTO) {
return Member.builder()
.name(oAuthDTO.getName())
.email(oAuthDTO.getSocialEmail())
.socialUid(oAuthDTO.getSocialUid())
.socialType(oAuthDTO.getSocialType())
.gender(Gender.NONE)
.memberStatus(MemberStatus.ACTIVE)
.build();
}

public static MemberResDTO.LoginResultDTO toLogin(String accessToken) {
return MemberResDTO.LoginResultDTO.builder()
.accessToken(accessToken)
.build();
}
}
8 changes: 8 additions & 0 deletions src/main/java/umc/domain/member/dto/MemberReqDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,12 @@ public static class TermDTO {
Long termId;
Boolean isAgree;
}

@Getter
public static class LoginDTO {
@NotBlank(message = "이메일을 입력해주세요")
String email;
@NotBlank(message = "비밀번호를 입력해주세요")
String password;
}
}
8 changes: 8 additions & 0 deletions src/main/java/umc/domain/member/dto/MemberResDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,12 @@ public static class MissionDetailDTO {
Integer point;
LocalTime deadline;
}

@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class LoginResultDTO {
String accessToken;
}
}
3 changes: 3 additions & 0 deletions src/main/java/umc/domain/member/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ public class Member extends BaseEntity {
@Enumerated(EnumType.STRING)
private SocialType socialType;

@Column(name = "social_uid")
private String socialUid;

@Builder.Default
@Column(nullable = false, length = 20)
@Enumerated(EnumType.STRING)
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/umc/domain/member/enums/Gender.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
public enum Gender {

MALE("남자"),
FEMALE("여자");
FEMALE("여자"),
NONE("선택안함");

private final String label;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 사용자를 찾을 수 없습니다."),

EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", "이미 존재하는 이메일입니다.");
EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", "이미 존재하는 이메일입니다."),

INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER400_1", "비밀번호가 일치하지 않습니다"),

NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, "MEMBER400_X", "지원하지 않는 소셜 로그인 제공자입니다.");
private final HttpStatus status;
private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
import org.springframework.data.repository.query.Param;

import umc.domain.member.entity.Member;
import umc.domain.member.enums.SocialType;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);

boolean existsByEmail(String email);

Optional<Member> findBySocialTypeAndSocialUid(SocialType providerId, String socialUid);
}
27 changes: 25 additions & 2 deletions src/main/java/umc/domain/member/service/MemberCommandService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import lombok.RequiredArgsConstructor;
import umc.domain.member.converter.MemberConverter;
import umc.domain.member.dto.MemberReqDTO;
import umc.domain.member.dto.MemberResDTO;
import umc.domain.member.entity.Member;
import umc.domain.member.entity.Term;
import umc.domain.member.entity.mapping.MemberFood;
Expand All @@ -27,6 +28,8 @@
import umc.domain.store.exception.StoreException;
import umc.domain.store.exception.code.StoreErrorCode;
import umc.domain.store.repository.FoodRepository;
import umc.global.security.entity.AuthMember;
import umc.global.security.util.JwtUtil;

@Service
@RequiredArgsConstructor
Expand All @@ -38,9 +41,10 @@ public class MemberCommandService {
private final MemberFoodRepository memberFoodRepository;
private final TermRepository termRepository;
private final MemberTermRepository memberTermRepository;
private final JwtUtil jwtUtil;

@Transactional
public Member joinMember(MemberReqDTO.JoinDTO request) {
public MemberResDTO.JoinResultDTO joinMember(MemberReqDTO.JoinDTO request) {

// 1. 이메일 중복 검증
if(memberRepository.existsByEmail(request.getEmail())) {
Expand Down Expand Up @@ -98,6 +102,25 @@ public Member joinMember(MemberReqDTO.JoinDTO request) {

memberTermRepository.saveAll(memberTermList);
}
return savedMember;
return MemberConverter.toJoinDTO(savedMember);
}

@Transactional
public MemberResDTO.LoginResultDTO login(MemberReqDTO.LoginDTO request) {

Member member = memberRepository.findByEmail(request.getEmail())
.orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

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

AuthMember authMember = new AuthMember(member);

String accessToken = jwtUtil.createAccessToken(authMember);

return MemberResDTO.LoginResultDTO.builder()
.accessToken(accessToken)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import umc.domain.store.exception.StoreException;
import umc.domain.store.exception.code.StoreErrorCode;
import umc.domain.store.repository.RegionRepository;
import umc.global.security.entity.AuthMember;

@Service
@RequiredArgsConstructor
Expand All @@ -31,9 +32,9 @@ public class MemberQueryService {
private final MissionRepository missionRepository;

@Transactional(readOnly = true)
public MemberResDTO.GetInfo getInfo(Long memberId) {
public MemberResDTO.GetInfo getInfo(AuthMember authMember) {

Member member = memberRepository.findById(memberId)
Member member = memberRepository.findById(authMember.getMember().getId())
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

return MemberConverter.toGetInfo(member);
Expand Down
62 changes: 57 additions & 5 deletions src/main/java/umc/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,72 @@

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcOperations;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.webauthn.management.JdbcPublicKeyCredentialUserEntityRepository;
import org.springframework.security.web.webauthn.management.JdbcUserCredentialRepository;

import lombok.RequiredArgsConstructor;
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.CustomAccessDenied;
import umc.global.security.util.CustomEntryPoint;
import umc.global.security.util.JwtUtil;

@EnableWebSecurity // Spring Security 설정 활성화
@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/**",
"/auth/**",
"/api/v1/signup",
"/api/v1/login",
"/error/**",
};

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
public SecurityFilterChain securityFilterChain(HttpSecurity http, OAuthSuccessHandler oAuthSuccessHandler) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(requests -> requests
.requestMatchers(allowUris).permitAll() // allowUris에 있는 주소들은 누구나 접근 가능
.anyRequest().authenticated() // 그 외 모든 요청은 로그인 필요
)
.formLogin(form -> form
.defaultSuccessUrl("/swagger-ui/index.html", true)
.permitAll()
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.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")
Expand Down Expand Up @@ -67,4 +98,25 @@ public CustomAccessDenied customAccessDenied() {
public CustomEntryPoint customEntryPoint() {
return new CustomEntryPoint();
}
}

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

@Bean
JdbcPublicKeyCredentialUserEntityRepository jdbcPublicKeyCredentialUserEntityRepository(
JdbcOperations jdbc) {
return new JdbcPublicKeyCredentialUserEntityRepository(jdbc);
}

@Bean
JdbcUserCredentialRepository jdbcUserCredentialRepository(JdbcOperations jdbc) {
return new JdbcUserCredentialRepository(jdbc);
}

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