Skip to content

Commit

Permalink
feat: 로그인 기능 구현 (#177)
Browse files Browse the repository at this point in the history
* feat: password 일방향 암호화 기능 구현

* feat: cookie 생산-소비 기능 구현

* chore: jwt 관련 의존성 추가

* feat: 토큰 생성 기능 구현

* feat: 로그인 API 구현

* test: 로그인 API 테스트

* feat: 회원가입 API 구현

* test: 회원가입 API 테스트

* feat: 닉네임 생성 기능 구현

* test: 닉네임 생성 기능 테스트

* fix: postconstruct 여러 개라 발생한 에러 해결

* feat: 회원가입 응답값에 랜덤생성한 닉네임 추가

* feat: MemberArgumentResolver 구현

* feat: MemberArgumentResolver 일부 적용

* test: 바뀐 스펙에 맞게 변경

* test: TestConfig 설정해 빈충돌 오류 해결

* test: 공모 작성 API로 MemberArgumentResolver 사용

* feat: 토큰 재발급 API 구현

* test: 토큰 재발급 API 테스트

* test: 토큰 재발급 API 에러 테스트

* feat: MemberArgumentResolver commant에 적용

* feat: MemberArgumentResolver offering에 적용

* feat: MemberArgumentResolver participant에 적용

* refactor: ci값이 일치하지 않을경우 오류메시지 문구 변경

* refactor: 클래스명 일관적으로 변경

* refactor: 직관적인 명명으로 enum 네이밍 변경

* refactor: Custom Exception 적용

* refactor: 컨트롤러 메서드에 접근제어자 명시

* fix: 중복된 enum 값 제거

* test: 바뀐 API 스펙에 맞게 변경

---------

Co-authored-by: fromitive <[email protected]>
  • Loading branch information
ChooSeoyeon and fromitive authored Aug 6, 2024
1 parent fb8b3e0 commit df16d2e
Show file tree
Hide file tree
Showing 58 changed files with 1,069 additions and 232 deletions.
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ dependencies {
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.765'
implementation 'org.jsoup:jsoup:1.18.1'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
23 changes: 23 additions & 0 deletions backend/http/auth.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
### 로그인 API
POST {{base-url}}/auth/login
Content-Type: application/json

{
"ci": "dora1234"
}

### 회원가입 API
POST {{base-url}}/auth/signup
Content-Type: application/json

{
"ci": "poke12345678"
}

### 토큰 재발급 API
POST {{base-url}}/auth/refresh
Content-Type: application/json

{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiZXhwIjoxNzI0MTMwODY0fQ.BoN2p--HbWn1zbed2NSZtZm4RumWtVm338sAtxXwEC8"
}
2 changes: 1 addition & 1 deletion backend/http/comment.http
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
### 댓글방 목록 조회 API
GET {{base-url}}/comments?member-id=1
GET {{base-url}}/comments
20 changes: 19 additions & 1 deletion backend/http/offering.http
Original file line number Diff line number Diff line change
@@ -1,8 +1,26 @@
### 공모 상세 조회 API
GET {{base-url}}/offerings/1?member-id=1
GET {{base-url}}/offerings/1

### 공모 목록 조회 API
GET {{base-url}}/offerings?filter=RECENT&search=&last-id=99&page-size=2

### 공모 필터 목록 조회 API
GET {{base-url}}/offerings/filters

### 공모 작성 API
POST {{base-url}}/offerings
Content-Type: application/json

{
"title": "공모 제목",
"productUrl": "www.naver.com",
"thumbnailUrl": "www.naver.com/favicon.ico",
"totalCount": 5,
"totalPrice": 10000,
"eachPrice": 2000,
"meetingAddress": "서울특별시 광진구 구의강변로 3길 11",
"meetingAddressDetail": "상세주소아파트",
"meetingAddressDong": "구의동",
"deadline": "2024-10-11T10:00:00",
"description": "내용입니다."
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.zzang.chongdae.auth.config;

import com.zzang.chongdae.auth.controller.CookieConsumer;
import com.zzang.chongdae.auth.service.AuthService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class AuthWebMvcConfig implements WebMvcConfigurer {

private final AuthService authService;
private final CookieConsumer cookieConsumer;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new MemberArgumentResolver(authService, cookieConsumer));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.zzang.chongdae.auth.config;

import com.zzang.chongdae.auth.controller.CookieConsumer;
import com.zzang.chongdae.auth.service.AuthService;
import com.zzang.chongdae.member.repository.entity.MemberEntity;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

@AllArgsConstructor
public class MemberArgumentResolver implements HandlerMethodArgumentResolver {

private final AuthService authService;
private final CookieConsumer cookieConsumer;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(MemberEntity.class);
}

@Override
public MemberEntity resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
String token = cookieConsumer.getAccessToken(request.getCookies());
return authService.findMemberByAccessToken(token);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.zzang.chongdae.auth.controller;

import com.zzang.chongdae.auth.service.AuthService;
import com.zzang.chongdae.auth.service.dto.LoginRequest;
import com.zzang.chongdae.auth.service.dto.RefreshRequest;
import com.zzang.chongdae.auth.service.dto.SignupRequest;
import com.zzang.chongdae.auth.service.dto.SignupResponse;
import com.zzang.chongdae.auth.service.dto.TokenDto;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RestController
public class AuthController {

private final AuthService authService;
private final CookieProducer cookieExtractor;
private final CookieConsumer cookieConsumer;

@PostMapping("/auth/login")
public ResponseEntity<Void> login(
@RequestBody @Valid LoginRequest request, HttpServletResponse servletResponse) {
TokenDto tokenDto = authService.login(request);
List<Cookie> cookies = cookieExtractor.extractAuthCookies(tokenDto);
cookieConsumer.addCookies(servletResponse, cookies);
return ResponseEntity.ok().build();
}

@PostMapping("/auth/signup")
public ResponseEntity<SignupResponse> signup(
@RequestBody SignupRequest request) {
SignupResponse response = authService.signup(request);
return ResponseEntity.ok(response);
}

@PostMapping("/auth/refresh")
public ResponseEntity<Void> refresh(
@RequestBody RefreshRequest request, HttpServletResponse servletResponse) {
TokenDto tokenDto = authService.refresh(request);
List<Cookie> cookies = cookieExtractor.extractAuthCookies(tokenDto);
cookieConsumer.addCookies(servletResponse, cookies);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.zzang.chongdae.auth.controller;

import com.zzang.chongdae.auth.exception.AuthErrorCode;
import com.zzang.chongdae.global.exception.MarketException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class CookieConsumer {

private static final String ACCESS_TOKEN_COOKIE_NAME = "access_token";
private static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";

public void addCookies(HttpServletResponse servletResponse, List<Cookie> cookies) {
for (Cookie cookie : cookies) {
servletResponse.addCookie(cookie);
}
}

public String getAccessToken(Cookie[] cookies) {
return getTokenByCookieName(ACCESS_TOKEN_COOKIE_NAME, cookies);
}

private String getTokenByCookieName(String cookieName, Cookie[] cookies) {
if (cookies == null) {
throw new MarketException(AuthErrorCode.INVALID_TOKEN);
}
return Arrays.stream(cookies)
.filter(cookie -> cookie.getName().equals(cookieName))
.findFirst()
.map(Cookie::getValue)
.orElseThrow(() -> new MarketException(AuthErrorCode.INVALID_TOKEN));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.zzang.chongdae.auth.controller;

import com.zzang.chongdae.auth.service.dto.TokenDto;
import jakarta.servlet.http.Cookie;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Component;

@Component
public class CookieProducer {

private static final String ACCESS_TOKEN_COOKIE_NAME = "access_token";
private static final String REFRESH_TOKEN_COOKIE_NAME = "refresh_token";

public List<Cookie> extractAuthCookies(TokenDto tokenDto) {
List<Cookie> cookies = new ArrayList<>();
cookies.add(createCookie(ACCESS_TOKEN_COOKIE_NAME, tokenDto.accessToken()));
cookies.add(createCookie(REFRESH_TOKEN_COOKIE_NAME, tokenDto.refreshToken()));
return cookies;
}

private Cookie createCookie(String tokenName, String token) {
Cookie cookie = new Cookie(tokenName, token);
cookie.setHttpOnly(true);
cookie.setPath("/");
return cookie;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.zzang.chongdae.auth.exception;

import com.zzang.chongdae.global.exception.ErrorMessage;
import com.zzang.chongdae.global.exception.ErrorResponse;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum AuthErrorCode implements ErrorResponse {

INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."),
EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."),
INVALID_PASSWORD(HttpStatus.NOT_FOUND, "가입하지 않은 회원입니다."),
DUPLICATED_MEMBER(HttpStatus.CONFLICT, "이미 가입한 회원입니다.");

private final HttpStatus status;
private final String message;

@Override
public ErrorMessage getErrorMessage() {
return new ErrorMessage(this.message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.zzang.chongdae.auth.service;

import com.zzang.chongdae.auth.exception.AuthErrorCode;
import com.zzang.chongdae.auth.service.dto.LoginRequest;
import com.zzang.chongdae.auth.service.dto.RefreshRequest;
import com.zzang.chongdae.auth.service.dto.SignupRequest;
import com.zzang.chongdae.auth.service.dto.SignupResponse;
import com.zzang.chongdae.auth.service.dto.TokenDto;
import com.zzang.chongdae.global.exception.MarketException;
import com.zzang.chongdae.member.exception.MemberErrorCode;
import com.zzang.chongdae.member.repository.MemberRepository;
import com.zzang.chongdae.member.repository.entity.MemberEntity;
import com.zzang.chongdae.member.service.NicknameGenerator;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@RequiredArgsConstructor
@Service
public class AuthService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final NicknameGenerator nickNameGenerator;

public TokenDto login(LoginRequest request) {
String password = passwordEncoder.encode(request.ci());
MemberEntity member = memberRepository.findByPassword(password)
.orElseThrow(() -> new MarketException(AuthErrorCode.INVALID_PASSWORD));
return jwtTokenProvider.createAuthToken(member.getId().toString());
}

@Transactional
public SignupResponse signup(SignupRequest request) {
String password = passwordEncoder.encode(request.ci());
if (memberRepository.existsByPassword(password)) {
throw new MarketException(AuthErrorCode.DUPLICATED_MEMBER);
}
MemberEntity member = new MemberEntity(nickNameGenerator.generate(), password);
MemberEntity savedMember = memberRepository.save(member);
return new SignupResponse(savedMember);
}

public TokenDto refresh(RefreshRequest request) {
Long memberId = jwtTokenProvider.getMemberIdByRefreshToken(request.refreshToken());
return jwtTokenProvider.createAuthToken(memberId.toString());
}

public MemberEntity findMemberByAccessToken(String token) {
Long memberId = jwtTokenProvider.getMemberIdByAccessToken(token);
return memberRepository.findById(memberId)
.orElseThrow(() -> new MarketException(MemberErrorCode.NOT_FOUND));
}
}
Loading

0 comments on commit df16d2e

Please sign in to comment.