Skip to content

Commit

Permalink
Merge pull request #16 from cmsong111/15-리뷰-기능
Browse files Browse the repository at this point in the history
Security(JWT 적용) 및 댓글 기능 추가 및 Rest API 리팩토링
  • Loading branch information
cmsong111 authored Apr 16, 2024
2 parents e313cec + 44a3c1e commit e8633e6
Show file tree
Hide file tree
Showing 86 changed files with 1,817 additions and 1,730 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/Build&Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

- name: Set Environment Variables
run: echo "IMGBB_API_KEY=${{ secrets.IMGBB_API_KEY }}" >> $GITHUB_ENV

- name: Test with Gradle
run: ./gradlew --info test

Expand Down
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ repositories {

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-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.3.0'
implementation 'org.commonmark:commonmark:0.21.0'
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -37,6 +39,12 @@ dependencies {
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'

// JWT
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
}

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

@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2024-04-01T12:57:49+0900",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.2 (Oracle Corporation)"
date = "2024-04-16T02:14:38+0900",
comments = "version: 1.5.5.Final, compiler: javac, environment: Java 17.0.10 (Oracle Corporation)"
)
@Component
public class MapStructMapperImpl implements MapStructMapper {
Expand Down
76 changes: 49 additions & 27 deletions src/main/java/ac/kr/deu/connect/luck/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
package ac.kr.deu.connect.luck.auth;

import ac.kr.deu.connect.luck.user.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.*;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@Controller
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {

@Value("${security.jwt.token.header}")
private String AUTHORIZATION_HEADER;

@Value("${security.jwt.token.prefix}")
private String TOKEN_PREFIX;

private final AuthService authService;

@Autowired
public AuthController(AuthService authService) {
this.authService = authService;
}

@GetMapping("/login")
public String login() {
return "auth/login";
Expand All @@ -25,15 +37,16 @@ public String login() {
public String loginPost(
@RequestParam("email") String email,
@RequestParam("password") String password,
HttpServletRequest httpServletRequest) {
// 세션 초기화
httpServletRequest.getSession().invalidate();
HttpSession session = httpServletRequest.getSession(true);

HttpServletResponse response) {
// 로그인
User user = authService.login(new LoginRequest(email, password));
session.setAttribute("user", user);
session.setMaxInactiveInterval(60 * 30); // 30분
String token = authService.login(new LoginRequest(email, password));
String encodedToken = URLEncoder.encode(TOKEN_PREFIX + token, StandardCharsets.UTF_8);

// 쿠키에 토큰 저장
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, encodedToken);
cookie.setPath("/");
response.addCookie(cookie);

return "redirect:/";
}

Expand All @@ -50,10 +63,10 @@ public String signup() {
/**
* 회원가입 처리
*
* @param email 이메일
* @param password 비밀번호
* @param name 이름
* @param httpServletRequest HttpServletRequest(세션)
* @param email 이메일
* @param password 비밀번호
* @param name 이름
* @param
* @return 홈 화면
*/
@PostMapping("/signup")
Expand All @@ -62,18 +75,27 @@ public String signupPost(
@RequestParam("password") String password,
@RequestParam("name") String name,
@RequestParam("phone") String phone,
HttpServletRequest httpServletRequest) {
User user = authService.signUp(new SignUpRequest(email, password, name, phone));
HttpServletResponse response) {
// 회원가입 후 자동 로그인
// 쿠키 저장 시 토큰을 URL 인코딩(URLEncoder.encode)하여 저장
String token = authService.signUp(new SignUpRequest(email, password, name, phone));
String encodedToken = URLEncoder.encode(TOKEN_PREFIX + token, StandardCharsets.UTF_8);
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, encodedToken);
cookie.setMaxAge(60 * 30); // 30분
cookie.setPath("/");
response.addCookie(cookie);

HttpSession session = httpServletRequest.getSession(true);
session.setAttribute("user", user);
session.setMaxInactiveInterval(60 * 30); // 30분
return "redirect:/";
}

@GetMapping("/logout")
public String logout(HttpServletRequest httpServletRequest) {
httpServletRequest.getSession().invalidate();
public String logout(@CookieValue(value = "Authorization", defaultValue = "", required = false) Cookie jwtCookie,
HttpServletResponse httpServletResponse) {
// 쿠키 삭제
jwtCookie.setMaxAge(0);
jwtCookie.setPath("/");
jwtCookie.setValue(null);
httpServletResponse.addCookie(jwtCookie);
return "redirect:/";
}

Expand Down
29 changes: 19 additions & 10 deletions src/main/java/ac/kr/deu/connect/luck/auth/AuthRestController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ac.kr.deu.connect.luck.auth;

import ac.kr.deu.connect.luck.exception.CustomErrorResponse;
import ac.kr.deu.connect.luck.user.User;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
Expand All @@ -13,37 +12,47 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Tag(name = "Auth", description = "인증 관련 API")
@Tag(name = "01-Auth", description = "인증 관련 API - <b>Auth API 호출 시 Authorization header 부분이 비어 있어야 합니다.</b>")
@RestController
@RequestMapping("/api")
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthRestController {
private final AuthService authService;

@PostMapping("/email-check")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "아이디 중복 체크 성공", content = @Content(schema = @Schema(implementation = Boolean.class)))
})
@Operation(summary = "아이디 중복 체크", description = "<h1>아이디 중복 체크를 수행합니다.</h1><h2>반환 값</h2><li>true: 사용가능</li><li>false: 불가능</li>")
public ResponseEntity<Boolean> idCheck(
@Parameter(description = "이메일") @RequestParam("email") String email) {
return ResponseEntity.ok(authService.idCheck(email));
}

@PostMapping("/login")
@Operation(summary = "로그인")
@Operation(summary = "로그인", description = "입력된 이메일과 비밀번호를 사용하여 로그인을 진행합니다.<br>로그인 성공 시 JWT 토큰을 반환합니다.<br>인증이 필요한 요청 시 헤더에 Authorization Bear {JWT}를 포함하여 요청을 보내야 합니다.<br><b>주의: Auth 관련 API 호출 시 Header 부분이 비어 있어야 합니다.</b>")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "로그인 실패", content = @Content(schema = @Schema(implementation = CustomErrorResponse.class)))
})
public User login(
public String login(
@Parameter(description = "로그인 요청 정보") @RequestBody LoginRequest loginRequest) {
return authService.login(loginRequest);
}

@PostMapping("/signup")
@Operation(summary = "회원가입")
@Operation(summary = "회원가입", description = "입력된 폼을 바탕으로 로그인을 진행합니다.<br>사용 전 email-check를 통해 이메일이 사용가능 여부를 확인바랍니다.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "회원가입 성공", content = @Content(schema = @Schema(implementation = User.class))),
@ApiResponse(responseCode = "200", description = "회원가입 성공", content = @Content(schema = @Schema(implementation = String.class))),
@ApiResponse(responseCode = "400", description = "회원가입 실패", content = @Content(schema = @Schema(implementation = CustomErrorResponse.class)))
})
public User signUp(
public String signUp(
@Parameter(name = "로그인 객체", description = "회원가입 요청 정보") @RequestBody SignUpRequest signUpRequest) {
return authService.signUp(signUpRequest);
}

@PostMapping("/findEmailByPhone")
@Operation(summary = "휴대폰 번호로 이메일 찾기")
@Operation(summary = "휴대폰 번호로 이메일 찾기", description = "휴대폰 번호로 가입된 이메일을 찾습니다.")
public ResponseEntity<String> findEmailByPhone(
@Parameter(description = "전화번호") @RequestParam("phone") String phone) {
return ResponseEntity.ok(authService.findEmailByPhone(phone));
Expand Down
46 changes: 35 additions & 11 deletions src/main/java/ac/kr/deu/connect/luck/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
package ac.kr.deu.connect.luck.auth;

import ac.kr.deu.connect.luck.configuration.MapStructMapper;
import ac.kr.deu.connect.luck.exception.CustomErrorCode;
import ac.kr.deu.connect.luck.exception.CustomException;
import ac.kr.deu.connect.luck.security.JwtTokenProvider;
import ac.kr.deu.connect.luck.user.User;
import ac.kr.deu.connect.luck.user.UserMapper;
import ac.kr.deu.connect.luck.user.UserRepository;
import ac.kr.deu.connect.luck.user.UserRole;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;


@Slf4j
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
private final MapStructMapper mapStructMapper;
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;

/**
* 회원가입
*
* @param signUpRequest 회원가입 요청 정보
* @return User 엔티티
* @return String jwt 토큰
*/
public User signUp(SignUpRequest signUpRequest) {
public String signUp(SignUpRequest signUpRequest) {
if (userRepository.existsByEmail(signUpRequest.email())) throw new CustomException(CustomErrorCode.ALREADY_EXIST_USER_ID);

User user = mapStructMapper.toUser(signUpRequest);
user.setRole(UserRole.USER);
return userRepository.save(user);
User user = userMapper.toUser(signUpRequest);
List<UserRole> roles = new ArrayList<>();
roles.add(UserRole.USER);
user.setRoles(roles);
user.setPassword(passwordEncoder.encode(user.getPassword()));
User savedUser = userRepository.save(user);

return jwtTokenProvider.createToken(savedUser.getEmail(), savedUser.getRoles());
}

/**
Expand All @@ -38,18 +50,30 @@ public User signUp(SignUpRequest signUpRequest) {
* @param loginRequest 로그인 요청 정보
* @return User 엔티티
*/
public User login(LoginRequest loginRequest) {
public String login(LoginRequest loginRequest) {
Optional<User> user = userRepository.findByEmail(loginRequest.email());

if (user.isEmpty()) throw new CustomException(CustomErrorCode.EMAIL_NOT_FOUND);
if (!user.get().getPassword().equals(loginRequest.password())) throw new CustomException(CustomErrorCode.PASSWORD_NOT_MATCH);
if (!passwordEncoder.matches(loginRequest.password(), user.get().getPassword()))
throw new CustomException(CustomErrorCode.PASSWORD_NOT_MATCH);

return jwtTokenProvider.createToken(user.get().getEmail(), user.get().getRoles());

return user.get();
}

public String findEmailByPhone(String phone) {
Optional<User> user = userRepository.findByPhone(phone);
if (user.isEmpty()) throw new CustomException(CustomErrorCode.PHONE_NOT_FOUND);
return user.get().getEmail();
}

/**
* 아이디 중복 체크
*
* @param email 사용하려는 이메일
* @return True: 사용 가능, False: 사용중인 이메일
*/
public boolean idCheck(String email) {
return !userRepository.existsByEmail(email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
public record SignUpRequest(
@Schema(description = "이메일", example = "[email protected]")
String email,
@Schema(description = "비밀번호", example = "test1234")
@Schema(description = "비밀번호", example = "test")
String password,
@Schema(description = "이름", example = "홍길동")
String name,
Expand Down

This file was deleted.

This file was deleted.

Loading

0 comments on commit e8633e6

Please sign in to comment.