Skip to content

Commit

Permalink
Merge branch 'feature' into BE/#168
Browse files Browse the repository at this point in the history
  • Loading branch information
kimhalin authored Jul 16, 2023
2 parents 97eef60 + a6fd5b0 commit 450faa1
Show file tree
Hide file tree
Showing 24 changed files with 528 additions and 82 deletions.
5 changes: 5 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ dependencies {

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis:2.3.1.RELEASE'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.graphy.backend.domain.follow.service;

import com.graphy.backend.domain.follow.domain.Follow;
import com.graphy.backend.domain.member.domain.Member;
import com.graphy.backend.domain.member.dto.MemberListDto;
import com.graphy.backend.domain.follow.repository.FollowRepository;
import com.graphy.backend.domain.member.repository.MemberRepository;
import com.graphy.backend.global.auth.jwt.CustomUserDetailsService;
import com.graphy.backend.global.error.ErrorCode;
import com.graphy.backend.global.error.exception.AlreadyFollowingException;
import com.graphy.backend.global.error.exception.AlreadyExistException;
import com.graphy.backend.global.error.exception.EmptyResultException;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -56,7 +55,7 @@ public List<MemberListDto> getFollowings() {

private void followingCheck(Long fromId, Long toId) {
if (followRepository.existsByFromIdAndToId(fromId, toId)) {
throw new AlreadyFollowingException(ErrorCode.FOLLOW_ALREADY_EXIST);
throw new AlreadyExistException(ErrorCode.FOLLOW_ALREADY_EXIST);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.graphy.backend.domain.member.service.MemberService;
import com.graphy.backend.global.auth.jwt.annotation.CurrentUser;
import com.graphy.backend.global.auth.jwt.dto.TokenInfo;
import com.graphy.backend.global.auth.jwt.dto.TokenDto;
import com.graphy.backend.global.result.ResultCode;
import com.graphy.backend.global.result.ResultResponse;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -14,9 +15,11 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

import static com.graphy.backend.domain.member.dto.MemberDto.*;
import static com.graphy.backend.global.auth.jwt.dto.TokenDto.*;

@Tag(name = "MemberController", description = "회원 API")
@RestController
Expand All @@ -27,8 +30,9 @@ public class MemberController {

@Operation(summary = "login", description = "로그인")
@PostMapping("/login")
public TokenInfo login(@RequestBody LoginMemberRequest request) {
return memberService.login(request);
public TokenInfo login(HttpServletRequest request,
@RequestBody LoginMemberRequest dto) {
return memberService.login(request, dto);
}

@Operation(summary = "join", description = "회원가입")
Expand All @@ -42,13 +46,26 @@ public ResponseEntity<ResultResponse> join(@Validated @RequestBody CreateMemberR
@GetMapping()
public ResponseEntity<ResultResponse> findMember(@RequestParam String nickname) {
List<GetMemberResponse> result = memberService.findMember(nickname);
return ResponseEntity.ok(ResultResponse.of(ResultCode.MEMBER_CREATE_SUCCESS, result));
return ResponseEntity.ok(ResultResponse.of(ResultCode.MEMBER_GET_SUCCESS, result));
}

@Operation(summary = "myPage", description = "마이페이지")
@GetMapping("/myPage")
public ResponseEntity<ResultResponse> myPage(@CurrentUser Member member) {
GetMyPage result = memberService.myPage(member);
GetMyPageResponse result = memberService.myPage(member);
return ResponseEntity.ok(ResultResponse.of(ResultCode.MYPAGE_GET_SUCCESS, result));
}

@Operation(summary = "reIssue", description = "토큰 재발급")
@PostMapping(value = "/reissue")
public TokenInfo reissue(HttpServletRequest request) {
return memberService.reissue(request);
}

@Operation(summary = "logout", description = "로그아웃")
@PostMapping(value = "/logout")
public ResponseEntity<ResultResponse> logout(@RequestBody LogoutRequest dto) {
memberService.logout(dto);
return ResponseEntity.ok(ResultResponse.of(ResultCode.MEMBER_LOGOUT_SUCCESS));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import com.graphy.backend.domain.member.domain.Member;
import com.graphy.backend.domain.member.domain.Role;
import com.graphy.backend.domain.project.domain.Like;
import com.graphy.backend.domain.project.dto.ProjectDto;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -71,15 +69,15 @@ public Member toEntity(String encodedPassword) {
@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class GetMyPage {
public static class GetMyPageResponse {
private String nickname;
private String introduction;
private int followerCount;
private int followingCount;
private List<ProjectInfo> projectInfoList;

public static GetMyPage from(Member member, List<ProjectInfo> projectInfoList) {
return GetMyPage.builder()
public static GetMyPageResponse from(Member member, List<ProjectInfo> projectInfoList) {
return GetMyPageResponse.builder()
.nickname(member.getNickname())
.introduction(member.getIntroduction())
.followerCount(member.getFollowerCount())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@
import com.graphy.backend.domain.member.repository.MemberRepository;
import com.graphy.backend.domain.project.service.ProjectService;
import com.graphy.backend.global.auth.jwt.CustomUserDetailsService;
import com.graphy.backend.global.auth.jwt.JwtFilter;
import com.graphy.backend.global.auth.jwt.TokenProvider;
import com.graphy.backend.global.auth.jwt.dto.TokenInfo;
import com.graphy.backend.global.auth.redis.domain.RefreshToken;
import com.graphy.backend.global.auth.redis.repository.RefreshTokenRepository;
import com.graphy.backend.global.common.Helper;
import com.graphy.backend.global.error.ErrorCode;
import com.graphy.backend.global.error.exception.AlreadyExistException;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
Expand All @@ -14,12 +19,16 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import javax.transaction.Transactional;
import java.sql.Ref;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static com.graphy.backend.domain.member.dto.MemberDto.*;
import static com.graphy.backend.domain.project.dto.ProjectDto.ProjectInfo;
import static com.graphy.backend.domain.project.dto.ProjectDto.*;
import static com.graphy.backend.global.auth.jwt.dto.TokenDto.*;

@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@Service
Expand All @@ -28,34 +37,109 @@ public class MemberService {
private final MemberRepository memberRepository;
private final CustomUserDetailsService customUserDetailsService;
private final ProjectService projectService;
private final JwtFilter filter;
private final PasswordEncoder encoder;
private final RefreshTokenRepository refreshTokenRepository;
private final AuthenticationManagerBuilder authenticationManagerBuilder;
private final TokenProvider tokenProvider;

public void join(CreateMemberRequest request) {
/**
* TODO
* 이메일 중복 체크
*/
String encodedPassword = encoder.encode(request.getPassword());
Member member = request.toEntity(encodedPassword);
memberRepository.save(member);
if (checkEmailDuplicate(request.getEmail())) {
String encodedPassword = encoder.encode(request.getPassword());
Member member = request.toEntity(encodedPassword);
memberRepository.save(member);
} else {
throw new AlreadyExistException(ErrorCode.MEMBER_ALREADY_EXIST);
}
}

public TokenInfo login(LoginMemberRequest request) {
public TokenInfo login(HttpServletRequest request,
LoginMemberRequest dto) {

UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(request.getEmail(), request.getPassword());
new UsernamePasswordAuthenticationToken(dto.getEmail(), dto.getPassword());

Authentication authentication = authenticationManagerBuilder.getObject()
.authenticate(authenticationToken);

TokenInfo token = tokenProvider.generateToken(authentication);

RefreshToken refreshToken = RefreshToken.builder()
.token(token.getRefreshToken())
.email(dto.getEmail())
.authorities(authentication.getAuthorities())
.ip(Helper.getClientIp(request))
.build();

Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
TokenInfo token = tokenProvider.generateTokenDto(authentication);
refreshTokenRepository.save(refreshToken);

/**
* TODO
* Refresh Token 저장 설정
*/
return token;
}

public void logout(LogoutRequest request){
// 로그아웃 하고 싶은 토큰이 유효한 지 먼저 검증하기
if (!tokenProvider.validateToken(request.getAccessToken())){
throw new IllegalArgumentException("로그아웃 : 유효하지 않은 토큰입니다.");
}

// Access Token에서 User email을 가져온다
String email = tokenProvider.getAuthentication(request.getAccessToken()).getName();

// Redis에서 해당 User email로 저장된 Refresh Token 조회
RefreshToken refreshToken = refreshTokenRepository.findByEmail(email);

// Refresh Token이 존재하는 경우 해당 토큰 삭제
if (refreshToken!=null){
refreshTokenRepository.delete(refreshToken);
}

// Access Token 유효시간을 blackList에 저장
Long expiration = tokenProvider.getExpiration(request.getAccessToken()) / 1000;

RefreshToken token = RefreshToken.builder()
.token(request.getAccessToken())
.expiration(expiration)
.build();

refreshTokenRepository.save(token);
}

public TokenInfo reissue(HttpServletRequest request) {
//1. Request Header 에서 JWT Token 추출
String requestToken = filter.resolveToken(request);
Member member = getLoginMember();

//2. validateToken 메서드로 토큰 유효성 검사
if (requestToken != null && tokenProvider.validateToken(requestToken)) {

//3. 저장된 refresh token 찾기
RefreshToken savedRefreshToken = refreshTokenRepository.findByEmail(member.getEmail());
if (savedRefreshToken != null) {

//4. 최초 로그인한 ip 와 같은지 확인
String currentIpAddress = Helper.getClientIp(request);
if (savedRefreshToken.getIp().equals(currentIpAddress)) {

// 5. Redis 에 저장된 RefreshToken 정보를 기반으로 JWT Token 생성
TokenInfo newToken = tokenProvider.generateToken(savedRefreshToken.getToken(), savedRefreshToken.getAuthorities());

// 6. Redis RefreshToken update
RefreshToken newRefreshToken = RefreshToken.builder()
.token(newToken.getRefreshToken())
.email(member.getEmail())
.ip(savedRefreshToken.getIp())
.authorities(savedRefreshToken.getAuthorities())
.build();

refreshTokenRepository.save(newRefreshToken);
savedRefreshToken.updateToken(newRefreshToken.getToken());
return newToken;
}
}
}
return null;
}


public List<GetMemberResponse> findMember(String nickname) {
List<Member> memberList = memberRepository.findMemberByNicknameStartingWith(nickname);
Expand All @@ -64,8 +148,19 @@ public List<GetMemberResponse> findMember(String nickname) {
.collect(Collectors.toList());
}


public GetMyPage myPage(Member member) {
List<ProjectInfo> projectInfoList = projectService.getProjectInfoList(member.getId());
return GetMyPage.from(member, projectInfoList);
return GetMyPageResponse.from(member, projectInfoList);
}

private Member getLoginMember() {
Member member = customUserDetailsService.getLoginUser();
return member;
}

public boolean checkEmailDuplicate(String email) {
if (memberRepository.findByEmail(email).isEmpty()) return true;
return false;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package com.graphy.backend.global.auth.jwt;

import com.graphy.backend.global.auth.redis.domain.RefreshToken;
import com.graphy.backend.global.auth.redis.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

Expand All @@ -12,10 +16,13 @@
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Optional;

@RequiredArgsConstructor
@Component
public class JwtFilter extends GenericFilterBean {
private final TokenProvider tokenProvider;
private final RefreshTokenRepository refreshTokenRepository;

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Expand All @@ -24,15 +31,19 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha

// 2. validateToken 으로 토큰 유효성 검사
if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

// 3. 로그아웃 여부 확인
Optional<RefreshToken> isLogout = refreshTokenRepository.findById(token);
if (ObjectUtils.isEmpty(isLogout)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}

// Request Header 에서 토큰 정보 추출
private String resolveToken(HttpServletRequest request) {
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
return bearerToken.substring(7);
Expand Down
Loading

0 comments on commit 450faa1

Please sign in to comment.