diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e3d9c76..c6a33bb 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -7,7 +7,7 @@ on: workflow_dispatch: env: - S3_BUCKET_NAME: wepro + S3_BUCKET_NAME: wepro1 RESOURCE_PATH: ./src/main/resources/application.yaml CODE_DEPLOY_APPLICATION_NAME: wepro-code-deploy CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: wepro-server @@ -16,6 +16,20 @@ jobs: build: runs-on: ubuntu-latest + services: + mysql: + image: mysql:8.0 + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} + MYSQL_DATABASE: wepro + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + steps: - name: Checkout uses: actions/checkout@v2 @@ -33,11 +47,23 @@ jobs: spring.datasource.url: ${{ secrets.SPRING_DATASOURCE_URL }} spring.datasource.username: ${{ secrets.SPRING_DATASOURCE_USERNAME }} spring.datasource.password: ${{ secrets.SPRING_DATASOURCE_PASSWORD }} + jwt.secret: ${{ secrets.JWT_SECRET }} + login.uri: ${{ secrets.LOGIN_URI }} + kakao.client-id: ${{ secrets.KAKAO_CLIENT_ID }} + kakao.client-secret: ${{ secrets.KAKAO_CLIENT_SECRET }} + kakao.redirect-uri: ${{ secrets.KAKAO_REDIRECT_URI }} - name: Grant execute permission for gradlew run: chmod +x ./gradlew shell: bash + - name: Wait for MySQL + run: | + while ! mysqladmin ping -h"127.0.0.1" --silent; do + echo "Waiting for MySQL to be ready..." + sleep 1 + done + - name: Build with Gradle run: ./gradlew build shell: bash @@ -62,4 +88,4 @@ jobs: --deployment-config-name CodeDeployDefault.AllAtOnce \ --application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \ --deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \ - --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip \ No newline at end of file + --s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip diff --git a/build.gradle b/build.gradle index 1fb633e..999582c 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,22 @@ dependencies { // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // security + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // jwt + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // feign + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.2") + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/db/docker-compose.yml b/db/docker-compose.yml index c1fea26..8aa9795 100644 --- a/db/docker-compose.yml +++ b/db/docker-compose.yml @@ -18,3 +18,12 @@ services: - ${DEFAULT_PATH}/mysql/data:/var/lib/mysql - ${DEFAULT_PATH}/mysql/initdb.d:/docker-entrypoint-initdb.d restart: always + redis: + container_name: "redis" + image: redis:latest + command: redis-server --port 6379 + ports: + - "6379:6379" + volumes: + - ${DEFAULT_PATH}/redis/data:/data + restart: always \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java b/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java new file mode 100644 index 0000000..02e779d --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java @@ -0,0 +1,29 @@ +package com._119.wepro.auth.client; + +import com._119.wepro.auth.dto.response.KakaoTokenResponse; +import com._119.wepro.auth.dto.response.OIDCPublicKeyResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; + +@FeignClient( + name = "KakaoOauthClient", + url = "https://kauth.kakao.com" +) +public interface KakaoOauthClient { + + // 만약 클라이언트로부터 code 받을 경우, + @PostMapping( + "/oauth/token?grant_type=authorization_code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&code={CODE}&client_secret={CLIENT_SECRET}") + KakaoTokenResponse kakaoAuth( + @PathVariable("CLIENT_ID") String clientId, + @PathVariable("REDIRECT_URI") String redirectUri, + @PathVariable("CODE") String code, + @PathVariable("CLIENT_SECRET") String client_secret); + + // oidc 공개 키 받아 오기 - 안 쓸 예정 +// @Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager") // 공개키 자주 요청할 거 같으면, 캐싱하기 + @GetMapping("/.well-known/jwks.json") + OIDCPublicKeyResponse getOIDCPublicKey(); +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java new file mode 100644 index 0000000..f3c6cee --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java @@ -0,0 +1,49 @@ +package com._119.wepro.auth.dto.request; + +import com._119.wepro.global.enums.Provider; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +public class AuthRequest { + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SignInRequest { + + @NotNull + @Enumerated(EnumType.STRING) + private Provider provider; + + @NotNull + private String idToken; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class RefreshRequest { + @NotNull + private String accessToken; + + @NotNull + private String refreshToken; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SignUpRequest { + + @NotNull + private String position; + } +} diff --git a/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java new file mode 100644 index 0000000..e6b4bab --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java @@ -0,0 +1,16 @@ +package com._119.wepro.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +public class AuthResponse { + + @Getter + @AllArgsConstructor + public static class SignInResponse { + + private boolean newMember; + private TokenInfo tokenInfo; + } + +} diff --git a/src/main/java/com/_119/wepro/auth/dto/response/KakaoTokenResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/KakaoTokenResponse.java new file mode 100644 index 0000000..f339f5f --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/KakaoTokenResponse.java @@ -0,0 +1,15 @@ +package com._119.wepro.auth.dto.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@JsonNaming(SnakeCaseStrategy.class) +public class KakaoTokenResponse { + private String accessToken; + private String refreshToken; + private String idToken; +} diff --git a/src/main/java/com/_119/wepro/auth/dto/response/OIDCPublicKeyResponse.java b/src/main/java/com/_119/wepro/auth/dto/response/OIDCPublicKeyResponse.java new file mode 100644 index 0000000..5f27996 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/OIDCPublicKeyResponse.java @@ -0,0 +1,23 @@ +package com._119.wepro.auth.dto.response; + +import java.util.List; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class OIDCPublicKeyResponse { + + private List keys; + + @Getter + @NoArgsConstructor + public static class OIDCPublicKey { + + private String kid; + private String alg; + private String use; + private String n; + private String e; + } +} diff --git a/src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java b/src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java new file mode 100644 index 0000000..8912728 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java @@ -0,0 +1,12 @@ +package com._119.wepro.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TokenInfo { + private String type; + private String accessToken; + private String refreshToken; +} diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java new file mode 100644 index 0000000..57a9415 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java @@ -0,0 +1,50 @@ +package com._119.wepro.auth.jwt; + +import com._119.wepro.global.dto.ErrorResponseDto; +import com._119.wepro.global.exception.RestApiException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +public class JwtTokenExceptionFilter extends OncePerRequestFilter { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (RestApiException e) { + logClientIpAndRequestUri(request); + sendErrorResponse(response, e); + } + } + + private void logClientIpAndRequestUri(HttpServletRequest request) { + String clientIp = request.getHeader("X-Forwarded-For"); + if (clientIp == null) { + clientIp = request.getRemoteAddr(); + } + log.error("Invalid token for requestURI: {}, Access from IP: {}", request.getRequestURI(), + clientIp); + } + + private void sendErrorResponse(HttpServletResponse response, RestApiException e) + throws IOException { + ErrorResponseDto errorResponseDto = ErrorResponseDto.builder() + .code(e.getErrorCode().name()) + .message(e.getErrorCode().getMessage()) + .build(); + + response.setStatus(e.getErrorCode().getHttpStatus().value()); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write(objectMapper.writeValueAsString(errorResponseDto)); + } +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java new file mode 100644 index 0000000..341c3d3 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java @@ -0,0 +1,55 @@ +package com._119.wepro.auth.jwt; + +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.NOT_EXIST_BEARER_SUFFIX; + +import com._119.wepro.global.exception.RestApiException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; + +@Slf4j +@RequiredArgsConstructor +public class JwtTokenFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + private final String accessHeader = "Authorization"; + private final String grantType = "Bearer"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + Optional token = getTokensFromHeader(request, accessHeader); + + token.ifPresent(t -> { + String accessToken = getAccessToken(t); + + Authentication authentication = jwtTokenProvider.getAuthentication(accessToken); + + SecurityContextHolder.getContext().setAuthentication(authentication); + }); + filterChain.doFilter(request, response); + } + + private Optional getTokensFromHeader(HttpServletRequest request, String header) { + return Optional.ofNullable(request.getHeader(header)); + } + + private String getAccessToken(String token) { + String suffix = grantType + " "; + + if (!token.startsWith(suffix)) { + throw new RestApiException(NOT_EXIST_BEARER_SUFFIX); + } + + return token.replace(suffix, ""); + } +} diff --git a/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java new file mode 100644 index 0000000..c95870c --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/jwt/JwtTokenProvider.java @@ -0,0 +1,164 @@ +package com._119.wepro.auth.jwt; + +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.EXPIRED_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; + +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.global.enums.Role; +import com._119.wepro.global.exception.RestApiException; +import com._119.wepro.global.util.RedisUtil; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +@Component +public class JwtTokenProvider { + + private static final long ACCESS_TOKEN_DURATION = 1000 * 60 * 60L * 24; // 1일 + private static final long REFRESH_TOKEN_DURATION = 1000 * 60 * 60L * 24 * 7; // 7일 + private static final String AUTHORITIES_KEY = "auth"; + private final RedisUtil redisUtil; + private SecretKey secretKey; + + public JwtTokenProvider(@Value("${jwt.secret}") String key, RedisUtil redisUtil) { + this.redisUtil = redisUtil; + byte[] keyBytes = key.getBytes(); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); + } + + public TokenInfo generateToken(String providerId, Role memberRole) { + String accessToken = generateAccessToken(providerId, memberRole); + String refreshToken = generateRefreshToken(); + + deleteInvalidRefreshToken(providerId); + redisUtil.setData(providerId, refreshToken); + + return new TokenInfo("Bearer", accessToken, refreshToken); + } + + public boolean validateToken(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + + Claims claims = parseClaims(token); + return claims.getExpiration().after(new Date()); + } + + + public Authentication getAuthentication(String accessToken) { + Claims claims = parseClaims(accessToken); + + if (claims.get(AUTHORITIES_KEY) == null) { + throw new RestApiException(INVALID_TOKEN); + } + List authority = getAuthorities(claims); + validateAuthorityValue(authority); + + UserDetails principal = new User(claims.getSubject(), "", authority); + return new UsernamePasswordAuthenticationToken(principal, "", authority); + } + + private void validateAuthorityValue(List authority) { + if (authority.size() != 1 + || !isValidAuthority(authority.get(0))) { + throw new RestApiException(INVALID_TOKEN); + } + } + + private boolean isValidAuthority(SimpleGrantedAuthority authority) { + for (Role role : Role.values()) { + if (role.name().equals(authority.getAuthority())) { + return true; + } + } + return false; + } + + private Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(accessToken) + .getBody(); + } catch (SecurityException | MalformedJwtException e) { + throw new RestApiException(INVALID_TOKEN); + } catch (ExpiredJwtException e) { + throw new RestApiException(EXPIRED_TOKEN); + } catch (UnsupportedJwtException e) { + throw new RestApiException(INVALID_TOKEN); + } catch (IllegalArgumentException e) { + throw new RestApiException(INVALID_TOKEN); + } catch (JwtException e) { + throw new RestApiException(INVALID_TOKEN); + } + } + + private String generateAccessToken(String providerId, Role memberRole) { + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + ACCESS_TOKEN_DURATION); + + return Jwts.builder() + .setSubject(providerId) + .claim(AUTHORITIES_KEY, memberRole.name()) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + private String generateRefreshToken() { + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + REFRESH_TOKEN_DURATION); + + return Jwts.builder() + .setExpiration(expiredDate) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + private List getAuthorities(Claims claims) { + return Collections.singletonList(new SimpleGrantedAuthority( + claims.get(AUTHORITIES_KEY).toString())); + } + + public String getRefreshToken(String provierId) { + return redisUtil.getData(provierId); + } + + public void deleteInvalidRefreshToken(String provierId) { + redisUtil.deleteData(provierId); + } + + public Claims parseExpiredToken(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } catch (JwtException e) { + throw new RestApiException(INVALID_TOKEN); + } + } +} diff --git a/src/main/java/com/_119/wepro/auth/presentation/AuthController.java b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java new file mode 100644 index 0000000..f9f8c8f --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/presentation/AuthController.java @@ -0,0 +1,75 @@ +package com._119.wepro.auth.presentation; + +import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.auth.service.KakaoService; +import com._119.wepro.auth.service.RefreshService; +import com._119.wepro.auth.service.AuthService; +import com._119.wepro.auth.dto.request.AuthRequest.*; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.view.RedirectView; + +@Slf4j +@RequiredArgsConstructor +@RestController +public class AuthController { + + private final KakaoService kakaoService; + private final AuthService authService; + private final RefreshService refreshService; + + @PostMapping("/login") + @Operation(summary = "Kakao idToken 받아 소셜 로그인") + public ResponseEntity signIn( + @RequestBody @Valid SignInRequest request) { + return ResponseEntity.ok(authService.signIn(request)); + } + + @PostMapping("/refresh") + @Operation(summary = "access token 재발급") + public ResponseEntity refresh( + @RequestBody @Valid RefreshRequest request) { + return ResponseEntity.ok(refreshService.refresh(request)); + } + + @PostMapping("/logout") + @Operation(summary = "로그아웃") + public ResponseEntity logout(Authentication authentication){ + authService.logOut(authentication.getName()); + return ResponseEntity.ok().build(); + } + + @PostMapping("/signup") + @Operation(summary = "직군 정보 받아 최종 회원가입") + public ResponseEntity register( + @RequestBody @Valid SignUpRequest request) { + return ResponseEntity.ok().build(); + } + + @GetMapping("/auth/kakao") + @Operation(summary = "Kakao Web 소셜 로그인 용 api, 백엔드용") + public RedirectView kakaoLogin() { + return new RedirectView(kakaoService.generateKakaoRedirectUrl()); + } + + @GetMapping("/login/oauth2/code/kakao") + @Operation(summary = "카카오 code 받는 api, 백엔드용") + public SignInResponse handleKakaoCallback(@RequestParam String code) { + return kakaoService.handleKakaoCallback(code); + } + + @GetMapping("/test") + public void test(Authentication authentication) { + log.info(authentication.getName()); + } +} diff --git a/src/main/java/com/_119/wepro/auth/service/AuthService.java b/src/main/java/com/_119/wepro/auth/service/AuthService.java new file mode 100644 index 0000000..865bde6 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/service/AuthService.java @@ -0,0 +1,80 @@ +package com._119.wepro.auth.service; + +import static com._119.wepro.global.enums.Provider.APPLE; +import static com._119.wepro.global.enums.Provider.KAKAO; + +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; +import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.auth.jwt.JwtTokenProvider; +import com._119.wepro.global.enums.Provider; +import com._119.wepro.global.enums.Role; +import com._119.wepro.member.domain.Member; +import com._119.wepro.member.domain.repository.MemberRepository; +import jakarta.transaction.Transactional; +import java.util.Map; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.stereotype.Service; + + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthService { + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + private final Map decoders = Map.of( + Provider.KAKAO, buildDecoder(KAKAO.getJwkSetUrl()), + Provider.APPLE, buildDecoder(APPLE.getJwkSetUrl())); + + private JwtDecoder buildDecoder(String jwkUrl) { + return NimbusJwtDecoder.withJwkSetUri(jwkUrl).build(); + } + + @Transactional + public SignInResponse signIn(SignInRequest request) { + OidcUser oidcDecodePayload = socialLogin(request); + + Member member = getOrSaveUser(request, oidcDecodePayload); + TokenInfo tokenInfo = jwtTokenProvider.generateToken(member.getProviderId(), member.getRole()); + boolean isNewMember = Role.GUEST == member.getRole(); + + return new SignInResponse(isNewMember, tokenInfo); + } + + @Transactional + public void logOut(String providerId) { + jwtTokenProvider.deleteInvalidRefreshToken(providerId); + } + + + private Member getOrSaveUser(SignInRequest request, OidcUser oidcDecodePayload) { + Optional member = memberRepository.findByProviderAndProviderId( + request.getProvider(), oidcDecodePayload.getName()); + + return member.orElseGet( + () -> memberRepository.save(Member.of(request, oidcDecodePayload))); + + } + + private OidcUser socialLogin(SignInRequest request) { + Provider provider = request.getProvider(); + + Jwt jwt = decoders.get(provider).decode(request.getIdToken()); + OidcIdToken oidcIdToken = new OidcIdToken( + jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); + + return new DefaultOidcUser(null, oidcIdToken); +// throw new RestApiException(UNSUPPORTED_PROVIDER); + } +} diff --git a/src/main/java/com/_119/wepro/auth/service/KakaoService.java b/src/main/java/com/_119/wepro/auth/service/KakaoService.java new file mode 100644 index 0000000..03b76bd --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/service/KakaoService.java @@ -0,0 +1,73 @@ +package com._119.wepro.auth.service; + +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; +import com._119.wepro.auth.dto.response.AuthResponse.SignInResponse; +import com._119.wepro.global.enums.Provider; +import com._119.wepro.auth.client.KakaoOauthClient; +import com._119.wepro.auth.dto.response.KakaoTokenResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +@Service +@RequiredArgsConstructor +public class KakaoService { + + private final KakaoOauthClient kakaoOauthClient; + + @Value("${login.uri}") + private String LOGIN_URI; + + @Value("${kakao.client-id}") + private String CLIENT_ID; + + @Value("${kakao.redirect-uri}") + private String REDIRECT_URI; + + @Value("${kakao.client-secret}") + private String CLIENT_SECRET; + + @Value("${kakao.authorization-uri}") + private String KAKAO_AUTH_URL; + + // 백엔드용 + public String generateKakaoRedirectUrl() { + return UriComponentsBuilder.fromUriString(KAKAO_AUTH_URL) + .queryParam("client_id", CLIENT_ID) + .queryParam("redirect_uri", REDIRECT_URI) + .queryParam("response_type", "code") + .build() + .toUriString(); + } + + // 백엔드용 + public SignInResponse handleKakaoCallback(String code) { + KakaoTokenResponse tokenResponse = kakaoOauthClient.kakaoAuth(CLIENT_ID, REDIRECT_URI, code, + CLIENT_SECRET); + String idToken = tokenResponse.getIdToken(); + ResponseEntity response = callLoginApiWithIdToken(idToken); + + return response.getBody(); + } + + // 백엔드용 + private ResponseEntity callLoginApiWithIdToken(String idToken) { + RestTemplate restTemplate = new RestTemplate(); + SignInRequest signInRequest = new SignInRequest(Provider.KAKAO, idToken); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity requestEntity = new HttpEntity<>(signInRequest, headers); + + ResponseEntity response = restTemplate.postForEntity(LOGIN_URI, requestEntity, + SignInResponse.class); + + return response; + } +} diff --git a/src/main/java/com/_119/wepro/auth/service/RefreshService.java b/src/main/java/com/_119/wepro/auth/service/RefreshService.java new file mode 100644 index 0000000..ed0d865 --- /dev/null +++ b/src/main/java/com/_119/wepro/auth/service/RefreshService.java @@ -0,0 +1,61 @@ +package com._119.wepro.auth.service; + +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.EXPIRED_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_TOKEN; +import static com._119.wepro.global.exception.errorcode.CommonErrorCode.REFRESH_DENIED; + +import com._119.wepro.auth.dto.request.AuthRequest.RefreshRequest; +import com._119.wepro.auth.dto.response.TokenInfo; +import com._119.wepro.auth.jwt.JwtTokenProvider; +import com._119.wepro.global.exception.RestApiException; +import com._119.wepro.global.exception.errorcode.UserErrorCode; +import com._119.wepro.member.domain.Member; +import com._119.wepro.member.domain.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +@RequiredArgsConstructor +public class RefreshService { + + private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; + + public TokenInfo refresh(RefreshRequest request) { + String accessToken = request.getAccessToken(); + String refreshToken = request.getRefreshToken(); + + if (!isTokenExpired(accessToken)) { + throw new RestApiException(REFRESH_DENIED); + } + String providerId = jwtTokenProvider.parseExpiredToken(accessToken) + .getSubject(); + validateRefreshToken(refreshToken, providerId); + + Member member = memberRepository.findByProviderId(providerId) + .orElseThrow(() -> new RestApiException(UserErrorCode.USER_NOT_FOUND)); + + return jwtTokenProvider.generateToken(providerId, member.getRole()); + } + + private boolean isTokenExpired(String accessToken) { + try { + jwtTokenProvider.validateToken(accessToken); + throw new RestApiException(REFRESH_DENIED); + } catch (RestApiException e) { + if (e.getErrorCode() == EXPIRED_TOKEN) { + return true; + } + throw e; + } + } + + private void validateRefreshToken(String refreshToken, String memberId) { + String savedRefreshToken = jwtTokenProvider.getRefreshToken(memberId); + if (!refreshToken.equals(savedRefreshToken)) { + throw new RestApiException(INVALID_TOKEN); + } + } +} diff --git a/src/main/java/com/_119/wepro/global/BaseEntity.java b/src/main/java/com/_119/wepro/global/BaseEntity.java index c382578..b3696ac 100644 --- a/src/main/java/com/_119/wepro/global/BaseEntity.java +++ b/src/main/java/com/_119/wepro/global/BaseEntity.java @@ -3,12 +3,11 @@ import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedDate; - -import java.time.LocalDateTime; import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Getter @@ -18,10 +17,9 @@ public abstract class BaseEntity { @CreatedDate - @Column(nullable = false, updatable = false) + @Column(updatable = false) private LocalDateTime createdAt; @LastModifiedDate - @Column(nullable = false) private LocalDateTime updatedAt; } diff --git a/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java b/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java new file mode 100644 index 0000000..bf04cb7 --- /dev/null +++ b/src/main/java/com/_119/wepro/global/config/FeignClientConfig.java @@ -0,0 +1,10 @@ +package com._119.wepro.global.config; + +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableFeignClients(basePackages = "com._119.wepro.auth.client") +public class FeignClientConfig { + +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/global/config/RedisConfig.java b/src/main/java/com/_119/wepro/global/config/RedisConfig.java new file mode 100644 index 0000000..a5f48ea --- /dev/null +++ b/src/main/java/com/_119/wepro/global/config/RedisConfig.java @@ -0,0 +1,37 @@ +package com._119.wepro.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +@EnableRedisRepositories +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + + return redisTemplate; + } +} diff --git a/src/main/java/com/_119/wepro/global/config/SecurityConfig.java b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java new file mode 100644 index 0000000..f19e5da --- /dev/null +++ b/src/main/java/com/_119/wepro/global/config/SecurityConfig.java @@ -0,0 +1,53 @@ +package com._119.wepro.global.config; + +import com._119.wepro.auth.jwt.JwtTokenExceptionFilter; +import com._119.wepro.auth.jwt.JwtTokenFilter; +import com._119.wepro.auth.jwt.JwtTokenProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtTokenProvider jwtTokenProvider; + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { // security를 적용하지 않을 리소스 + return web -> web.ignoring() + .requestMatchers("/css/**", "/images/**", "/js/**", "/lib/**") + .requestMatchers("/", "/swagger-ui-custom.html", "/api-docs/**", "/swagger-ui/**", + "swagger-ui.html", "/v3/api-docs/**") + .requestMatchers("/error", "/favicon.ico") + .requestMatchers("/auth/**", "/login/oauth2/code/**", "/login", "/refresh"); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .headers(header -> header.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)) + .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(request -> request + .requestMatchers("/", "/auth/**", "/login/oauth2/code/**", "/login").permitAll() + .requestMatchers(HttpMethod.OPTIONS).permitAll() + .anyRequest().authenticated() + ) + .logout(logout -> logout.disable()) + .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), + UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(new JwtTokenExceptionFilter(), JwtTokenFilter.class); + return http.build(); + } +} diff --git a/src/main/java/com/_119/wepro/global/enums/Provider.java b/src/main/java/com/_119/wepro/global/enums/Provider.java new file mode 100644 index 0000000..c7b508b --- /dev/null +++ b/src/main/java/com/_119/wepro/global/enums/Provider.java @@ -0,0 +1,17 @@ +package com._119.wepro.global.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum Provider { + KAKAO("https://kauth.kakao.com/.well-known/jwks.json"), + //TODO apple jwk set uri 세팅하기 + APPLE("https://kauth.kakao.com/.well-known/jwks.json"), + ; + + private final String jwkSetUrl; + +// public static IdentityProvider of(){} +} \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/global/enums/Role.java b/src/main/java/com/_119/wepro/global/enums/Role.java new file mode 100644 index 0000000..954a8b6 --- /dev/null +++ b/src/main/java/com/_119/wepro/global/enums/Role.java @@ -0,0 +1,5 @@ +package com._119.wepro.global.enums; + +public enum Role { + ADMIN, USER, GUEST +} diff --git a/src/main/java/com/_119/wepro/global/enums/Status.java b/src/main/java/com/_119/wepro/global/enums/Status.java new file mode 100644 index 0000000..645cbbc --- /dev/null +++ b/src/main/java/com/_119/wepro/global/enums/Status.java @@ -0,0 +1,5 @@ +package com._119.wepro.global.enums; + +public enum Status { + ACTIVE, INACTIVE +} diff --git a/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java b/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java index 6178f2f..344e555 100644 --- a/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/_119/wepro/global/exception/GlobalExceptionHandler.java @@ -3,9 +3,14 @@ import com._119.wepro.global.dto.ErrorResponseDto; import com._119.wepro.global.exception.errorcode.CommonErrorCode; import com._119.wepro.global.exception.errorcode.ErrorCode; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import feign.FeignException; import java.util.Collections; +import java.util.Map; import java.util.Objects; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; @@ -20,10 +25,13 @@ import static com._119.wepro.global.exception.errorcode.CommonErrorCode.INVALID_PARAMETER; +@RequiredArgsConstructor @RestControllerAdvice @Slf4j public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + private final ObjectMapper objectMapper; + @ExceptionHandler(RestApiException.class) public ResponseEntity handleCustomException(RestApiException e) { ErrorCode errorCode = e.getErrorCode(); @@ -44,6 +52,17 @@ public ResponseEntity handleAllException(Exception ex) { return handleExceptionInternal(errorCode); } + @ExceptionHandler(FeignException.class) + public ResponseEntity feignExceptionHandler(FeignException feignException) throws JsonProcessingException { + + String responseJson = feignException.contentUTF8(); + Map responseMap = objectMapper.readValue(responseJson, Map.class); + + return ResponseEntity + .status(feignException.status()) + .body(responseMap); + } + private ResponseEntity handleExceptionInternal(ErrorCode errorCode) { return ResponseEntity.status(errorCode.getHttpStatus()).body(makeErrorResponseDto(errorCode)); } diff --git a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java index 1e5c1cf..f3cf9cb 100644 --- a/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java +++ b/src/main/java/com/_119/wepro/global/exception/errorcode/CommonErrorCode.java @@ -10,6 +10,10 @@ public enum CommonErrorCode implements ErrorCode { INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "Invalid parameter included"), RESOURCE_NOT_FOUND(HttpStatus.NOT_FOUND, "Resource not exists"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error"), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "Invalid token"), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Expired token"), + NOT_EXIST_BEARER_SUFFIX(HttpStatus.UNAUTHORIZED, "Bearer prefix is missing."), + REFRESH_DENIED(HttpStatus.FORBIDDEN, "Refresh denied"), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java b/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java index 922ca47..8a6335d 100644 --- a/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java +++ b/src/main/java/com/_119/wepro/global/exception/errorcode/UserErrorCode.java @@ -10,6 +10,7 @@ public enum UserErrorCode implements ErrorCode { INACTIVE_USER(HttpStatus.FORBIDDEN, "User is inactive"), USER_NOT_FOUND(HttpStatus.NOT_FOUND, "User not found"), + UNSUPPORTED_PROVIDER(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "Unsupported provider"), ; private final HttpStatus httpStatus; diff --git a/src/main/java/com/_119/wepro/global/util/RedisUtil.java b/src/main/java/com/_119/wepro/global/util/RedisUtil.java new file mode 100644 index 0000000..872d4a4 --- /dev/null +++ b/src/main/java/com/_119/wepro/global/util/RedisUtil.java @@ -0,0 +1,42 @@ +package com._119.wepro.global.util; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RedisUtil { + + private final RedisTemplate redisTemplate; + + public String getData(String key) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + return valueOperations.get(key); + } + + public void setData(String key, String value) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value); + } + + public void setDataExpire(String key, String value, long duration) { + ValueOperations valueOperations = redisTemplate.opsForValue(); + valueOperations.set(key, value, Duration.ofMillis(duration)); + } + + public void deleteData(String key) { + redisTemplate.delete(key); + } + + public void expireValues(String key, int timeout) { + redisTemplate.expire(key, timeout, TimeUnit.MILLISECONDS); + } + + public boolean existsData(String key) { + return redisTemplate.hasKey(key); + } +} diff --git a/src/main/java/com/_119/wepro/member/domain/Member.java b/src/main/java/com/_119/wepro/member/domain/Member.java index c020c99..f7125c1 100644 --- a/src/main/java/com/_119/wepro/member/domain/Member.java +++ b/src/main/java/com/_119/wepro/member/domain/Member.java @@ -1,26 +1,84 @@ package com._119.wepro.member.domain; +import com._119.wepro.auth.dto.request.AuthRequest.SignInRequest; import com._119.wepro.global.BaseEntity; +import com._119.wepro.global.enums.Provider; +import com._119.wepro.global.enums.Role; +import com._119.wepro.global.enums.Status; +import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.PostPersist; +import jakarta.persistence.Table; +import java.time.LocalDateTime; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder +@Table( + indexes = { + @Index(name = "idx_provider_id", columnList = "providerId") + } +) public class Member extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + private String profile; + private String name; + + @Enumerated(value = EnumType.STRING) + @Column(length = 10, nullable = false) + private Provider provider; + + @Column(length = 20, nullable = false) + private String providerId; + + @Enumerated(value = EnumType.STRING) + @Column(length = 10, nullable = false) + private Status status; + + @Enumerated(value = EnumType.STRING) + @Column(length = 10, nullable = false) + private Role role; + + private String position; + + private String tag; + + private LocalDateTime inactivatedAt; + + // 엔티티가 저장된 후 id로 태그를 생성합니다. + @PostPersist + public void generateTag() { + this.tag = this.id.toString(); + } + + public static Member of(SignInRequest request, OidcUser oidcDecodePayload) { + return Member.builder() + .profile(oidcDecodePayload.getPicture()) + .name(oidcDecodePayload.getNickName()) + .provider(request.getProvider()) + .role(Role.GUEST) + .providerId(oidcDecodePayload.getName()) + .status(Status.ACTIVE) + // 태그는 나중에 설정됩니다. + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java b/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java index 412cbac..bd3d0cd 100644 --- a/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java +++ b/src/main/java/com/_119/wepro/member/domain/repository/MemberRepository.java @@ -1,10 +1,15 @@ package com._119.wepro.member.domain.repository; +import com._119.wepro.global.enums.Provider; import com._119.wepro.member.domain.Member; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface MemberRepository extends JpaRepository { + Optional findByProviderAndProviderId(Provider provider, String providerId); + + Optional findByProviderId(String providerId); } diff --git a/src/main/java/com/_119/wepro/member/presentation/MemberController.java b/src/main/java/com/_119/wepro/member/presentation/MemberController.java new file mode 100644 index 0000000..7958b5c --- /dev/null +++ b/src/main/java/com/_119/wepro/member/presentation/MemberController.java @@ -0,0 +1,12 @@ +package com._119.wepro.member.presentation; + +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/members") +public class MemberController { + +} diff --git a/src/main/java/com/_119/wepro/member/service/MemberService.java b/src/main/java/com/_119/wepro/member/service/MemberService.java new file mode 100644 index 0000000..ab8c31b --- /dev/null +++ b/src/main/java/com/_119/wepro/member/service/MemberService.java @@ -0,0 +1,10 @@ +package com._119.wepro.member.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class MemberService { + +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index b624804..2a79187 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -14,6 +14,20 @@ spring: hibernate: format_sql: true dialect: org.hibernate.dialect.MySQL8Dialect + data: + redis: + host: localhost + port: 6379 +jwt: + secret: ${jwt.secret} +login: + uri: ${login.uri} +kakao: + client-id: ${kakao.client-id} + client-secret: ${kakao.client-secret} + redirect-uri: ${kakao.redirect-uri} + iss: https://kauth.kakao.com + authorization-uri: https://kauth.kakao.com/oauth/authorize