Skip to content
Merged
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 .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
SPRING_PROFILES_ACTIVE=local
# prod에서 local로 변경
LOCAL_DB_URL=jdbc:mysql://localhost:3306/cokerthon-local?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&createDatabaseIfNotExist=true
LOCAL_DB_USERNAME=root
LOCAL_DB_PASSWORD=dlgodnjs!
PROD_DB_URL=jdbc:mysql://cokerthon-db.clgeccwgurgu.ap-northeast-2.rds.amazonaws.com:3306/cokathon?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
PROD_DB_USERNAME=admin
PROD_DB_PASSWORD=cokerthonpassword
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -37,7 +38,10 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

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

@EnableJpaAuditing
@SpringBootApplication
public class Team3BackendApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.cokerthon.Team3_Backend.domain.auth.controller;

import com.cokerthon.Team3_Backend.domain.auth.dto.request.LoginRequest;
import com.cokerthon.Team3_Backend.domain.auth.dto.request.SignupRequest;
import com.cokerthon.Team3_Backend.domain.auth.dto.response.AuthResponse;
import com.cokerthon.Team3_Backend.domain.auth.exception.AuthSuccessCode;
import com.cokerthon.Team3_Backend.domain.auth.service.AuthService;
import com.cokerthon.Team3_Backend.global.apiPayload.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
@Tag(name = "Auth API")
public class AuthController {

private final AuthService authService;

@Operation(summary = "회원가입 API", description = "닉네임, 아이디, 비밀번호를 입력받아 회원가입합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "회원가입 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "이미 존재하는 닉네임 또는 아이디")
})
@PostMapping("/signup")
public ApiResponse<AuthResponse> signUp(@Valid @RequestBody SignupRequest request) {

AuthResponse response = authService.signUp(request);
return ApiResponse.onSuccess(AuthSuccessCode.SIGNUP_SUCCESS, response);
}

@Operation(summary = "로그인 API", description = "아이디와 비밀번호로 로그인합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "로그인 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "비밀번호 불일치")
})
@PostMapping("/login")
public ApiResponse<AuthResponse> login(@Valid @RequestBody LoginRequest request, HttpServletRequest httpRequest) {

AuthResponse response = authService.login(request);

// 세션 생성
HttpSession session = httpRequest.getSession(true);
SecurityContext securityContext = SecurityContextHolder.getContext();
session.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
securityContext
);

return ApiResponse.onSuccess(AuthSuccessCode.LOGIN_SUCCESS, response);
}

@Operation(summary = "로그아웃 API", description = "현재 사용자의 세션을 만료시키고 쿠키를 삭제합니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "로그아웃 성공"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "인증되지 않은 사용자")
})
@PostMapping("/logout")
public ApiResponse<Void> logout(HttpServletRequest request, HttpServletResponse response) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

// 인증 정보 있으면 로그아웃 처리
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
return ApiResponse.onSuccess(AuthSuccessCode.LOGOUT_SUCCESS, null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.cokerthon.Team3_Backend.domain.auth.dto.request;


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

public record LoginRequest(
@Schema(description = "로그인 아이디", example = "test")
@NotBlank(message = "아이디는 필수입니다.")
String loginId,

@Schema(description = "비밀번호", example = "password")
@NotBlank(message = "비밀번호는 필수입니다.")
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cokerthon.Team3_Backend.domain.auth.dto.request;

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

public record SignupRequest(

@Schema(description = "닉네임", example = "닉네임")
@NotBlank(message = "닉네임은 필수입니다.")
String nickname,

@Schema(description = "로그인 아이디", example = "test")
@NotBlank(message = "아이디는 필수입니다.")
String loginId,

@Schema(description = "비밀번호", example = "password")
@NotBlank(message = "비밀번호는 필수입니다.")
String password
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.cokerthon.Team3_Backend.domain.auth.dto.response;

import com.cokerthon.Team3_Backend.domain.user.entity.User;
import lombok.Builder;

@Builder
public record AuthResponse(
Long id,
String nickname,
String loginId
){
public static AuthResponse from(User user) {
return new AuthResponse(
user.getId(),
user.getNickname(),
user.getLoginId()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.cokerthon.Team3_Backend.domain.auth.exception;

import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum AuthErrorCode implements BaseErrorCode {


UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "AUTH-001", "비밀번호가 일치하지 않습니다."),
NOT_FOUND(HttpStatus.NOT_FOUND, "AUTH-002", "사용자를 찾을 수 없습니다."),
DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "AUTH-003", "이미 존재하는 닉네임입니다."),
DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "AUTH-004", "이미 존재하는 아이디입니다."),

;

private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public HttpStatus getStatus() {
return this.httpStatus;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.cokerthon.Team3_Backend.domain.auth.exception;

import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseErrorCode;
import com.cokerthon.Team3_Backend.global.exception.GeneralException;

public class AuthException extends GeneralException {
public AuthException(BaseErrorCode code) { super(code); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cokerthon.Team3_Backend.domain.auth.exception;

import com.cokerthon.Team3_Backend.global.apiPayload.code.BaseSuccessCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum AuthSuccessCode implements BaseSuccessCode {

SIGNUP_SUCCESS(HttpStatus.OK, "SIGNUP200_1", "회원가입에 성공했습니다."),
LOGIN_SUCCESS(HttpStatus.OK, "LOGIN200_1", "로그인에 성공했습니다."),
LOGOUT_SUCCESS(HttpStatus.OK, "LOGOUT200_1", "로그아웃에 성공했습니다.")

;
private final HttpStatus status;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.cokerthon.Team3_Backend.domain.auth.service;

import com.cokerthon.Team3_Backend.domain.auth.dto.request.LoginRequest;
import com.cokerthon.Team3_Backend.domain.auth.dto.request.SignupRequest;
import com.cokerthon.Team3_Backend.domain.auth.dto.response.AuthResponse;
import com.cokerthon.Team3_Backend.domain.auth.exception.AuthErrorCode;
import com.cokerthon.Team3_Backend.domain.auth.exception.AuthException;
import com.cokerthon.Team3_Backend.domain.user.entity.User;
import com.cokerthon.Team3_Backend.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;

@Transactional
public AuthResponse signUp(SignupRequest request) {

// 닉네임 중복 확인
if (userRepository.findByNickname(request.nickname()).isPresent()) {
throw new AuthException(AuthErrorCode.DUPLICATE_NICKNAME);
}

// 아이디 중복 확인
if (userRepository.findByLoginId(request.loginId()).isPresent()) {
throw new AuthException(AuthErrorCode.DUPLICATE_LOGIN_ID);
}

String encodedPassword = passwordEncoder.encode(request.password());

// 4. User 엔티티 생성 및 저장
User user = User.builder()
.loginId(request.loginId())
.password(encodedPassword)
.nickname(request.nickname())
.build();

User newUser = userRepository.save(user);
return AuthResponse.from(user);
}

public AuthResponse login(LoginRequest request) {

User user = userRepository.findByLoginId(request.loginId())
.orElseThrow(() -> new AuthException(AuthErrorCode.NOT_FOUND));

// 시큐리티에게 로그인 검사 요청
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
request.loginId(),
request.password()
);

try {
// 비밀번호 확인
Authentication authentication = authenticationManager.authenticate(token);

// 세션 확인
SecurityContextHolder.getContext().setAuthentication(authentication);
return AuthResponse.from(user);
} catch (BadCredentialsException e) {
throw new AuthException(AuthErrorCode.UNAUTHORIZED);
}


}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@ public class User extends BaseEntity {
private String password;

/** 닉네임 */
@Column(nullable = false, length = 256)
@Column(nullable = false, length = 256, unique = true)
private String nickname;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.cokerthon.Team3_Backend.domain.user.repository;

import com.cokerthon.Team3_Backend.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {

Optional<User> findByLoginId(String loginId);

Optional<User> findByNickname(String nickname); }
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public enum GeneralErrorCode implements BaseErrorCode {
BAD_REQUEST(HttpStatus.BAD_REQUEST,
"COMMON400_1",
"잘못된 요청입니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED,
"COMMON401_1",
"인증이 필요합니다."),
NOT_FOUND(HttpStatus.NOT_FOUND,
"COMMON404_1",
"요청한 리소스를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,5 @@ public CorsConfigurationSource corsConfigurationSource() {
source.registerCorsConfiguration("/**", config);
return source;
}

}
Loading