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
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ 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
Binary file added docs/week9_login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/week9_myPage.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/week9_singUp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/main/java/com/example/umc10th/Umc10thApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class Umc10thApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.exception.MemberSuccessCode;
import com.example.umc10th.domain.member.service.MemberService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
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 @@ -21,13 +24,20 @@ public ApiResponse<MemberResDTO.SignUp> signup(@RequestBody MemberReqDTO.SignUp
return ApiResponse.onSuccess(code, memberService.signUp(dto));
}

@GetMapping("/members/me")
public ApiResponse<MemberResDTO.MyPage> getMyPage(
@RequestParam Long id // 추후 JWT 적용 시 @AuthenticationPrincipal로 교체
) {
@PostMapping("/auth/login")
public ApiResponse<MemberResDTO.Login> login(@RequestBody MemberReqDTO.Login dto){
return ApiResponse.onSuccess(
MemberSuccessCode.GET_MY_PAGE,
memberService.getMyPage(id)
MemberSuccessCode.LOGIN_SUCCESS,
memberService.login(dto)
);
}


@GetMapping("/members/me")
public ApiResponse<MemberResDTO.MyPage> getMyPage(
@AuthenticationPrincipal AuthMember member
) {
BaseSuccessCode code = MemberSuccessCode.GET_MY_PAGE;
return ApiResponse.onSuccess(code, memberService.getMyPage(member));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
public class MemberConverter {

// ReqDTO → Entity
public static Member toMember(MemberReqDTO.SignUp dto) {
public static Member toMember(MemberReqDTO.SignUp dto, String encodedPassword) {
return Member.builder()
.name(dto.name())
.gender(dto.gender())
.birth(dto.birth())
.address(dto.address())
.email(dto.email())
.password(encodedPassword)
.build();
}

Expand All @@ -24,10 +26,18 @@ public static MemberResDTO.SignUp toSignUpRes(Member member) {
.build();
}

public static MemberResDTO.Login toLoginRes(Member member, String accessToken){
return MemberResDTO.Login.builder()
.memberId(member.getId())
.accessToken(accessToken)
.build();
}

// Entity → ResDTO (마이페이지)
public static MemberResDTO.MyPage toMyPageRes(Member member) {
return MemberResDTO.MyPage.builder()
.name(member.getName())
.email(member.getEmail())
.point(member.getPoint())
.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public record SignUp(
String password
){}

public record Login(
String email,
String password
){}

public record GetMyPage(
Long id
) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ public record SignUp(
LocalDateTime createdAt
){}

@Builder
public record Login(
String accessToken,
Long memberId
){}

@Builder
public record MyPage(
String name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,20 @@ public class Member extends BaseEntity {
@JoinColumn(name = "region_id")
private Region region;

@Column(name = "name")
@Column(name = "name", nullable = false)
private String name;

@Enumerated(EnumType.STRING)
@Column(name = "gender")
@Column(name = "gender", nullable = false)
private Gender gender;

@Column(name = "birth")
@Column(name = "birth", nullable = false)
private LocalDate birth;

@Column(name = "address")
@Column(name = "address", nullable = false)
private String address;

@Column(name = "email")
@Column(name = "email", nullable = false)
private String email;

@Column(name = "password")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,18 @@
@RequiredArgsConstructor
public enum MemberErrorCode implements BaseErrorCode {

// 400
INVALID_PASSWORD(HttpStatus.NOT_FOUND,
"MEMBER404_2",
"이메일 또는 비밀번호가 일치하지 않습니다."),
// 404
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND,
"MEMBER404_1",
"멤버를 찾을 수 없습니다."),
// 409
DUPLICATE_EMAIL(HttpStatus.CONFLICT,
"MEMBER409_1",
"이미 존재하는 이메일입니다.")
"이미 존재하는 이메일입니다."),
;

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ public enum MemberSuccessCode implements BaseSuccessCode {
GET_MY_PAGE(HttpStatus.OK,
"MEMBER200_1",
"마이페이지가 조회되었습니다."),
LOGIN_SUCCESS(HttpStatus.OK,
"MEMBER200_2",
"성공적으로 로그인 하였습니다."),
SIGN_UP(HttpStatus.CREATED,
"MEMBER201_1",
"성공적으로 회원가입 하였습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
import com.example.umc10th.domain.member.exception.MemberErrorCode;
import com.example.umc10th.domain.member.repository.MemberRepository;
import com.example.umc10th.global.apiPayload.exception.ProjectException;
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -16,18 +19,44 @@
public class MemberService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;

@Transactional
@Transactional
public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp dto) {
Member member = MemberConverter.toMember(dto);
String encodedPassword = passwordEncoder.encode(dto.password());

Member member = MemberConverter.toMember(dto, encodedPassword);

Member saved = memberRepository.save(member);
return MemberConverter.toSignUpRes(saved);
}

@Transactional(readOnly = true)
public MemberResDTO.MyPage getMyPage(Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new ProjectException(MemberErrorCode.MEMBER_NOT_FOUND));
return MemberConverter.toMyPageRes(member);
@Transactional
public MemberResDTO.Login login(MemberReqDTO.Login dto){
Member member = memberRepository.findByEmail(dto.email())
.orElseThrow(()-> new ProjectException(MemberErrorCode.MEMBER_NOT_FOUND));

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

AuthMember authMember = new AuthMember(member);

String accessToken = jwtUtil.createAccessToken(authMember);

return MemberConverter.toLoginRes(member, accessToken);
}

// @Transactional(readOnly = true)
// public MemberResDTO.MyPage getMyPage(Long memberId) {
// Member member = memberRepository.findById(memberId)
// .orElseThrow(() -> new ProjectException(MemberErrorCode.MEMBER_NOT_FOUND));
// return MemberConverter.toMyPageRes(member);
// }

@Transactional(readOnly = true)
public MemberResDTO.MyPage getMyPage(AuthMember member) {
return MemberConverter.toMyPageRes(member.getMember());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import com.example.umc10th.global.security.exception.CustomAccessDenied;
import com.example.umc10th.global.security.exception.CustomEntryPoint;
import com.example.umc10th.global.security.filter.JwtAuthFilter;
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,15 +14,22 @@
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 String[] allowUris = {
// Swagger 허용
"/swagger-ui/**",
"/swagger-resources/**",
"/swagger-ui.html",
"/v3/api-docs/swagger-config",
"/v3/api-docs/**",

// public api
Expand All @@ -29,19 +40,28 @@ public class SecurityConfig {
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()
)
// 세션
.sessionManagement(AbstractHttpConfigurer::disable)
// JWT 필터 -> 순서는 상관없고 시큐리티 필터체인에 들어가면 된다.
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
// 예외사항 핸들러
.exceptionHandling(exception ->exception
.accessDeniedHandler(customAccessDenied())
.authenticationEntryPoint(customEntryPoint())
Expand All @@ -65,4 +85,9 @@ public CustomAccessDenied customAccessDenied(){
public CustomEntryPoint customEntryPoint(){
return new CustomEntryPoint();
}

@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtil, customUserDetailsService);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.example.umc10th.global.security.filter;

import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
import com.example.umc10th.global.apiPayload.code.GeneralErrorCode;
import com.example.umc10th.global.security.service.CustomUserDetailsService;
import com.example.umc10th.global.security.util.JwtUtil;
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 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