diff --git a/build.gradle b/build.gradle index d764002..abcfb1a 100644 --- a/build.gradle +++ b/build.gradle @@ -42,9 +42,9 @@ dependencies { // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.12.3' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' // AWS implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4' diff --git a/src/main/java/com/fledge/fledgeserver/auth/controller/AuthController.java b/src/main/java/com/fledge/fledgeserver/auth/controller/AuthController.java new file mode 100644 index 0000000..aa228ee --- /dev/null +++ b/src/main/java/com/fledge/fledgeserver/auth/controller/AuthController.java @@ -0,0 +1,42 @@ +package com.fledge.fledgeserver.auth.controller; + +import com.fledge.fledgeserver.auth.dto.TokenResponse; +import com.fledge.fledgeserver.auth.service.AuthService; +import com.fledge.fledgeserver.response.ApiResponse; +import com.fledge.fledgeserver.response.SuccessStatus; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "인증 관련 API", description = "인증과 관련된 API") +@Slf4j +@RestController +@RequestMapping("/api/v1/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @Operation(summary = "로그아웃", description = "현재 사용자를 로그아웃 합니다.") + @PostMapping("/logout") + public ResponseEntity> logout(HttpServletRequest request, HttpServletResponse response) { + authService.logout(request, response); + return ApiResponse.success(SuccessStatus.LOGOUT_SUCCESS); + } + + @Operation(summary = "토큰 재발급", description = "만료된 JWT 토큰을 재발급 합니다.") + @GetMapping(value = "/tokenRefresh", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> refresh() { + TokenResponse tokenResponse = authService.refreshToken(); + return ApiResponse.success(SuccessStatus.TOKEN_REFRESH_SUCCESS, tokenResponse); + } +} diff --git a/src/main/java/com/fledge/fledgeserver/auth/dto/TokenResponse.java b/src/main/java/com/fledge/fledgeserver/auth/dto/TokenResponse.java new file mode 100644 index 0000000..e8f7c56 --- /dev/null +++ b/src/main/java/com/fledge/fledgeserver/auth/dto/TokenResponse.java @@ -0,0 +1,18 @@ +package com.fledge.fledgeserver.auth.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Data +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +public class TokenResponse { + + @Schema(description = "액세스 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...") + private String accessToken; + + @Schema(description = "리프레시 토큰", example = "dGhpc2lzYXJlZnJlc2h0b2tlbg==") + private String refreshToken; + +} diff --git a/src/main/java/com/fledge/fledgeserver/auth/filter/TokenAuthenticationFilter.java b/src/main/java/com/fledge/fledgeserver/auth/filter/TokenAuthenticationFilter.java deleted file mode 100644 index 04e1e9e..0000000 --- a/src/main/java/com/fledge/fledgeserver/auth/filter/TokenAuthenticationFilter.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.fledge.fledgeserver.auth.filter; - -import static org.springframework.http.HttpHeaders.AUTHORIZATION; - -import com.fledge.fledgeserver.auth.jwt.TokenProvider; -import com.fledge.fledgeserver.common.constants.TokenKey; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -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.OncePerRequestFilter; - -@RequiredArgsConstructor -@Component -public class TokenAuthenticationFilter extends OncePerRequestFilter { - - private final TokenProvider tokenProvider; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - - String accessToken = resolveToken(request); - - if (tokenProvider.validateToken(accessToken)) { - setAuthentication(accessToken); - } else { - - String reissueAccessToken = tokenProvider.reissueAccessToken(accessToken); - - if (StringUtils.hasText(reissueAccessToken)) { - setAuthentication(reissueAccessToken); - response.setHeader(AUTHORIZATION, TokenKey.TOKEN_PREFIX + reissueAccessToken); - } - } - - filterChain.doFilter(request, response); - } - - private void setAuthentication(String accessToken) { - Authentication authentication = tokenProvider.getAuthentication(accessToken); - SecurityContextHolder.getContext().setAuthentication(authentication); - } - - private String resolveToken(HttpServletRequest request) { - String token = request.getHeader(AUTHORIZATION); - if (ObjectUtils.isEmpty(token) || !token.startsWith(TokenKey.TOKEN_PREFIX)) { - return null; - } - return token.substring(TokenKey.TOKEN_PREFIX.length()); - } -} \ No newline at end of file diff --git a/src/main/java/com/fledge/fledgeserver/auth/handler/OAuth2SuccessHandler.java b/src/main/java/com/fledge/fledgeserver/auth/handler/OAuth2SuccessHandler.java index 6991967..3d0a9a6 100644 --- a/src/main/java/com/fledge/fledgeserver/auth/handler/OAuth2SuccessHandler.java +++ b/src/main/java/com/fledge/fledgeserver/auth/handler/OAuth2SuccessHandler.java @@ -1,5 +1,6 @@ package com.fledge.fledgeserver.auth.handler; +import com.fledge.fledgeserver.auth.dto.TokenResponse; import com.fledge.fledgeserver.auth.jwt.TokenProvider; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -21,11 +22,11 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { - String accessToken = tokenProvider.generateAccessToken(authentication); - tokenProvider.generateRefreshToken(authentication, accessToken); + TokenResponse tokenResponse = tokenProvider.createToken(authentication); String redirectUrl = UriComponentsBuilder.fromUriString(oauthRedirectUrl) - .queryParam("accessToken", accessToken) + .queryParam("accessToken", tokenResponse.getAccessToken()) + .queryParam("refreshToken", tokenResponse.getRefreshToken()) .build().toUriString(); response.sendRedirect(redirectUrl); diff --git a/src/main/java/com/fledge/fledgeserver/auth/jwt/JwtConstants.java b/src/main/java/com/fledge/fledgeserver/auth/jwt/JwtConstants.java new file mode 100644 index 0000000..ba470d4 --- /dev/null +++ b/src/main/java/com/fledge/fledgeserver/auth/jwt/JwtConstants.java @@ -0,0 +1,6 @@ +package com.fledge.fledgeserver.auth.jwt; + +public class JwtConstants { + public static final String BEARER_PREFIX = "Bearer "; + public static final String AUTHORITIES = "role"; +} diff --git a/src/main/java/com/fledge/fledgeserver/auth/jwt/Token.java b/src/main/java/com/fledge/fledgeserver/auth/jwt/Token.java deleted file mode 100644 index af4ee2d..0000000 --- a/src/main/java/com/fledge/fledgeserver/auth/jwt/Token.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.fledge.fledgeserver.auth.jwt; - - -import jakarta.persistence.Id; -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.data.redis.core.RedisHash; -import org.springframework.data.redis.core.index.Indexed; - -@Getter -@AllArgsConstructor -@RedisHash(value = "jwt", timeToLive = 60 * 60 * 24 * 7) -public class Token { - - @Id - private String id; - - private String refreshToken; - - @Indexed - private String accessToken; - - public Token updateRefreshToken(String refreshToken) { - this.refreshToken = refreshToken; - return this; - } - - public void updateAccessToken(String accessToken) { - this.accessToken = accessToken; - } -} diff --git a/src/main/java/com/fledge/fledgeserver/auth/jwt/TokenProvider.java b/src/main/java/com/fledge/fledgeserver/auth/jwt/TokenProvider.java index 4cd942a..301bf50 100644 --- a/src/main/java/com/fledge/fledgeserver/auth/jwt/TokenProvider.java +++ b/src/main/java/com/fledge/fledgeserver/auth/jwt/TokenProvider.java @@ -1,132 +1,148 @@ package com.fledge.fledgeserver.auth.jwt; +import com.fledge.fledgeserver.auth.dto.TokenResponse; import com.fledge.fledgeserver.auth.service.CustomUserDetailsService; -import com.fledge.fledgeserver.auth.service.TokenService; -import com.fledge.fledgeserver.exception.TokenException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; +import com.fledge.fledgeserver.exception.CustomException; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.SecurityException; -import jakarta.annotation.PostConstruct; -import java.util.Collections; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; import java.util.Date; -import java.util.List; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.crypto.SecretKey; -import lombok.RequiredArgsConstructor; + +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; -import static com.fledge.fledgeserver.exception.ErrorCode.INVALID_JWT_SIGNATURE; +import static com.fledge.fledgeserver.auth.jwt.JwtConstants.*; import static com.fledge.fledgeserver.exception.ErrorCode.INVALID_TOKEN; -@RequiredArgsConstructor @Component +@Slf4j public class TokenProvider { - @Value("${jwt.key}") - private String key; - private SecretKey secretKey; - - @Value("${jwt.access_expired-time}") - private long ACCESS_TOKEN_EXPIRE_TIME; - @Value("${jwt.refresh_expired-time}") - private long REFRESH_TOKEN_EXPIRE_TIME; - - private static final String KEY_ROLE = "role"; - private final TokenService tokenService; private final CustomUserDetailsService userDetailsService; + private final RedisTemplate redisTemplate; + private final long accessExpired; + private final long refreshExpired; + private SecretKey key; + + private String secret; + + public TokenProvider( + CustomUserDetailsService userDetailsService, @Value("${jwt.key}") String secret, + RedisTemplate redisTemplate, + @Value("${jwt.access_expired-time}") long accessExpired, + @Value("${jwt.refresh_expired-time}") long refreshExpired) { + this.userDetailsService = userDetailsService; + this.secret = secret; + this.redisTemplate = redisTemplate; + this.accessExpired = accessExpired; + this.refreshExpired = refreshExpired; + } @PostConstruct - private void setSecretKey() { - secretKey = Keys.hmacShaKeyFor(key.getBytes()); + public void afterPropertiesSet() { + byte[] decoded = Decoders.BASE64.decode(secret); + this.key = Keys.hmacShaKeyFor(decoded); } - public String generateAccessToken(Authentication authentication) { - return generateToken(authentication, ACCESS_TOKEN_EXPIRE_TIME); + public String resolveToken(HttpServletRequest request) { + String token = request.getHeader("Authorization"); + if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) { + return token.substring(BEARER_PREFIX.length()); + } + return null; } - public void generateRefreshToken(Authentication authentication, String accessToken) { - String refreshToken = generateToken(authentication, REFRESH_TOKEN_EXPIRE_TIME); - tokenService.saveOrUpdate(authentication.getName(), refreshToken, accessToken); - } + public boolean validateToken(String token) { + if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) { + token = token.substring(BEARER_PREFIX.length()); + } - private String generateToken(Authentication authentication, long expireTime) { - Date now = new Date(); - Date expiredDate = new Date(now.getTime() + expireTime); + JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(key).build(); + try { + jwtParser.parseClaimsJws(token); + return true; + } catch (SecurityException | MalformedJwtException e) { + throw new CustomException(INVALID_TOKEN, "Invalid token " + e); + } catch (ExpiredJwtException e) { + throw new CustomException(INVALID_TOKEN, "Expired token " + e); + } catch (UnsupportedJwtException e) { + throw new CustomException(INVALID_TOKEN, "Token not supported " + e); + } catch (IllegalArgumentException e) { + throw new CustomException(INVALID_TOKEN, "Invalid token " + e); + } + } + public TokenResponse createToken(Authentication authentication) { String authorities = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) - .collect(Collectors.joining()); - - return Jwts.builder() - .subject(authentication.getName()) - .claim(KEY_ROLE, authorities) - .issuedAt(now) - .expiration(expiredDate) - .signWith(secretKey, Jwts.SIG.HS512) - .compact(); - } + .collect(Collectors.joining(",")); - public Authentication getAuthentication(String token) { - Claims claims = parseClaims(token); - List authorities = getAuthorities(claims); + Instant issuedAt = Instant.now().truncatedTo(ChronoUnit.SECONDS); - UserDetails userDetails = userDetailsService.loadUserByUsername( - claims.getSubject()); - return new UsernamePasswordAuthenticationToken(userDetails, token, authorities); - } + Date accessExpiration = Date.from(issuedAt.plus(accessExpired, ChronoUnit.SECONDS)); + Date refreshExpiration = Date.from(issuedAt.plus(refreshExpired, ChronoUnit.SECONDS)); - private List getAuthorities(Claims claims) { - return Collections.singletonList(new SimpleGrantedAuthority( - claims.get(KEY_ROLE).toString())); - } + var accessToken = Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer("Fledge") + .setIssuedAt(new Date()) + .setExpiration(accessExpiration) + .setSubject(authentication.getName()) + .signWith(key, SignatureAlgorithm.HS512) + .claim(AUTHORITIES, authorities) + .compact(); - public String reissueAccessToken(String accessToken) { - if (StringUtils.hasText(accessToken)) { - Token token = tokenService.findByAccessTokenOrThrow(accessToken); - String refreshToken = token.getRefreshToken(); + var refreshToken = Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer("Fledge") + .setIssuedAt(new Date()) + .setExpiration(refreshExpiration) + .setSubject(authentication.getName()) + .signWith(key, SignatureAlgorithm.HS512) + .claim(AUTHORITIES, authorities) + .compact(); - if (validateToken(refreshToken)) { - String reissueAccessToken = generateAccessToken(getAuthentication(refreshToken)); - tokenService.updateToken(reissueAccessToken, token); + updateUserAndStoreRefreshToken(authentication.getName(), refreshToken); - return reissueAccessToken; - } - } + return new TokenResponse(accessToken, refreshToken); + } - return null; + private void updateUserAndStoreRefreshToken(String username, String refreshToken) { + redisTemplate.opsForValue().set(username, refreshToken, refreshExpired, TimeUnit.SECONDS); } - public boolean validateToken(String token) { - if (!StringUtils.hasText(token)) { - return false; - } + public Authentication resolveToken(String token) { - Claims claims = parseClaims(token); - return claims.getExpiration().after(new Date()); - } + JwtParser jwtParser = Jwts.parserBuilder().setSigningKey(key).build(); + Claims claims = jwtParser.parseClaimsJws(token).getBody(); - private Claims parseClaims(String token) { - try { - return Jwts.parser().verifyWith(secretKey).build() - .parseSignedClaims(token).getPayload(); - } catch (ExpiredJwtException e) { - return e.getClaims(); - } catch (MalformedJwtException e) { - throw new TokenException(INVALID_TOKEN); - } catch (SecurityException e) { - throw new TokenException(INVALID_JWT_SIGNATURE); - } + Collection authorities = Stream.of( + String.valueOf(claims.get(AUTHORITIES)).split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + UserDetails userDetails = userDetailsService.loadUserByUsername( + claims.getSubject()); + return new UsernamePasswordAuthenticationToken(userDetails, token, authorities); } -} \ No newline at end of file + +} diff --git a/src/main/java/com/fledge/fledgeserver/auth/jwt/TokenRepository.java b/src/main/java/com/fledge/fledgeserver/auth/jwt/TokenRepository.java deleted file mode 100644 index 46a33cf..0000000 --- a/src/main/java/com/fledge/fledgeserver/auth/jwt/TokenRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.fledge.fledgeserver.auth.jwt; - -import java.util.Optional; - -import org.springframework.data.repository.CrudRepository; -import org.springframework.stereotype.Repository; - -@Repository -public interface TokenRepository extends CrudRepository { - - Optional findByAccessToken(String accessToken); -} \ No newline at end of file diff --git a/src/main/java/com/fledge/fledgeserver/auth/jwt/filter/JwtFilter.java b/src/main/java/com/fledge/fledgeserver/auth/jwt/filter/JwtFilter.java new file mode 100644 index 0000000..05fa61d --- /dev/null +++ b/src/main/java/com/fledge/fledgeserver/auth/jwt/filter/JwtFilter.java @@ -0,0 +1,40 @@ +package com.fledge.fledgeserver.auth.jwt.filter; + +import com.fledge.fledgeserver.auth.jwt.TokenProvider; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtFilter extends OncePerRequestFilter { + private final TokenProvider tokenProvider; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + + String jwt = tokenProvider.resolveToken(request); + + if (jwt != null) { + tokenProvider.validateToken(jwt); + setAuthentication(jwt); + } + + chain.doFilter(request, response); + } + + private void setAuthentication(String accessToken) { + Authentication authentication = tokenProvider.resolveToken(accessToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + +} \ No newline at end of file diff --git a/src/main/java/com/fledge/fledgeserver/auth/jwt/filter/RefreshFilter.java b/src/main/java/com/fledge/fledgeserver/auth/jwt/filter/RefreshFilter.java new file mode 100644 index 0000000..d1e5b1a --- /dev/null +++ b/src/main/java/com/fledge/fledgeserver/auth/jwt/filter/RefreshFilter.java @@ -0,0 +1,54 @@ +package com.fledge.fledgeserver.auth.jwt.filter; + +import com.fledge.fledgeserver.auth.jwt.TokenProvider; +import com.fledge.fledgeserver.exception.CustomException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.fledge.fledgeserver.exception.ErrorCode.INVALID_TOKEN; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RefreshFilter extends OncePerRequestFilter { + + private final TokenProvider tokenProvider; + private final RedisTemplate redisTemplate; + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { + + + if (request.getRequestURI().equals("/api/v1/auth/tokenRefresh")) { + + String jwt = tokenProvider.resolveToken(request); + + if (jwt != null) { + tokenProvider.validateToken(jwt); + + Authentication authentication = tokenProvider.resolveToken(jwt); + String refreshToken = redisTemplate.opsForValue().get(authentication.getName()); + if (refreshToken == null) { + throw new CustomException(INVALID_TOKEN, "Refresh Token not found."); + } else if (!refreshToken.equals(jwt)) { + throw new CustomException(INVALID_TOKEN, "Refresh Token doesn't match."); + } + } + } + + chain.doFilter(request, response); + + } + +} \ No newline at end of file diff --git a/src/main/java/com/fledge/fledgeserver/auth/service/AuthService.java b/src/main/java/com/fledge/fledgeserver/auth/service/AuthService.java new file mode 100644 index 0000000..fd338a3 --- /dev/null +++ b/src/main/java/com/fledge/fledgeserver/auth/service/AuthService.java @@ -0,0 +1,50 @@ +package com.fledge.fledgeserver.auth.service; + +import com.fledge.fledgeserver.auth.dto.TokenResponse; +import com.fledge.fledgeserver.auth.jwt.TokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + private final TokenProvider tokenProvider; + private final RedisTemplate redisTemplate; + @Value("${jwt.refresh_expired-time}") + long refreshExpired; + + public void logout(HttpServletRequest request, HttpServletResponse response) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null) { + redisTemplate.delete(authentication.getName()); + + new SecurityContextLogoutHandler().logout(request, response, authentication); + } + } + + public TokenResponse refreshToken() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + TokenResponse jwt = tokenProvider.createToken(authentication); + redisTemplate.opsForValue().set( + authentication.getName(), + jwt.getRefreshToken(), + refreshExpired, + TimeUnit.SECONDS + ); + return jwt; + } + + +} diff --git a/src/main/java/com/fledge/fledgeserver/auth/service/TokenService.java b/src/main/java/com/fledge/fledgeserver/auth/service/TokenService.java deleted file mode 100644 index 323c9b8..0000000 --- a/src/main/java/com/fledge/fledgeserver/auth/service/TokenService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.fledge.fledgeserver.auth.service; - -import com.fledge.fledgeserver.auth.jwt.Token; -import com.fledge.fledgeserver.auth.jwt.TokenRepository; -import com.fledge.fledgeserver.exception.TokenException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import static com.fledge.fledgeserver.exception.ErrorCode.TOKEN_EXPIRED; - -@Slf4j -@RequiredArgsConstructor -@Service -public class TokenService { - - private final TokenRepository tokenRepository; - - public void deleteRefreshToken(String memberKey) { - tokenRepository.deleteById(memberKey); - } - - @Transactional - public void saveOrUpdate(String memberKey, String refreshToken, String accessToken) { - Token token = tokenRepository.findByAccessToken(accessToken) - .map(o -> o.updateRefreshToken(refreshToken)) - .orElseGet(() -> new Token(memberKey, refreshToken, accessToken)); - - tokenRepository.save(token); - } - - public Token findByAccessTokenOrThrow(String accessToken) { - return tokenRepository.findByAccessToken(accessToken) - .orElseThrow(() -> new TokenException(TOKEN_EXPIRED)); - } - - @Transactional - public void updateToken(String accessToken, Token token) { - token.updateAccessToken(accessToken); - tokenRepository.save(token); - } -} diff --git a/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileRequest.java b/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileRequest.java index e316d15..495ffc3 100644 --- a/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileRequest.java +++ b/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileRequest.java @@ -1,10 +1,7 @@ package com.fledge.fledgeserver.canary.dto; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Past; -import jakarta.validation.constraints.Size; +import jakarta.validation.constraints.*; import lombok.Getter; import lombok.Setter; @@ -27,6 +24,7 @@ public class CanaryProfileRequest { @Schema(description = "전화번호", required = true, example = "010-1234-5678") @NotBlank(message = "전화번호는 필수입니다.") @Size(max = 255, message = "전화번호는 최대 255자까지 입력 가능합니다.") + @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", message = "10 ~ 11 자리의 숫자만 입력 가능합니다.") private String phone; @Schema(description = "생년월일", required = true, example = "1990-01-01") diff --git a/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileUpdateRequest.java b/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileUpdateRequest.java index 685bb46..6b39a41 100644 --- a/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileUpdateRequest.java +++ b/src/main/java/com/fledge/fledgeserver/canary/dto/CanaryProfileUpdateRequest.java @@ -1,13 +1,10 @@ package com.fledge.fledgeserver.canary.dto; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.*; import lombok.Getter; import lombok.Setter; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Past; -import jakarta.validation.constraints.Size; import java.util.Date; @Getter @@ -23,6 +20,7 @@ public class CanaryProfileUpdateRequest { @Schema(description = "전화번호", required = true, example = "010-1234-5678") @NotBlank(message = "전화번호는 필수입니다.") @Size(max = 20, message = "전화번호는 최대 20자까지 입력 가능합니다.") + @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", message = "10 ~ 11 자리의 숫자만 입력 가능합니다.") private String phone; @Schema(description = "생년월일", required = true, example = "1990-01-01") diff --git a/src/main/java/com/fledge/fledgeserver/canary/repository/CanaryProfileRepository.java b/src/main/java/com/fledge/fledgeserver/canary/repository/CanaryProfileRepository.java index 23b820c..65f4a52 100644 --- a/src/main/java/com/fledge/fledgeserver/canary/repository/CanaryProfileRepository.java +++ b/src/main/java/com/fledge/fledgeserver/canary/repository/CanaryProfileRepository.java @@ -7,9 +7,13 @@ import java.util.Optional; public interface CanaryProfileRepository extends JpaRepository { - boolean existsByMember(Member member); Optional findByMemberId(Long memberId); - Optional findCanaryProfileByMemberId(Long memberId); + Optional findByMemberIdAndApprovalStatusIsTrue(Long memberId); + + boolean existsByMemberAndApprovalStatusIsTrue(Member member); + + boolean existsByMember(Member member); + } diff --git a/src/main/java/com/fledge/fledgeserver/canary/service/CanaryProfileService.java b/src/main/java/com/fledge/fledgeserver/canary/service/CanaryProfileService.java index 8421c5b..ced94dc 100644 --- a/src/main/java/com/fledge/fledgeserver/canary/service/CanaryProfileService.java +++ b/src/main/java/com/fledge/fledgeserver/canary/service/CanaryProfileService.java @@ -1,6 +1,5 @@ package com.fledge.fledgeserver.canary.service; -import com.fledge.fledgeserver.auth.dto.OAuthUserImpl; import com.fledge.fledgeserver.canary.dto.*; import com.fledge.fledgeserver.canary.entity.CanaryProfile; import com.fledge.fledgeserver.canary.repository.CanaryProfileRepository; @@ -12,8 +11,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static com.fledge.fledgeserver.exception.ErrorCode.MEMBER_FORBIDDEN; - @Service @RequiredArgsConstructor @@ -25,8 +22,7 @@ public class CanaryProfileService { public void createCanaryProfile(CanaryProfileRequest request) { Member member = SecurityUtils.checkAndGetCurrentUser(request.getUserId()); - boolean exists = canaryProfileRepository.existsByMember(member); - if (exists) { + if (canaryProfileRepository.existsByMember(member)){ throw new CustomException(ErrorCode.DUPLICATE_APPLICATION); } @@ -74,7 +70,11 @@ public CanaryProfileResponse getCanaryProfile(Long userId) { @Transactional public CanaryProfileResponse updateCanaryProfile(Long userId, CanaryProfileUpdateRequest request) { - SecurityUtils.checkAndGetCurrentUser(userId); + Member member = SecurityUtils.checkAndGetCurrentUser(userId); + + if (!canaryProfileRepository.existsByMemberAndApprovalStatusIsTrue(member)){ + throw new CustomException(ErrorCode.CANARY_NOT_FOUND, "인증되지 않은 자립준비청년 입니다."); + } CanaryProfile existingProfile = canaryProfileRepository.findByMemberId(userId) .orElseThrow(() -> new CustomException(ErrorCode.CANARY_NOT_FOUND)); @@ -90,7 +90,7 @@ public CanaryProfileResponse updateCanaryProfile(Long userId, CanaryProfileUpdat @Transactional(readOnly = true) public CanaryGetDeliveryInfoResponse getCanaryDeliveryInfo() { Long userId = SecurityUtils.getCurrentUserId(); - CanaryProfile canary = canaryProfileRepository.findCanaryProfileByMemberId(userId) + CanaryProfile canary = canaryProfileRepository.findByMemberIdAndApprovalStatusIsTrue(userId) .orElseThrow(() -> new CustomException(ErrorCode.CANARY_NOT_FOUND)); return new CanaryGetDeliveryInfoResponse( canary.getName(), @@ -103,7 +103,7 @@ public CanaryGetDeliveryInfoResponse getCanaryDeliveryInfo() { @Transactional(readOnly = true) public CanaryProfileGetResponse getCanaryForSupport(Long memberId) { - CanaryProfile canaryProfile = canaryProfileRepository.findByMemberId(memberId) + CanaryProfile canaryProfile = canaryProfileRepository.findByMemberIdAndApprovalStatusIsTrue(memberId) .orElseThrow(() -> new CustomException(ErrorCode.CANARY_NOT_FOUND)); return new CanaryProfileGetResponse(canaryProfile); diff --git a/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeParticipationService.java b/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeParticipationService.java index 5f1726a..0b10e74 100644 --- a/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeParticipationService.java +++ b/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeParticipationService.java @@ -1,5 +1,6 @@ package com.fledge.fledgeserver.challenge.service; +import com.fledge.fledgeserver.canary.repository.CanaryProfileRepository; import com.fledge.fledgeserver.challenge.repository.ChallengeRepository; import com.fledge.fledgeserver.challenge.Enum.Frequency; import com.fledge.fledgeserver.challenge.dto.TopParticipantResponse; @@ -13,7 +14,6 @@ import com.fledge.fledgeserver.exception.CustomException; import com.fledge.fledgeserver.exception.ErrorCode; import com.fledge.fledgeserver.member.entity.Member; -import com.fledge.fledgeserver.member.repository.MemberRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -29,7 +29,7 @@ public class ChallengeParticipationService { private final ChallengeParticipationRepository participationRepository; private final ChallengeProofRepository proofRepository; - private final MemberRepository memberRepository; + private final CanaryProfileRepository canaryProfileRepository; private final ChallengeRepository challengeRepository; @Transactional @@ -37,6 +37,10 @@ public ChallengeParticipationResponse participateInChallenge(Long memberId, Long Member member = SecurityUtils.checkAndGetCurrentUser(memberId); + if (!canaryProfileRepository.existsByMemberAndApprovalStatusIsTrue(member)){ + throw new CustomException(ErrorCode.CANARY_NOT_FOUND, "인증된 자립준비 청년이 아닙니다."); + } + Challenge challenge = challengeRepository.findById(challengeId) .orElseThrow(() -> new CustomException(ErrorCode.CHALLENGE_NOT_FOUND)); diff --git a/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeProofService.java b/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeProofService.java index 4100d80..c7b99cb 100644 --- a/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeProofService.java +++ b/src/main/java/com/fledge/fledgeserver/challenge/service/ChallengeProofService.java @@ -1,10 +1,13 @@ package com.fledge.fledgeserver.challenge.service; +import com.fledge.fledgeserver.canary.repository.CanaryProfileRepository; import com.fledge.fledgeserver.challenge.repository.ChallengeProofRepository; import com.fledge.fledgeserver.challenge.dto.ChallengeProofResponse; import com.fledge.fledgeserver.challenge.entity.ChallengeProof; +import com.fledge.fledgeserver.common.utils.SecurityUtils; import com.fledge.fledgeserver.exception.CustomException; import com.fledge.fledgeserver.exception.ErrorCode; +import com.fledge.fledgeserver.member.entity.Member; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -16,9 +19,14 @@ public class ChallengeProofService { private final ChallengeProofRepository proofRepository; + private final CanaryProfileRepository canaryProfileRepository; @Transactional public ChallengeProofResponse uploadProof(Long participationId, LocalDate proofDate, String proofImageUrl) { + Member member = SecurityUtils.getCurrentMember(); + if (!canaryProfileRepository.existsByMemberAndApprovalStatusIsTrue(member)){ + throw new CustomException(ErrorCode.CANARY_NOT_FOUND, "인증된 자립준비 청년이 아닙니다."); + } ChallengeProof proof = proofRepository.findByParticipationIdAndProofDate(participationId, proofDate) .orElseThrow(() -> new CustomException(ErrorCode.CHALLENGE_PROOF_NOT_FOUND)); diff --git a/src/main/java/com/fledge/fledgeserver/config/WebSecurityConfig.java b/src/main/java/com/fledge/fledgeserver/config/WebSecurityConfig.java index d8d0990..ad83861 100644 --- a/src/main/java/com/fledge/fledgeserver/config/WebSecurityConfig.java +++ b/src/main/java/com/fledge/fledgeserver/config/WebSecurityConfig.java @@ -1,10 +1,11 @@ package com.fledge.fledgeserver.config; +import com.fledge.fledgeserver.auth.jwt.filter.JwtFilter; +import com.fledge.fledgeserver.auth.jwt.filter.RefreshFilter; import com.fledge.fledgeserver.auth.handler.CustomAccessDeniedHandler; import com.fledge.fledgeserver.auth.handler.CustomAuthenticationEntryPoint; import com.fledge.fledgeserver.auth.handler.OAuth2FailureHandler; import com.fledge.fledgeserver.auth.handler.OAuth2SuccessHandler; -import com.fledge.fledgeserver.auth.filter.TokenAuthenticationFilter; import com.fledge.fledgeserver.auth.service.CustomOAuth2UserService; import com.fledge.fledgeserver.exception.GlobalExceptionHandlerFilter; import lombok.RequiredArgsConstructor; @@ -35,7 +36,8 @@ public class WebSecurityConfig { private final CustomOAuth2UserService oAuth2UserService; private final OAuth2SuccessHandler oAuth2SuccessHandler; private final OAuth2FailureHandler oAuth2FailureHandler; - private final TokenAuthenticationFilter tokenAuthenticationFilter; + private final JwtFilter jwtFilter; + private final RefreshFilter refreshFilter; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @@ -69,9 +71,10 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .failureHandler(oAuth2FailureHandler) ) - .addFilterBefore(tokenAuthenticationFilter, + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) - .addFilterBefore(new GlobalExceptionHandlerFilter(), tokenAuthenticationFilter.getClass()) + .addFilterBefore(refreshFilter, JwtFilter.class) + .addFilterBefore(new GlobalExceptionHandlerFilter(), refreshFilter.getClass()) .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) diff --git a/src/main/java/com/fledge/fledgeserver/member/controller/MemberController.java b/src/main/java/com/fledge/fledgeserver/member/controller/MemberController.java index a38a3c0..85f31b3 100644 --- a/src/main/java/com/fledge/fledgeserver/member/controller/MemberController.java +++ b/src/main/java/com/fledge/fledgeserver/member/controller/MemberController.java @@ -43,8 +43,7 @@ public ResponseEntity> getMemberDetails( @PutMapping("/{id}/nickname") public ResponseEntity> updateNickname( @Parameter(description = "회원 ID", required = true, example = "1") @PathVariable Long id, - @Parameter(description = "닉네임 수정 요청", required = true) @RequestBody MemberNicknameUpdateRequest request, - @AuthenticationPrincipal OAuthUserImpl oAuth2User) { + @Parameter(description = "닉네임 수정 요청", required = true) @RequestBody MemberNicknameUpdateRequest request) { MemberResponse memberResponse = memberService.updateNickname(id, request.getNickname()); return ApiResponse.success(SuccessStatus.MEMBER_NICKNAME_UPDATE_SUCCESS, memberResponse); } diff --git a/src/main/java/com/fledge/fledgeserver/response/SuccessStatus.java b/src/main/java/com/fledge/fledgeserver/response/SuccessStatus.java index 3fabe55..f66e5f8 100644 --- a/src/main/java/com/fledge/fledgeserver/response/SuccessStatus.java +++ b/src/main/java/com/fledge/fledgeserver/response/SuccessStatus.java @@ -9,6 +9,12 @@ @RequiredArgsConstructor(access = AccessLevel.PROTECTED) public enum SuccessStatus { + /** + * auth + */ + TOKEN_REFRESH_SUCCESS(HttpStatus.OK, "토큰 갱신 성공"), + LOGOUT_SUCCESS(HttpStatus.OK, "로그아웃 성공"), + /** * member */ diff --git a/src/main/java/com/fledge/fledgeserver/support/dto/request/PostCreateRequest.java b/src/main/java/com/fledge/fledgeserver/support/dto/request/PostCreateRequest.java index affdd31..24abadf 100644 --- a/src/main/java/com/fledge/fledgeserver/support/dto/request/PostCreateRequest.java +++ b/src/main/java/com/fledge/fledgeserver/support/dto/request/PostCreateRequest.java @@ -71,6 +71,7 @@ public class PostCreateRequest { private String recipientName; @Schema(description = "전화번호", example = "010-1234-5678") + @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", message = "10 ~ 11 자리의 숫자만 입력 가능합니다.") private String phone; @Schema(description = "주소", example = "서울특별시 노원구 공릉로232") diff --git a/src/main/java/com/fledge/fledgeserver/support/dto/request/PostUpdateRequest.java b/src/main/java/com/fledge/fledgeserver/support/dto/request/PostUpdateRequest.java index 0844129..29a34d9 100644 --- a/src/main/java/com/fledge/fledgeserver/support/dto/request/PostUpdateRequest.java +++ b/src/main/java/com/fledge/fledgeserver/support/dto/request/PostUpdateRequest.java @@ -68,6 +68,7 @@ public class PostUpdateRequest { private String recipientName; @Schema(description = "전화번호", example = "010-1234-5678") + @Pattern(regexp = "^01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$", message = "10 ~ 11 자리의 숫자만 입력 가능합니다.") @NotBlank(message = "전화번호는 필수입니다.") private String phone;