Skip to content

Commit

Permalink
[MERGE] Merge pull request #179 from Team-WSS/feat/#177
Browse files Browse the repository at this point in the history
[FEAT] 카카오 로그인 구현 및 사용자 인증 방식 개선
  • Loading branch information
Kim-TaeUk authored Sep 21, 2024
2 parents c65f16c + fe1a758 commit c7b2e07
Show file tree
Hide file tree
Showing 18 changed files with 287 additions and 71 deletions.
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ dependencies {

//spring boot oauth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// Lettuce for Java Redis client
implementation 'io.lettuce:lettuce-core:6.3.2.RELEASE'

// spring data redis
implementation 'org.springframework.data:spring-data-redis:3.3.2'
}

tasks.named('test') {
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/org/websoso/WSSServer/WssServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisKeyValueAdapter;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
@SpringBootApplication
public class WssServerApplication {

public static void main(String[] args) {
SpringApplication.run(WssServerApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(WssServerApplication.class, args);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ public class SecurityConfig {
"/feeds/popular",
"/users/{userId}/feeds",
"/users/profile/{userId}",
"/{userId}/preferences/genres"
"/{userId}/preferences/genres",
"/reissue"
};

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ private void setResponse(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}

}
}
54 changes: 54 additions & 0 deletions src/main/java/org/websoso/WSSServer/config/jwt/JWTUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.websoso.WSSServer.config.jwt;

import static org.websoso.WSSServer.config.jwt.JwtProvider.CLAIM_USER_ID;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class JWTUtil {

private final JwtProvider jwtProvider;

public Long getUserIdFromJwt(String token) {
Claims claims = getClaim(token);
return Long.valueOf(claims.get(CLAIM_USER_ID).toString());
}

public JwtValidationType validateJWT(String token) {
try {
final Claims claims = getClaim(token);
String tokenType = claims.getSubject();
if (tokenType.equals("access")) {
return JwtValidationType.VALID_ACCESS;
}
return JwtValidationType.VALID_REFRESH;
} catch (MalformedJwtException ex) {
return JwtValidationType.INVALID_TOKEN;
} catch (ExpiredJwtException ex) {
String tokenType = ex.getClaims().getSubject();
if (tokenType.equals("access")) {
return JwtValidationType.EXPIRED_ACCESS;
}
return JwtValidationType.EXPIRED_REFRESH;
} catch (UnsupportedJwtException ex) {
return JwtValidationType.UNSUPPORTED_TOKEN;
} catch (IllegalArgumentException ex) {
return JwtValidationType.EMPTY_TOKEN;
}
}

private Claims getClaim(final String token) {
return Jwts.parserBuilder()
.setSigningKey(jwtProvider.getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.websoso.WSSServer.config.jwt;

import static org.websoso.WSSServer.config.jwt.JwtValidationType.VALID_TOKEN;
import static org.websoso.WSSServer.config.jwt.JwtValidationType.EXPIRED_ACCESS;
import static org.websoso.WSSServer.config.jwt.JwtValidationType.VALID_ACCESS;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand All @@ -23,20 +24,23 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final static String TOKEN_PREFIX = "Bearer ";
private final JwtProvider jwtProvider;
private final JWTUtil jwtUtil;

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
final String token = getJwtFromRequest(request);
if (jwtProvider.validateToken(token) == VALID_TOKEN) {
Long memberId = jwtProvider.getUserFromJwt(token);
// authentication 객체 생성 -> principal에 유저정보를 담는다.
UserAuthentication authentication = new UserAuthentication(memberId.toString(), null, null);
final JwtValidationType validationResult = jwtUtil.validateJWT(token);
if (validationResult == VALID_ACCESS) {
Long userId = jwtUtil.getUserIdFromJwt(token);
UserAuthentication authentication = new UserAuthentication(userId.toString(), null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else if (validationResult == EXPIRED_ACCESS) {
handleExpiredAccessToken(request, response);
return;
}
} catch (Exception exception) {
try {
Expand All @@ -45,7 +49,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request,
throw new RuntimeException(e);
}
}
// 다음 필터로 요청 전달
filterChain.doFilter(request, response);
}

Expand All @@ -56,4 +59,12 @@ private String getJwtFromRequest(HttpServletRequest request) {
}
return null;
}
}

private void handleExpiredAccessToken(HttpServletRequest request,
HttpServletResponse response) throws IOException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter()
.write("{\"code\": \"AUTH-000\", \"message\": \"Access Token Expired. Use Refresh Token to reissue.\"}");
}
}
80 changes: 31 additions & 49 deletions src/main/java/org/websoso/WSSServer/config/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package org.websoso.WSSServer.config.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
Expand All @@ -21,66 +19,50 @@
@RequiredArgsConstructor
public class JwtProvider {

private static final String USER_ID = "userId";
private static final Long TOKEN_EXPIRATION_TIME = 6 * 30 * 24 * 60 * 60 * 1000L; // 6개월
protected static final String CLAIM_USER_ID = "userId";

@Value("${jwt.secret}")
private String JWT_SECRET;
private String JWT_SECRET_KEY;

@Value("${jwt.expiration-time.access-token}")
private Long ACCESS_TOKEN_EXPIRATION_TIME;

@Value("${jwt.expiration-time.refresh-token}")
private Long REFRESH_TOKEN_EXPIRATION_TIME;

@PostConstruct
protected void init() {
//base64 라이브러리에서 encodeToString을 이용해서 byte[] 형식을 String 형식으로 변환
JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
JWT_SECRET_KEY = Base64.getEncoder().encodeToString(JWT_SECRET_KEY.getBytes(StandardCharsets.UTF_8));
}

public String generateToken(Authentication authentication) {
final Date now = new Date();

final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + TOKEN_EXPIRATION_TIME)); // 만료 시간
public String generateAccessToken(Authentication authentication) {
return generateJWT(authentication, ACCESS_TOKEN_EXPIRATION_TIME, "access");
}

claims.put(USER_ID, authentication.getPrincipal());
public String generateRefreshToken(Authentication authentication) {
return generateJWT(authentication, REFRESH_TOKEN_EXPIRATION_TIME, "refresh");
}

public String generateJWT(Authentication authentication, Long expirationTime, String tokenType) {
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header
.setClaims(claims) // Claim
.signWith(getSigningKey()) // Signature
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setClaims(generateClaims(authentication, expirationTime, tokenType))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}

private SecretKey getSigningKey() {
String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성
return Keys.hmacShaKeyFor(
encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용
}

public JwtValidationType validateToken(String token) {
try {
final Claims claims = getBody(token);
return JwtValidationType.VALID_TOKEN;
} catch (MalformedJwtException ex) {
return JwtValidationType.INVALID_TOKEN;
} catch (ExpiredJwtException ex) {
return JwtValidationType.EXPIRED_TOKEN;
} catch (UnsupportedJwtException ex) {
return JwtValidationType.UNSUPPORTED_TOKEN;
} catch (IllegalArgumentException ex) {
return JwtValidationType.EMPTY_TOKEN;
}
}
private Claims generateClaims(Authentication authentication, Long expirationTime, String tokenType) {
long now = System.currentTimeMillis();
final Claims claims = Jwts.claims()
.setSubject(tokenType)
.setIssuedAt(new Date(now))
.setExpiration(new Date(now + expirationTime));
claims.put(CLAIM_USER_ID, authentication.getPrincipal());

private Claims getBody(final String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims;
}

public Long getUserFromJwt(String token) {
Claims claims = getBody(token);
return Long.valueOf(claims.get(USER_ID).toString());
protected SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(JWT_SECRET_KEY.getBytes());
}

}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.websoso.WSSServer.config.jwt;

public enum JwtValidationType {
VALID_TOKEN, // 유효한 JWT
INVALID_SIGNATURE, // 유효하지 않은 서명
INVALID_TOKEN, // 유효하지 않은 토큰
EXPIRED_TOKEN, // 만료된 토큰
UNSUPPORTED_TOKEN, // 지원하지 않는 형식의 토큰
EMPTY_TOKEN // 빈 JWT
VALID_ACCESS,
VALID_REFRESH,
INVALID_TOKEN,
EXPIRED_ACCESS,
EXPIRED_REFRESH,
UNSUPPORTED_TOKEN,
EMPTY_TOKEN
}
27 changes: 27 additions & 0 deletions src/main/java/org/websoso/WSSServer/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.websoso.WSSServer.controller;

import static org.springframework.http.HttpStatus.OK;

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;
import org.websoso.WSSServer.dto.auth.ReissueRequest;
import org.websoso.WSSServer.dto.auth.ReissueResponse;
import org.websoso.WSSServer.service.AuthService;

@RestController
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/reissue")
public ResponseEntity<ReissueResponse> reissue(@RequestBody ReissueRequest reissueRequest) {
String refreshToken = reissueRequest.refreshToken();
return ResponseEntity
.status(OK)
.body(authService.reissue(refreshToken));
}
}
17 changes: 17 additions & 0 deletions src/main/java/org/websoso/WSSServer/domain/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.websoso.WSSServer.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;

@Getter
@AllArgsConstructor
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 7 * 2)
public class RefreshToken {

@Id
private String refreshToken;

private Long userId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.websoso.WSSServer.dto.auth;

public record ReissueRequest(
String refreshToken
) {
}
11 changes: 11 additions & 0 deletions src/main/java/org/websoso/WSSServer/dto/auth/ReissueResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.websoso.WSSServer.dto.auth;

public record ReissueResponse(
String Authorization,
String refreshToken
) {

public static ReissueResponse of(String accessToken, String refreshToken) {
return new ReissueResponse(accessToken, refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.websoso.WSSServer.exception.error;

import static org.springframework.http.HttpStatus.UNAUTHORIZED;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.websoso.WSSServer.exception.common.ICustomError;

@Getter
@AllArgsConstructor
public enum CustomAuthError implements ICustomError {

INVALID_TOKEN("AUTH-001", "유효하지 않은 토큰입니다.", UNAUTHORIZED);

private final String code;
private final String description;
private final HttpStatus statusCode;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.websoso.WSSServer.exception.exception;

import lombok.Getter;
import org.websoso.WSSServer.exception.common.AbstractCustomException;
import org.websoso.WSSServer.exception.error.CustomAuthError;

@Getter
public class CustomAuthException extends AbstractCustomException {

public CustomAuthException(CustomAuthError customAuthError, String message) {
super(customAuthError, message);
}
}
Loading

0 comments on commit c7b2e07

Please sign in to comment.