Skip to content

Commit

Permalink
Merge pull request #54 from Team-Haruchi/fix/1-member#1
Browse files Browse the repository at this point in the history
[REFACTOR] 리프레시 토큰 삭제 및 액세스 토큰 만료 시간 삭제
  • Loading branch information
hcg0127 authored Aug 10, 2024
2 parents c936e14 + d6e7a3e commit 35b1caf
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 112 deletions.
2 changes: 1 addition & 1 deletion src/main/java/umc/haruchi/config/login/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
.authorizeHttpRequests((request) -> request
.requestMatchers("/member/signup/**").permitAll()
.requestMatchers("/member/login").permitAll()
.requestMatchers("/member/refresh").permitAll()
// .requestMatchers("/member/refresh").permitAll() // 보안 강화 시 주석 처리 해제
.requestMatchers("/health").permitAll() // health check
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated())
Expand Down
44 changes: 29 additions & 15 deletions src/main/java/umc/haruchi/config/login/jwt/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public Authentication getAuthentication(String token) {
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}

public static String createRefreshJwt(Long memberId, String email, String role) {
public static String createNewAccessJwt(Long memberId, String email, String role) {
return Jwts.builder()
.header()
.add("typ", "JWT")
Expand All @@ -137,24 +137,38 @@ public static String createRefreshJwt(Long memberId, String email, String role)
.claim("email", email)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + refreshExpireMs))
.signWith(secretKey)
.compact();
}

public static String createAccessJwt(Long memberId, String email, String role) {
return Jwts.builder()
.header()
.add("typ", "JWT")
.and()
.claim("memberId", memberId)
.claim("email", email)
.claim("role", role)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + accessExpireMs))
.signWith(secretKey)
.compact();
}
// 기존 토큰 발급 기능 - 보안 강화 시 주석 처리 해제
// public static String createRefreshJwt(Long memberId, String email, String role) {
// return Jwts.builder()
// .header()
// .add("typ", "JWT")
// .and()
// .claim("memberId", memberId)
// .claim("email", email)
// .claim("role", role)
// .issuedAt(new Date(System.currentTimeMillis()))
// .expiration(new Date(System.currentTimeMillis() + refreshExpireMs))
// .signWith(secretKey)
// .compact();
// }
//
// public static String createAccessJwt(Long memberId, String email, String role) {
// return Jwts.builder()
// .header()
// .add("typ", "JWT")
// .and()
// .claim("memberId", memberId)
// .claim("email", email)
// .claim("role", role)
// .issuedAt(new Date(System.currentTimeMillis()))
// .expiration(new Date(System.currentTimeMillis() + accessExpireMs))
// .signWith(secretKey)
// .compact();
// }

public static String getPassword(String token) {
return Jwts.parser()
Expand Down
187 changes: 117 additions & 70 deletions src/main/java/umc/haruchi/service/MemberService.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package umc.haruchi.service;

import io.jsonwebtoken.JwtException;
import jakarta.mail.Message;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.With;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.MailException;
import org.springframework.mail.javamail.JavaMailSender;
Expand Down Expand Up @@ -141,102 +143,147 @@ public void verificationEmail(String code, String savedCode) throws Exception {
}
}

// 로그인 (access token 발급)
public MemberResponseDTO.LoginJwtTokenDTO login(MemberRequestDTO.MemberLoginDTO loginDto) {
String email = loginDto.getEmail();
// 로그인 (access token 발급; 만료 시간 없음)
public MemberResponseDTO.NewLoginJwtTokenDTO newLogin(MemberRequestDTO.MemberLoginDTO loginDTO) {
String email = loginDTO.getEmail();

Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new MemberHandler(ErrorStatus.NO_MEMBER_EXIST));

if (!passwordEncoder.matches(loginDto.getPassword(), member.getPassword())) {
if (!passwordEncoder.matches(loginDTO.getPassword(), member.getPassword())) {
throw new MemberHandler(ErrorStatus.PASSWORD_NOT_MATCH);
}

// 30일 이상 미접속 시 로그아웃 되도록 토큰 유효시간을 수정
String accessToken = JwtUtil.createAccessJwt(member.getId(), member.getEmail(), null);
String refreshToken = JwtUtil.createRefreshJwt(member.getId(), member.getEmail(), null);
String accessToken = JwtUtil.createNewAccessJwt(member.getId(), member.getEmail(), null);

Long accessExpiredAt = JwtUtil.getExpiration(accessToken);
Long refreshExpiredAt = JwtUtil.getExpiration(refreshToken);

redisTemplate.opsForValue().set("RT" + email, refreshToken, refreshExpiredAt, TimeUnit.MILLISECONDS);

return MemberResponseDTO.LoginJwtTokenDTO.builder()
return MemberResponseDTO.NewLoginJwtTokenDTO.builder()
.grantType("Bearer")
.refreshToken(refreshToken)
.accessToken(accessToken)
.accessTokenExpiresAt(accessExpiredAt)
.refreshTokenExpirationAt(refreshExpiredAt)
.build();
}

// 토큰 재발급
public MemberResponseDTO.LoginJwtTokenDTO reissue(String refreshToken) {

if (!jwtUtil.validateToken(refreshToken)) {
throw new JwtExceptionHandler(ErrorStatus.NOT_VALID_TOKEN.getMessage());
}

String email = jwtUtil.getEmail(refreshToken);

Object o = redisTemplate.opsForValue().get("RT" + email);
if (o == null) {
throw new JwtExceptionHandler(ErrorStatus.NO_MATCH_REFRESHTOKEN.getMessage());
}

Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new MemberHandler(ErrorStatus.NO_MEMBER_EXIST));

String newAccessToken = JwtUtil.createAccessJwt(member.getId(), member.getEmail(), null);
String newRefreshToken = JwtUtil.createRefreshJwt(member.getId(), member.getEmail(), null);

Long accessExpiredAt = JwtUtil.getExpiration(newAccessToken);
Long refreshExpiredAt = JwtUtil.getExpiration(newRefreshToken);

redisTemplate.opsForValue().set("RT" + member.getEmail(), newRefreshToken, refreshExpiredAt, TimeUnit.MILLISECONDS);

return MemberResponseDTO.LoginJwtTokenDTO.builder()
.grantType("Bearer")
.accessToken(newAccessToken)
.accessTokenExpiresAt(accessExpiredAt)
.refreshToken(newRefreshToken)
.refreshTokenExpirationAt(refreshExpiredAt)
.build();
}

// 로그아웃 (액세스 토큰 블랙리스트에 저장)
public void logout(String accessToken, String refreshToken, String type) {

// 기존 로그인 (access token과 refresh token 발급; 각각 만료 시간 존재)
// public MemberResponseDTO.LoginJwtTokenDTO login(MemberRequestDTO.MemberLoginDTO loginDto) {
// String email = loginDto.getEmail();
//
// Member member = memberRepository.findByEmail(email)
// .orElseThrow(() -> new MemberHandler(ErrorStatus.NO_MEMBER_EXIST));
//
// if (!passwordEncoder.matches(loginDto.getPassword(), member.getPassword())) {
// throw new MemberHandler(ErrorStatus.PASSWORD_NOT_MATCH);
// }
//
// // 30일 이상 미접속 시 로그아웃 되도록 토큰 유효시간을 수정
// String accessToken = JwtUtil.createAccessJwt(member.getId(), member.getEmail(), null);
// String refreshToken = JwtUtil.createRefreshJwt(member.getId(), member.getEmail(), null);
//
// Long accessExpiredAt = JwtUtil.getExpiration(accessToken);
// Long refreshExpiredAt = JwtUtil.getExpiration(refreshToken);
//
// redisTemplate.opsForValue().set("RT" + email, refreshToken, refreshExpiredAt, TimeUnit.MILLISECONDS);
//
// return MemberResponseDTO.LoginJwtTokenDTO.builder()
// .grantType("Bearer")
// .refreshToken(refreshToken)
// .accessToken(accessToken)
// .accessTokenExpiresAt(accessExpiredAt)
// .refreshTokenExpirationAt(refreshExpiredAt)
// .build();
// }

// 토큰 재발급 (보안 강화 시 주석 처리 해제)
// public MemberResponseDTO.LoginJwtTokenDTO reissue(String refreshToken) {
//
// if (!jwtUtil.validateToken(refreshToken)) {
// throw new JwtExceptionHandler(ErrorStatus.NOT_VALID_TOKEN.getMessage());
// }
//
// String email = jwtUtil.getEmail(refreshToken);
//
// Object o = redisTemplate.opsForValue().get("RT" + email);
// if (o == null) {
// throw new JwtExceptionHandler(ErrorStatus.NO_MATCH_REFRESHTOKEN.getMessage());
// }
//
// Member member = memberRepository.findByEmail(email)
// .orElseThrow(() -> new MemberHandler(ErrorStatus.NO_MEMBER_EXIST));
//
// String newAccessToken = JwtUtil.createAccessJwt(member.getId(), member.getEmail(), null);
// String newRefreshToken = JwtUtil.createRefreshJwt(member.getId(), member.getEmail(), null);
//
// Long accessExpiredAt = JwtUtil.getExpiration(newAccessToken);
// Long refreshExpiredAt = JwtUtil.getExpiration(newRefreshToken);
//
// redisTemplate.opsForValue().set("RT" + member.getEmail(), newRefreshToken, refreshExpiredAt, TimeUnit.MILLISECONDS);
//
// return MemberResponseDTO.LoginJwtTokenDTO.builder()
// .grantType("Bearer")
// .accessToken(newAccessToken)
// .accessTokenExpiresAt(accessExpiredAt)
// .refreshToken(newRefreshToken)
// .refreshTokenExpirationAt(refreshExpiredAt)
// .build();
// }

// 새 로그아웃; 토큰 블랙리스트화(만료 시간 X) + refresh token 삭제 X
public void newLogout(String accessToken) {
try {
jwtUtil.validateToken(accessToken);
} catch (JwtExceptionHandler e) {
} catch (JwtException e) {
throw new JwtExceptionHandler(ErrorStatus.NOT_VALID_TOKEN.getMessage());
}

String email = jwtUtil.getEmail(accessToken);

if (redisTemplate.opsForValue().get("RT" + email) != null) {
redisTemplate.delete("RT" + email);
}

Long expiration = JwtUtil.getExpiration(accessToken);
redisTemplate.opsForValue().set(accessToken, "logout", expiration, TimeUnit.MILLISECONDS);

if (type.equals("DELETE")) {
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new MemberHandler(ErrorStatus.NO_MEMBER_EXIST));
memberRepository.delete(member);
}
redisTemplate.opsForValue().set(accessToken, "logout");
}

// 회원 즉시 탈퇴 - 이유 저장
public void withdrawer(String reason) {
// 기존 로그아웃 - 액세스 토큰 블랙리스트에 저장 (보안 강화 시 주석 처리 해제)
// public void logout(String accessToken, String refreshToken, String type) {
//
// try {
// jwtUtil.validateToken(accessToken);
// } catch (JwtExceptionHandler e) {
// throw new JwtExceptionHandler(ErrorStatus.NOT_VALID_TOKEN.getMessage());
// }
//
// String email = jwtUtil.getEmail(accessToken);
//
// if (redisTemplate.opsForValue().get("RT" + email) != null) {
// redisTemplate.delete("RT" + email);
// }
//
// Long expiration = JwtUtil.getExpiration(accessToken);
// redisTemplate.opsForValue().set(accessToken, "logout", expiration, TimeUnit.MILLISECONDS);
//
// if (type.equals("DELETE")) {
// Member member = memberRepository.findByEmail(email)
// .orElseThrow(() -> new MemberHandler(ErrorStatus.NO_MEMBER_EXIST));
// memberRepository.delete(member);
// }
// }

// 새 회원 탈퇴 - 이유 저장 + 회원 정보 영구 삭제
public void newWithdrawer(String reason, String accessToken) {
Withdrawer withdrawer = Withdrawer.builder()
.reason(reason)
.build();
withdrawerRepository.save(withdrawer);

String email = jwtUtil.getEmail(accessToken);

Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new MemberHandler(ErrorStatus.NO_MEMBER_EXIST));

memberRepository.delete(member);
}

// 기존 회원 탈퇴 - 이유 저장 (보안 강화 시 주석 처리 해제)
// public void withdrawer(String reason) {
// Withdrawer withdrawer = Withdrawer.builder()
// .reason(reason)
// .build();
// withdrawerRepository.save(withdrawer);
// }


// 회원 더보기 정보(가입일, 가입 이메일, 닉네임) 조회
public MemberResponseDTO.MemberDetailResultDTO getMemberDetail(String email) {
Expand Down
71 changes: 49 additions & 22 deletions src/main/java/umc/haruchi/web/controller/MemberApiController.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,18 @@ public ApiResponse<MemberResponseDTO> verifyEmail(@Email(message = "이메일
return ApiResponse.onSuccess(null);
}

// 보안 강화 시 주석 처리 해제
// @PostMapping("/login")
// @Operation(summary = "로그인 API", description = "로그인을 진행하는 API (토큰 발급) (액세스 토큰 필요 없음)")
// public ApiResponse<MemberResponseDTO.LoginJwtTokenDTO> login(@Valid @RequestBody MemberRequestDTO.MemberLoginDTO request) {
// MemberResponseDTO.LoginJwtTokenDTO token = memberService.login(request);
// return ApiResponse.onSuccess(token);
// }

@PostMapping("/login")
@Operation(summary = "로그인 API", description = "로그인을 진행하는 API (토큰 발급) (액세스 토큰 필요 없음)")
public ApiResponse<MemberResponseDTO.LoginJwtTokenDTO> login(@Valid @RequestBody MemberRequestDTO.MemberLoginDTO request) {
MemberResponseDTO.LoginJwtTokenDTO token = memberService.login(request);
@Operation(summary = "로그인 API", description = "로그인을 진행하는 API (토큰 방급; 액세스 토큰 필요 없음)")
public ApiResponse<MemberResponseDTO.NewLoginJwtTokenDTO> login(@Valid @RequestBody MemberRequestDTO.MemberLoginDTO request) {
MemberResponseDTO.NewLoginJwtTokenDTO token = memberService.newLogin(request);
return ApiResponse.onSuccess(token);
}

Expand All @@ -84,36 +92,55 @@ public String loginTest() {
return "login user";
}

@PostMapping("/refresh") // 오류 발생 -> 헤더 인식 불가능
@Operation(summary = "액세스 토큰과 리프레시 토큰 재발급 API", description = "리프레시 토큰으로 액세스 토큰과 리프레시 토큰을 재발급하는 API (액세스 토큰 필요 없음)")
@Parameters({
@Parameter(name = "refreshToken", description = "리프레시 토큰")
})
public ApiResponse<MemberResponseDTO.LoginJwtTokenDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
MemberResponseDTO.LoginJwtTokenDTO tokens = memberService.reissue(refreshToken);
return ApiResponse.onSuccess(tokens);
}
// 보안 강화 시 주석 처리 해제
// @PostMapping("/refresh")
// @Operation(summary = "액세스 토큰과 리프레시 토큰 재발급 API", description = "리프레시 토큰으로 액세스 토큰과 리프레시 토큰을 재발급하는 API (액세스 토큰 필요 없음)")
// @Parameters({
// @Parameter(name = "refreshToken", description = "리프레시 토큰")
// })
// public ApiResponse<MemberResponseDTO.LoginJwtTokenDTO> refreshToken(@RequestParam("refreshToken") String refreshToken) {
// MemberResponseDTO.LoginJwtTokenDTO tokens = memberService.reissue(refreshToken);
// return ApiResponse.onSuccess(tokens);
// }

@PostMapping("/logout")
@Operation(summary = "로그아웃 API", description = "로그아웃을 진행하는 API (토큰 만료 및 블랙리스트화)")
@Parameters({
@Parameter(name = "accessToken", description = "액세스 토큰"),
@Parameter(name = "refreshToken", description = "리프레시 토큰")
})
public ApiResponse<MemberResponseDTO> logout(@RequestParam("accessToken") String accessToken,
@RequestParam("refreshToken") String refreshToken) {
memberService.logout(accessToken, refreshToken, "LOGOUT");
public ApiResponse<MemberResponseDTO> logout(@RequestHeader("Authorization") String accessToken) {
memberService.newLogout(accessToken.substring(7));
return ApiResponse.onSuccess(null);
}

// 보안 강화 시 주석 처리 해제
// @PostMapping("/logout")
// @Operation(summary = "로그아웃 API", description = "로그아웃을 진행하는 API (토큰 만료 및 블랙리스트화)")
// @Parameters({
// @Parameter(name = "accessToken", description = "액세스 토큰"),
// @Parameter(name = "refreshToken", description = "리프레시 토큰")
// })
// public ApiResponse<MemberResponseDTO> logout(@RequestParam("accessToken") String accessToken,
// @RequestParam("refreshToken") String refreshToken) {
// memberService.logout(accessToken, refreshToken, "LOGOUT");
// return ApiResponse.onSuccess(null);
// }

@PostMapping("/delete")
@Operation(summary = "회원탈퇴 API", description = "회원탈퇴를 진행하는 API (토큰 만료 및 회원 영구 삭제)")
public ApiResponse<MemberResponseDTO> deleteMember(@Valid @RequestBody MemberRequestDTO.MemberWithdrawRequestDTO request) {
memberService.logout(request.getAccessToken(), request.getRefreshToken(), "DELETE");
memberService.withdrawer(request.getReason());
public ApiResponse<MemberResponseDTO> deleteMember(@RequestHeader("Authorization") String accessToken,
@RequestParam String reason) {
memberService.newWithdrawer(reason,accessToken.substring(7));
memberService.newLogout(accessToken.substring(7));
return ApiResponse.onSuccess(null);
}

// 보안 강화 시 주석 처리 해제
// @PostMapping("/delete")
// @Operation(summary = "회원탈퇴 API", description = "회원탈퇴를 진행하는 API (토큰 만료 및 회원 영구 삭제)")
// public ApiResponse<MemberResponseDTO> deleteMember(@Valid @RequestBody MemberRequestDTO.MemberWithdrawRequestDTO request) {
// memberService.logout(request.getAccessToken(), request.getRefreshToken(), "DELETE");
// memberService.withdrawer(request.getReason());
// return ApiResponse.onSuccess(null);
// }

@GetMapping("/")
@Operation(summary = "회원정보조회 API", description = "헤더에 있는 토큰으로 회원을 식별하고, 더보기 화면에서 회원의 정보를 조회하는 API")
public ApiResponse<MemberResponseDTO.MemberDetailResultDTO> getMemberDetail(@AuthenticationPrincipal MemberDetail memberDetail) {
Expand Down
Loading

0 comments on commit 35b1caf

Please sign in to comment.