Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth 회원가입 및 로그인 기능 구현 #16

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
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
44 changes: 25 additions & 19 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,37 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// JWT 관련 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}

tasks.named('test') {
useJUnitPlatform()
}

spotless {
java {
importOrder(
'java|javax|jakarta',
'org.springframework',
'lombok',
'',
'org.junit|org.mockito',
'\\#',
'\\#org.junit'
)

googleJavaFormat()
formatAnnotations()
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
}
}
//spotless {
// java {
// importOrder(
// 'java|javax|jakarta',
// 'org.springframework',
// 'lombok',
// '',
// 'org.junit|org.mockito',
// '\\#',
// '\\#org.junit'
// )
//
// googleJavaFormat()
// formatAnnotations()
// removeUnusedImports()
// trimTrailingWhitespace()
// endWithNewline()
// }
//}

tasks.register("updateGitHooks", Copy) {
from "./scripts/pre-commit"
Expand Down
44 changes: 44 additions & 0 deletions src/main/java/com/gdgoc/study_group/auth/api/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.gdgoc.study_group.auth.api;

import com.gdgoc.study_group.auth.application.JoinService;
import com.gdgoc.study_group.auth.application.ReissueService;
import com.gdgoc.study_group.auth.dto.JoinDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
@Tag(name = "Auth", description = "로그인 API")
public class AuthController {

private final JoinService joinService;
private final ReissueService reissueService;

/**
* 학번과 비밀번호 등의 정보로 회원 가입을 합니다.
*
* @param request 회원 가입에 필요한 데이터 dto
* @return 생성 시, 201 회원 가입 성공 메시지
*/
@Operation(summary = "회원 가입", description = "학번과 비밀번호 등 정보 입력 시 회원 가입")
@PostMapping("/signup")
public ResponseEntity<Void> joinMember(@RequestBody JoinDto request) {

joinService.joinMember(request);

return ResponseEntity.status(HttpStatus.CREATED).build();
}

@PostMapping("/reissue")
public ResponseEntity<?> reissue(HttpServletRequest request, HttpServletResponse response) {

return reissueService.reissueToken(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.gdgoc.study_group.auth.application;

import com.gdgoc.study_group.auth.dao.AuthRepository;
import com.gdgoc.study_group.auth.domain.Auth;
import com.gdgoc.study_group.auth.dto.CustomUserDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final AuthRepository authRepository;

/**
* 학번으로 회원을 조회하여 반환합니다.
* @param studentNumber 조회할 대상 정보
* @return 조회된 회원 정보를 UserDetails 형식으로 반환
*/
@Override
public UserDetails loadUserByUsername(String studentNumber) throws UsernameNotFoundException {

Auth userData = authRepository.findByMember_StudentNumber(studentNumber);

if (userData == null) {
throw new UsernameNotFoundException("해당 학번의 사용자를 찾을 수 없습니다");
}

return new CustomUserDetails(userData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.gdgoc.study_group.auth.application;

import com.gdgoc.study_group.auth.dao.AuthRepository;
import com.gdgoc.study_group.auth.domain.Auth;
import com.gdgoc.study_group.auth.dto.JoinDto;
import com.gdgoc.study_group.member.dao.MemberRepository;
import com.gdgoc.study_group.member.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

private final AuthRepository authRepository;
private final MemberRepository memberRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;

/**
* 학번과 비밀번호 등의 정보로 회원 가입을 합니다.
*
* @param joinDto 회원 가입에 필요한 데이터 dto
*/
@Transactional(readOnly = false)
public void joinMember(JoinDto joinDto) {

String studentNumber = joinDto.studentNumber();

if (authRepository.existsByMember_StudentNumber(studentNumber)) {
throw new IllegalArgumentException("이미 존재하는 학번입니다.");
}

Member member = Member.builder()
.name(joinDto.name())
.github(joinDto.github())
.studentNumber(joinDto.studentNumber())
.build();

member = memberRepository.save(member);

Auth auth = Auth.builder()
.member(member)
.password(bCryptPasswordEncoder.encode(joinDto.password()))
.role("ROLE_USER")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이건 어떤 role 인가요? 일반 회원과 운영진인가여
그리고 enum 으로 따로 관리하면 어떨까요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네, 일반 유저와 관리자입니다.
enum으로 바꿀게요.

.build();

authRepository.save(auth);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.gdgoc.study_group.auth.application;

import com.gdgoc.study_group.auth.dao.AuthRepository;
import com.gdgoc.study_group.auth.dao.RefreshRepository;
import com.gdgoc.study_group.auth.domain.Auth;
import com.gdgoc.study_group.auth.domain.Refresh;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.NoSuchElementException;

@Service
@RequiredArgsConstructor
public class RefreshTokenService {

private final RefreshRepository refreshRepository;
private final AuthRepository authRepository;

/**
* 로그인 시 생성되는 리프레쉬 토큰을 데이터베이스에 저장합니다.
*
* @param authId 회원 정보
* @param refreshToken 저장하려는 리프레쉬 토큰
* @param expiredMs 저장하려는 리프레쉬 토큰의 만료 시간
*/
@Transactional
public void saveRefresh(Long authId, String refreshToken, Long expiredMs) {

Auth auth = authRepository.findById(authId)
.orElseThrow(() -> new NoSuchElementException("인증 정보를 찾을 수 없습니다."));

// 기존 사용자의 refresh 토큰이 있다면 삭제
refreshRepository.deleteByAuth(auth);
refreshRepository.flush();

Refresh refreshAuth = Refresh.builder()
.auth(auth)
.refresh(refreshToken)
.expiration(new Date(System.currentTimeMillis() + expiredMs).toString())
.build();

refreshRepository.save(refreshAuth);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.gdgoc.study_group.auth.application;

import com.gdgoc.study_group.auth.jwt.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ReissueService {
private final JwtUtil jwtUtil;

public ResponseEntity<?> reissueToken(HttpServletRequest request, HttpServletResponse response) {

String refresh = null;

/**
* 요청에서 refresh 쿠키를 찾아서,
* 해당 값의 null 여부를 확인합니다.
*/
Cookie[] cookies = request.getCookies();

for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
}
}

if (refresh == null) {
return new ResponseEntity<>("refresh token null", HttpStatus.BAD_REQUEST);
}

/**
* refresh 쿠키 존재 시,
* 만료 여부와 category를 확인합니다.
*/
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
return new ResponseEntity<>("refresh token expired", HttpStatus.BAD_REQUEST);
}

String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
return new ResponseEntity<>("invalid refresh token", HttpStatus.BAD_REQUEST);
}

/**
* refresh token 검증을 마치면,
* 새로운 access token을 발급합니다.
*/
Long authId = jwtUtil.getAuthId(refresh);
String studentNumber = jwtUtil.getStudentNumber(refresh);
String role = jwtUtil.getRole(refresh);

String newAccess = jwtUtil.createJWT("access", authId, studentNumber, role, 600000L);

// 응답
response.setHeader("Authorization", "Bearer " + newAccess);

return new ResponseEntity<>(HttpStatus.OK);
}

}
Loading