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

6주차 산출물(Weekly -> Develop) #59

Merged
merged 7 commits into from
Oct 11, 2024
23 changes: 23 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Deploy

on:
push:
branches:
- Weekly

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Set up SSH
uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

- name: Run deployment script on server
run: |
ssh -o StrictHostKeyChecking=no ubuntu@${{ secrets.SERVER_IP }} 'bash /home/ubuntu/deploy.sh'
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,8 @@ out/

### dev file ###
src/main/resources/application-dev.properties

### macOS DS_Store files ###
.DS_Store
src/.DS_Store
src/main/.DS_Store
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
implementation group: 'com.twilio.sdk', name: 'twilio', version: '10.5.0'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.example.sinitto.auth.controller;

import com.example.sinitto.auth.exception.JWTExpirationException;
import com.example.sinitto.auth.exception.KakaoRefreshTokenExpirationException;
import com.example.sinitto.auth.exception.TokenNotFoundException;
import com.example.sinitto.auth.exception.UnauthorizedException;
import com.example.sinitto.auth.exception.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
Expand All @@ -15,11 +12,17 @@
@RestControllerAdvice(basePackages = "com.example.sinitto.auth")
public class AuthControllerAdvice {

private static final String UNAUTHORIZED_ACCESS_URI = "/errors/unauthorized-access";
private static final String JWT_EXPIRATION_URI = "/errors/unauthorized-access-by-jwt-expiration";
private static final String TOKEN_NOT_FOUND_URI = "/errors/token-not-found";
private static final String KAKAO_REFRESH_TOKEN_EXPIRATION_URI = "/errors/unauthorized-access-by-kakao-refresh-token-expiration";
private static final String KAKAO_EMAIL_NOT_FOUND_URI = "/errors/kakao-email-not-found";

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ProblemDetail> handleUnauthorizedException(UnauthorizedException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED,
ex.getMessage());
problemDetail.setType(URI.create("/errors/unauthorized-access"));
problemDetail.setType(URI.create(UNAUTHORIZED_ACCESS_URI));
problemDetail.setTitle("Unauthorized Access");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail);
}
Expand All @@ -28,7 +31,7 @@ public ResponseEntity<ProblemDetail> handleUnauthorizedException(UnauthorizedExc
public ResponseEntity<ProblemDetail> handleJWTExpirationException(JWTExpirationException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED,
ex.getMessage());
problemDetail.setType(URI.create("/errors/unauthorized-access-by-jwt-expiration"));
problemDetail.setType(URI.create(JWT_EXPIRATION_URI));
problemDetail.setTitle("Unauthorized Access By JWT Expiration");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail);
}
Expand All @@ -37,7 +40,7 @@ public ResponseEntity<ProblemDetail> handleJWTExpirationException(JWTExpirationE
public ResponseEntity<ProblemDetail> handleTokenNotFoundException(TokenNotFoundException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
ex.getMessage());
problemDetail.setType(URI.create("/errors/token-not-found"));
problemDetail.setType(URI.create(TOKEN_NOT_FOUND_URI));
problemDetail.setTitle("Token Not Found");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail);
}
Expand All @@ -46,8 +49,17 @@ public ResponseEntity<ProblemDetail> handleTokenNotFoundException(TokenNotFoundE
public ResponseEntity<ProblemDetail> handleKakaoRefreshTokenExpirationException(KakaoRefreshTokenExpirationException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED,
ex.getMessage());
problemDetail.setType(URI.create("/errors/unauthorized-access-by-kakao-refresh-token-expiration"));
problemDetail.setType(URI.create(KAKAO_REFRESH_TOKEN_EXPIRATION_URI));
problemDetail.setTitle("Unauthorized Access By Kakao Refresh Token Expiration");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(problemDetail);
}

@ExceptionHandler(KakaoEmailNotFoundException.class)
public ResponseEntity<ProblemDetail> handleKakaoEmailNotFoundException(KakaoEmailNotFoundException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND,
ex.getMessage());
problemDetail.setType(URI.create(KAKAO_EMAIL_NOT_FOUND_URI));
problemDetail.setTitle("Kakao Email Not Found");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
public record KakaoTokenResponse(
String accessToken,
String refreshToken,
Integer expiresIn,
Integer refreshTokenExpiresIn
int expiresIn,
int refreshTokenExpiresIn

) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public record LoginResponse(
String refreshToken,
String redirectUrl,
String email,
boolean isSinitto
boolean isSinitto,
boolean isMember
) {
}
20 changes: 10 additions & 10 deletions src/main/java/com/example/sinitto/auth/entity/KakaoToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,28 @@ public class KakaoToken {
@NotNull
private String refreshToken;
@NotNull
private int expires_in;
private int expiresIn;
@NotNull
private int refresh_token_expires_in;
private int refreshTokenExpiresIn;

public KakaoToken(String memberEmail, String accessToken, String refreshToken,
int expires_in, int refresh_token_expires_in) {
int expiresIn, int refreshTokenExpiresIn) {
this.memberEmail = memberEmail;
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expires_in = expires_in;
this.refresh_token_expires_in = refresh_token_expires_in;
this.expiresIn = expiresIn;
this.refreshTokenExpiresIn = refreshTokenExpiresIn;
}

protected KakaoToken() {
}

public boolean isAccessTokenExpired() {
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(expires_in));
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(expiresIn));
}

public boolean isRefreshTokenExpired() {
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(refresh_token_expires_in));
return LocalDateTime.now().isAfter(issuedAt.plusSeconds(refreshTokenExpiresIn));
}

public String getAccessToken() {
Expand All @@ -65,11 +65,11 @@ public String getMemberEmail() {
}

public void updateKakaoToken(String accessToken, String refreshToken,
int expires_in, int refresh_token_expires_in) {
int expiresIn, int refreshTokenExpiresIn) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
this.expires_in = expires_in;
this.refresh_token_expires_in = refresh_token_expires_in;
this.expiresIn = expiresIn;
this.refreshTokenExpiresIn = refreshTokenExpiresIn;
this.issuedAt = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.sinitto.auth.exception;

public class KakaoEmailNotFoundException extends RuntimeException {
public KakaoEmailNotFoundException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.example.sinitto.auth.dto.KakaoTokenResponse;
import com.example.sinitto.auth.dto.KakaoUserResponse;
import com.example.sinitto.auth.exception.KakaoEmailNotFoundException;
import com.example.sinitto.common.properties.KakaoProperties;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
Expand All @@ -14,6 +15,8 @@
@Service
public class KakaoApiService {

private static final String KAKAO_AUTH_BASE_URL = "https://kauth.kakao.com/oauth";
private static final String KAKAO_API_BASE_URL = "https://kapi.kakao.com/v2/user";
private final RestTemplate restTemplate;
private final KakaoProperties kakaoProperties;

Expand All @@ -23,14 +26,15 @@ public KakaoApiService(RestTemplate restTemplate, KakaoProperties kakaoPropertie
}

public String getAuthorizationUrl() {
return "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id="
return KAKAO_AUTH_BASE_URL + "/authorize?response_type=code&client_id="
+ kakaoProperties.clientId() + "&redirect_uri=" + kakaoProperties.redirectUri();
}

public KakaoTokenResponse getAccessToken(String authorizationCode) {
String url = "https://kauth.kakao.com/oauth/token";
String url = KAKAO_AUTH_BASE_URL + "/token";
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);

LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", kakaoProperties.clientId());
Expand All @@ -47,8 +51,7 @@ public KakaoTokenResponse getAccessToken(String authorizationCode) {
}

public KakaoTokenResponse refreshAccessToken(String refreshToken) {
String url = "https://kauth.kakao.com/oauth/token";

String url = KAKAO_AUTH_BASE_URL + "/token";
String body = "grant_type=refresh_token&client_id=" + kakaoProperties.clientId()
+ "&refresh_token=" + refreshToken;

Expand All @@ -64,7 +67,7 @@ public KakaoTokenResponse refreshAccessToken(String refreshToken) {
}

public KakaoUserResponse getUserInfo(String accessToken) {
String url = "https://kapi.kakao.com/v2/user/me";
String url = KAKAO_API_BASE_URL + "/me";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
headers.setBearerAuth(accessToken);
Expand All @@ -77,7 +80,10 @@ public KakaoUserResponse getUserInfo(String accessToken) {
ResponseEntity<KakaoUserResponse> response = restTemplate.exchange(
url, HttpMethod.POST, request, KakaoUserResponse.class);

if (response.getBody().kakaoAccount().email() == null) {
throw new KakaoEmailNotFoundException("카카오 계정으로부터 전달받은 이메일이 없습니다.");
}

return response.getBody();
}

}
66 changes: 32 additions & 34 deletions src/main/java/com/example/sinitto/auth/service/TokenService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@Service
public class TokenService {
Expand All @@ -20,10 +22,12 @@ public class TokenService {
private static final long REFRESH_SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7;

private final Key secretKey;
private final RedisTemplate<String, String> redisTemplate;

public TokenService(@Value("${jwt.secret}") String secretKey) {
public TokenService(@Value("${jwt.secret}") String secretKey, RedisTemplate<String, String> redisTemplate) {
byte[] decodedKey = Base64.getDecoder().decode(secretKey);
this.secretKey = new SecretKeySpec(decodedKey, 0, decodedKey.length, "HmacSHA256");
this.redisTemplate = redisTemplate;
}

public String generateAccessToken(String email) {
Expand All @@ -36,51 +40,45 @@ public String generateAccessToken(String email) {
}

public String generateRefreshToken(String email) {
return Jwts.builder()
String refreshToken = Jwts.builder()
.setSubject(email)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_SEVEN_DAYS))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();

redisTemplate.opsForValue().set(email, refreshToken, REFRESH_SEVEN_DAYS, TimeUnit.MILLISECONDS);
return refreshToken;
}


public String extractEmail(String token) {
try {
var claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();

if (claims.getExpiration().before(new Date())) {
throw new JWTExpirationException("엑세스 토큰이 만료되었습니다.");
}

return claims.getSubject();
} catch (Exception e) {
throw new UnauthorizedException("유효하지 않은 엑세스 토큰입니다.");
var claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();

if (claims.getExpiration().before(new Date())) {
throw new JWTExpirationException("토큰이 만료되었습니다. 재로그인이 필요합니다.");
}

return claims.getSubject();
}

public TokenResponse refreshAccessToken(String refreshToken) {
try {
var claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(refreshToken)
.getBody();

if (claims.getExpiration().before(new Date())) {
throw new JWTExpirationException("리프레쉬 토큰이 만료되었습니다.");
}

String newAccessToken = generateAccessToken(claims.getSubject());
String newRefreshToken = generateRefreshToken(claims.getSubject());

return new TokenResponse(newAccessToken, newRefreshToken);
} catch (Exception e) {
throw new UnauthorizedException("유효하지 않은 리프레쉬 토큰입니다.");
String email = extractEmail(refreshToken);

String storedRefreshToken = redisTemplate.opsForValue().get(email);
if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) {
throw new UnauthorizedException("만료되거나 이미 한번 사용된 리프레쉬 토큰입니다. 재로그인이 필요합니다.");
}
}

redisTemplate.delete(email);

String newAccessToken = generateAccessToken(email);
String newRefreshToken = generateRefreshToken(email);

return new TokenResponse(newAccessToken, newRefreshToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ public ResponseEntity<Page<CallbackResponse>> getCallbackList(@MemberId Long mem
return ResponseEntity.ok(callbackService.getCallbacks(memberId, pageable));
}

@Operation(summary = "콜백 전화 완료", description = "시니또와 시니어의 연락이 끝났을 때 시니어의 요청사항을 수행 여부를 선택하여 처리합니다.")
@Operation(summary = "진행 상태인 콜백을 완료 대기 상태로 전환(시니또가)", description = "시니또가 수락한 콜백 수행을 완료했을때 이 api 호출하면 완료 대기 상태로 변합니다.")
@PutMapping("/pendingComplete/{callbackId}")
public ResponseEntity<Void> pendingCompleteCallback(@MemberId Long memberId,
@PathVariable Long callbackId) {

callbackService.pendingComplete(memberId, callbackId);
return ResponseEntity.ok().build();
}

@Operation(summary = "완료 대기 상태인 콜백을 완료 상태로 전환(보호자가)", description = "보호자가 완료 대기 상태인 콜백을 완료 확정 시킵니다.")
@PutMapping("/complete/{callbackId}")
public ResponseEntity<Void> completeCallback(@MemberId Long memberId,
@PathVariable Long callbackId) {
Expand Down Expand Up @@ -63,4 +72,11 @@ public ResponseEntity<String> addCallCheck(@RequestParam("From") String fromNumb

return ResponseEntity.ok(callbackService.add(fromNumber));
}

@Operation(summary = "시니또에게 현재 할당된 콜백 조회", description = "현재 시니또 본인에게 할당된 콜백을 조회합니다.")
@GetMapping("/sinitto/accepted")
public ResponseEntity<CallbackResponse> getAcceptedCallback(@MemberId Long memberId) {

return ResponseEntity.ok(callbackService.getAcceptedCallback(memberId));
}
}
Loading
Loading