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


}

tasks.named('test') {
Expand Down
33 changes: 17 additions & 16 deletions src/main/java/umc/domain/member/controller/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +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.MemberRequestDTO;
import umc.domain.member.dto.MemberResponseDTO;
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
@RequestMapping("/api/members")
Expand All @@ -24,6 +27,14 @@ public ApiResponse<MemberResponseDTO.SignUpDTO> signUp(
return ApiResponse.onSuccess(MemberSuccessCode.CREATED, memberService.signUp(requestDto));
}

// 로그인
@PostMapping("/login")
public ApiResponse<MemberResponseDTO.LoginDTO> login(
@RequestBody @Valid MemberRequestDTO.LoginDTO requestDto
) {
return ApiResponse.onSuccess(MemberSuccessCode.LOGIN_SUCCESS, memberService.login(requestDto));
}

// 홈 화면 조회
@GetMapping("/me/home")
public ApiResponse<MemberResponseDTO.HomeDTO> getHome(
Expand All @@ -38,21 +49,11 @@ public ApiResponse<MemberResponseDTO.HomeDTO> getHome(

// 마이페이지
@GetMapping("/me")
public ApiResponse<MemberResponseDTO.MyPageDTO> getMyPage() {
MemberResponseDTO.MyPageDTO responseDto = memberService.getMyPage(1L);
return ApiResponse.onSuccess(MemberSuccessCode.MY_PAGE_VIEW, responseDto);
}












public ApiResponse<MemberResponseDTO.MyPageDTO> get(
@AuthenticationPrincipal AuthMember member
) {
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.onSuccess(code, memberService.getMyPage(member));

}
}
8 changes: 8 additions & 0 deletions src/main/java/umc/domain/member/dto/MemberRequestDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,12 @@ public record TermDTO(
Boolean isAgreed
) {}

// 로그인
public record LoginDTO(
@NotBlank
@Email
String email,
@NotBlank
String password
) {}
}
5 changes: 5 additions & 0 deletions src/main/java/umc/domain/member/dto/MemberResponseDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,9 @@ public record MyPageDTO(
Integer points,
String profileUrl
) {}

@Builder
public record LoginDTO(
String accessToken
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 멤버를 찾을 수 없습니다."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "MEMBER409_1", "이미 사용 중인 이메일입니다."),
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "MEMBER401_1", "아이디 또는 비밀번호가 올바르지 않습니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@
public enum MemberSuccessCode implements BaseSuccessCode {

CREATED(HttpStatus.CREATED, "MEMBER201_1", "회원가입을 완료했습니다!"),
LOGIN_SUCCESS(HttpStatus.OK, "MEMBER200_4", "로그인에 성공했습니다."),
HOME_VIEW(HttpStatus.OK, "HOME200_1", "홈 화면"),
OK(HttpStatus.OK, "MEMBER200_2", "성공적으로 유저를 조회했습니다."),
MY_PAGE_VIEW(HttpStatus.OK, "MEMBER200_2", "마이페이지를 성공적으로 조회했습니다."),
MY_PAGE_VIEW(HttpStatus.OK, "MEMBER200_3", "마이페이지를 성공적으로 조회했습니다."),
;

private final HttpStatus status;
Expand Down
27 changes: 22 additions & 5 deletions src/main/java/umc/domain/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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;

Expand All @@ -49,6 +51,7 @@ public class MemberService {
private final MemberPreferFoodRepository memberPreferFoodRepository;
private final MemberTermRepository memberTermRepository;
private final TermRepository termRepository;
private final JwtUtil jwtUtil;


// 회원가입
Expand Down Expand Up @@ -100,13 +103,27 @@ private void saveTermAgreements(Member member, List<MemberRequestDTO.TermDTO> te
memberTermRepository.saveAll(memberTerms);
}

// 마이 페이지
// 로그인
@Transactional
public MemberResponseDTO.MyPageDTO getMyPage(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));
public MemberResponseDTO.LoginDTO login(MemberRequestDTO.LoginDTO dto) {
Member member = memberRepository.findByEmail(dto.email())
.orElseThrow(() -> new MemberException(MemberErrorCode.INVALID_CREDENTIALS));
if (!passwordEncoder.matches(dto.password(), member.getPassword())) {
throw new MemberException(MemberErrorCode.INVALID_CREDENTIALS);
}

return MemberConverter.toMyPageViewDTO(member);
String token = jwtUtil.createAccessToken(new AuthMember(member));
return MemberResponseDTO.LoginDTO.builder()
.accessToken(token)
.build();
}

// 마이 페이지
@Transactional
public MemberResponseDTO.MyPageDTO getMyPage(
AuthMember authMember
) {
return MemberConverter.toMyPageViewDTO(authMember.getMember());
}

// 홈 화면 조회
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/umc/global/config/JacksonConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package umc.global.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
20 changes: 13 additions & 7 deletions src/main/java/umc/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@
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.filter.JwtAuthFilter;
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
@Configuration
Expand All @@ -20,13 +24,16 @@ public class SecurityConfig {

private final CustomAccessDenied customAccessDenied;
private final CustomEntryPoint customEntryPoint;
private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailService;

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

@Bean
Expand All @@ -37,10 +44,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers(allowUris).permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.defaultSuccessUrl("/swagger-ui/index.html", true)
.permitAll()
)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(AbstractHttpConfigurer::disable)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
Expand All @@ -60,7 +66,7 @@ public PasswordEncoder passwordEncoder() {
}

@Bean
public ObjectMapper objectMapepr() {
return new ObjectMapper();
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtil, customUserDetailService);
}
}
73 changes: 73 additions & 0 deletions src/main/java/umc/global/security/filter/JwtAuthFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package umc.global.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.OncePerRequestFilter;
import umc.global.apiPayload.ApiResponse;
import umc.global.apiPayload.code.BaseErrorCode;
import umc.global.apiPayload.code.GeneralErrorCode;
import umc.global.security.service.CustomUserDetailsService;
import umc.global.security.util.JwtUtil;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {

try {
// 토큰 가져오기
String token = request.getHeader("Authorization");
// token이 없거나 Bearer가 아니면 넘기기
if (token == null || !token.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
// Bearer이면 추출
token = token.replace("Bearer ", "");
// AccessToken 검증하기: 올바른 토큰이면
if (jwtUtil.isValid(token)) {
// 토큰에서 이메일 추출
String email = jwtUtil.getEmail(token);
// 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성
UserDetails user = customUserDetailsService.loadUserByUsername(email);
Authentication auth = new UsernamePasswordAuthenticationToken(
user,
null,
user.getAuthorities()
);
// 인증 완료 후 SecurityContextHolder에 넣기
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
} catch (Exception e) {
ObjectMapper mapper = new ObjectMapper();
BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;

response.setContentType("application/json;charset=UTF-8");
response.setStatus(code.getStatus().value());

ApiResponse<Void> errorResponse = ApiResponse.onFailure(code,null);

mapper.writeValue(response.getOutputStream(), errorResponse);
}
}
}
Loading