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

[REFACTOR] 리프레시 토큰 삭제 및 액세스 토큰 만료 시간 삭제 #54

Merged
merged 5 commits into from
Aug 10, 2024
Merged
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
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
Loading