Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0dc6178
feat: 로그인 기능 기본 구조 구현 (#11)
lgw323 Aug 11, 2025
3c65433
feat(#11): JWT 의존성 추가
lgw323 Aug 17, 2025
0afd832
feat(#11): SecurityConfig 설정
lgw323 Aug 17, 2025
9e1a23a
feat(#11): JWT 구현
lgw323 Aug 17, 2025
ab34f73
refactor(#11): Member 도메인에서 로그인 관련 로직 제거
lgw323 Aug 17, 2025
b99c46b
feat(#11): 인증용 DTO(AuthLoginRequest) 추가
lgw323 Aug 17, 2025
11ff918
feat(#11): 로그인 비즈니스 로직을 처리하는 AuthService 구현
lgw323 Aug 17, 2025
818dc31
feat(#11): 로그인 API 엔드포인트를 위한 AuthController 구현
lgw323 Aug 17, 2025
ed12892
feat(#11): Auth 도메인 관련 Success/Exception 코드 정의
lgw323 Aug 17, 2025
0cd2a08
test(#11): AuthFixture 작성
lgw323 Aug 17, 2025
27a01f8
test(#11): AuthService 로그인 테스트 추가
lgw323 Aug 17, 2025
63c34e5
test(#11): AuthController 통합 테스트 추가
lgw323 Aug 17, 2025
0887b95
refactor(#11): 불필요해진 MemberLoginRequest 삭제
lgw323 Aug 17, 2025
c51cd1b
style(#11): 불필요한 import문 제거
lgw323 Aug 17, 2025
3fdc645
feat(#11): 로그인 API 응답 DTO 추가
lgw323 Aug 26, 2025
93936c4
feat(#11): 액세스/리프레시 토큰 발급 로직 구현
lgw323 Aug 26, 2025
a9c1382
feat(#11): 로그인 API 응답 형식 변경
lgw323 Aug 26, 2025
199f692
docs(#11): 로그인 API 문서 응답 DTO 수정
lgw323 Aug 26, 2025
afbd3a3
test(#11): AuthService 로그인 로직 테스트 수정
lgw323 Aug 26, 2025
2dc09b5
test(#11): AuthController 로그인 API 테스트 수정
lgw323 Aug 26, 2025
17f733b
refactor(#11): 불필요한 import문 제거
lgw323 Aug 26, 2025
75978b6
refactor(#11): 예외 코드 수정
lgw323 Aug 26, 2025
70e81ed
refactor(#11): JwtUtil이 설정 파일에서 키와 만료 시간을 읽도록 수정
lgw323 Aug 26, 2025
2101705
refactor(#11): 변경된 JwtUtil 토큰 생성 메서드 적용
lgw323 Aug 26, 2025
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package kr.ac.ks.cs_web_back.domain.auth.controller;

import jakarta.validation.Valid;
import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthSuccessCode;
import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest;
import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse;
import kr.ac.ks.cs_web_back.domain.auth.service.AuthService;
import kr.ac.ks.cs_web_back.global.response.CsResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthController implements SpringDocAuthController{

private final AuthService authService;

@PostMapping("/login")
public CsResponse<AuthLoginResponse> login(
@Valid @RequestBody AuthLoginRequest request
) {
AuthLoginResponse token = authService.loginMember(request);
return CsResponse.of(AuthSuccessCode.LOGIN_SUCCESS, token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package kr.ac.ks.cs_web_back.domain.auth.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest;
import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse;
import kr.ac.ks.cs_web_back.global.response.CsResponse;
import kr.ac.ks.cs_web_back.global.swagger.error.ApiErrorResponse;
import kr.ac.ks.cs_web_back.global.swagger.error.ErrorCase;
import org.springframework.http.HttpStatus;

@Tag(name = "인증 API", description = "인증 관련 API")
public interface SpringDocAuthController {

@Operation(summary = "로그인", description = "사용자의 이메일과 비밀번호로 로그인하고, 성공시 JWT 토큰을 반환합니다.")
@ApiResponse(responseCode = "200", description = "로그인에 성공했습니다.")
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/auth/login", errorCases = {
@ErrorCase(description = "이메일 없음", code = 9010, exampleMessage = "이메일이 입력되지 않았습니다."),
@ErrorCase(description = "비밀번호 없음", code = 9011, exampleMessage = "비밀번호가 입력되지 않았습니다.")
})
@ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/auth/login", errorCases = {
@ErrorCase(description = "비밀번호 불일치", code = 8011, exampleMessage = "비밀번호가 일치하지 않습니다.")
})
@ApiErrorResponse(status = HttpStatus.NOT_FOUND, instance = "/auth/login", errorCases = {
@ErrorCase(description = "사용자 없음", code = 6001, exampleMessage = "일치하는 사용자가 존재하지 않습니다.")
})
CsResponse<AuthLoginResponse> login(
@RequestBody AuthLoginRequest request
);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package kr.ac.ks.cs_web_back.domain.auth.controller.code;

import kr.ac.ks.cs_web_back.global.exeption.dto.ExceptionCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum AuthExceptionCode implements ExceptionCode {

BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."),
BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."),
UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."),
NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."),
;

private final int code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package kr.ac.ks.cs_web_back.domain.auth.controller.code;

import kr.ac.ks.cs_web_back.global.response.SuccessCode;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum AuthSuccessCode implements SuccessCode {

LOGIN_SUCCESS(2001, "로그인에 성공했습니다."),
;

private final int code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kr.ac.ks.cs_web_back.domain.auth.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

public record AuthLoginRequest(
@Schema(description = "이메일", example = "example@ks.ac.kr")
@NotBlank(message = "이메일이 입력되지 않았습니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email,
@Schema(description = "비밀번호", example = "examplePassword123")
@NotBlank(message = "비밀번호가 입력되지 않았습니다.")
String password
) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package kr.ac.ks.cs_web_back.domain.auth.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@Builder
public record AuthLoginResponse(
@Schema(description = "액세스 토큰", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJle...")
String authorization,
@Schema(description = "리프레시 토큰", example = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJle...")
String authorizationRefresh
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package kr.ac.ks.cs_web_back.domain.auth.service;

import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode;
import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest;
import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse;
import kr.ac.ks.cs_web_back.domain.member.model.Member;
import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository;
import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException;
import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException;
import kr.ac.ks.cs_web_back.global.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class AuthService {

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

public AuthLoginResponse loginMember(AuthLoginRequest request) {
Member member = memberRepository.findByEmail(request.email())
.orElseThrow(() -> new NotFoundException(AuthExceptionCode.NOT_FOUND_USER));

if(!passwordEncoder.matches(request.password(), member.getPassword()))
throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_PASSWORD);
// 24 hours
String accessToken = jwtUtil.generateAccessToken(member.getEmail());
String refreshToken = jwtUtil.generateRefreshToken(member.getEmail());
return AuthLoginResponse.builder()
.authorization(accessToken)
.authorizationRefresh(refreshToken)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import jakarta.validation.Valid;
import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberSuccessCode;
import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest;
import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest;
import kr.ac.ks.cs_web_back.domain.member.service.MemberService;
import kr.ac.ks.cs_web_back.global.response.CsResponse;
import lombok.RequiredArgsConstructor;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum MemberExceptionCode implements ExceptionCode {
BAD_REQUEST_NO_USERNAME(9012, "사용자명이 입력되지 않았습니다."),
CONFLICT_EMAIL(5010, "이미 존재하는 이메일입니다."),
CONFLICT_USERNAME(5012, "이미 존재하는 사용자명입니다."),
NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."),
;

private final int code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import kr.ac.ks.cs_web_back.domain.member.model.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsByEmail(String email);
boolean existsByUsername(String username);

Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

import kr.ac.ks.cs_web_back.domain.member.controller.code.MemberExceptionCode;
import kr.ac.ks.cs_web_back.domain.member.dto.request.MemberCreateRequest;
import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest;
import kr.ac.ks.cs_web_back.domain.member.model.Member;
import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository;
import kr.ac.ks.cs_web_back.global.exeption.domain.ConflictException;
import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException;
import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException;
import kr.ac.ks.cs_web_back.global.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers(PathRequest.toH2Console()).permitAll() // H2 콘솔 경로 허용
.requestMatchers("/member/register", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/member/register", "/auth/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated()
);

Expand Down
49 changes: 49 additions & 0 deletions src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kr.ac.ks.cs_web_back.global.jwt;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Base64;
import java.util.Date;

@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;

@Value("${jwt.accessTokenExpireTime}")
private Long accessTokenExpireTime;

@Value("${jwt.refreshTokenExpireTime}")
private Long refreshTokenExpireTime;

private Key hmacKey;

@PostConstruct
public void init() {
byte[] decodedKey = Base64.getDecoder().decode(secret);
this.hmacKey = new SecretKeySpec(decodedKey, SignatureAlgorithm.HS256.getJcaName());
}
public String generateAccessToken(String email) {
return generateToken(email, accessTokenExpireTime);
}

public String generateRefreshToken(String email) {
return generateToken(email, refreshTokenExpireTime);
}

public String generateToken(String email, Long expireTime) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expireTime);
return Jwts.builder().setSubject(email)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(hmacKey)
.compact();
}
}
Loading
Loading