diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 238afe2..46e6800 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: - name: application-ci.yml 주입 run: | mkdir -p src/main/resources - echo "${{ secrets.APPLICATION_YML_CI }}" | base64 --decode > src/main/resources/application-ci.yml + echo "${{ secrets.APPLICATION_YML_CI }}" > src/main/resources/application-ci.yml - name: 권한 세팅 run: chmod +x ./gradlew diff --git a/build.gradle b/build.gradle index 3fb9bea..997118e 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,11 @@ dependencies { implementation 'com.github.ben-manes.caffeine:caffeine' implementation 'org.springframework.retry:spring-retry:2.0.12' + + //jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' } tasks.named('test') { diff --git a/src/main/java/com/oronaminc/join/Web57OronaminCBeApplication.java b/src/main/java/com/oronaminc/join/Web57OronaminCBeApplication.java index 49f9e5b..dddd698 100644 --- a/src/main/java/com/oronaminc/join/Web57OronaminCBeApplication.java +++ b/src/main/java/com/oronaminc/join/Web57OronaminCBeApplication.java @@ -1,11 +1,14 @@ package com.oronaminc.join; +import com.oronaminc.join.member.token.JwtConfiguration; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @EnableJpaAuditing @SpringBootApplication +@EnableConfigurationProperties(JwtConfiguration.class) public class Web57OronaminCBeApplication { public static void main(String[] args) { diff --git a/src/main/java/com/oronaminc/join/global/dev/DevController.java b/src/main/java/com/oronaminc/join/global/dev/DevController.java index 4eaef57..d0baaf5 100644 --- a/src/main/java/com/oronaminc/join/global/dev/DevController.java +++ b/src/main/java/com/oronaminc/join/global/dev/DevController.java @@ -1,73 +1,66 @@ -package com.oronaminc.join.global.dev; - -import java.util.List; - -import org.springframework.context.annotation.Profile; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import com.oronaminc.join.member.dao.MemberRepository; -import com.oronaminc.join.member.domain.Member; -import com.oronaminc.join.member.domain.MemberType; -import com.oronaminc.join.member.security.MemberDetails; - -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import lombok.RequiredArgsConstructor; - -@Profile("local") -@RestController -@Tag(name = "개발용 API") -@RequestMapping("/dev") -@RequiredArgsConstructor -public class DevController { - private final MemberRepository memberRepository; - - @PostMapping("/join") - @ResponseStatus(HttpStatus.OK) - public Member devJoin(@RequestBody DevJoinRequest devJoinRequest) { - return memberRepository.save( - Member.builder() - .email(devJoinRequest.email()) - .nickname(devJoinRequest.nickname()) - .memberType(MemberType.MEMBER) - .build() - ); - } - - @PostMapping("/login") - @ResponseStatus(HttpStatus.OK) - public void devLogin(@RequestBody DevLoginRequest devLoginRequest, HttpServletRequest request) { - Member member = memberRepository.findById(devLoginRequest.memberId()) - .orElseThrow(() -> new IllegalArgumentException("해당 ID의 사용자가 존재하지 않습니다.")); - - MemberDetails memberDetails = MemberDetails.builder() - .id(member.getId()) - .name(member.getEmail()) - .nickname(member.getNickname()) - .role(member.getMemberType()) - .build(); - - Authentication authentication = new UsernamePasswordAuthenticationToken( - memberDetails, - null, - List.of(new SimpleGrantedAuthority(memberDetails.getRole())) - ); - - SecurityContextHolder.getContext().setAuthentication(authentication); - - HttpSession session = request.getSession(true); - session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext()); - } - - -} +//package com.oronaminc.join.global.dev; +// +//import com.oronaminc.join.member.dao.MemberRepository; +//import com.oronaminc.join.member.domain.Member; +//import com.oronaminc.join.member.domain.MemberType; +//import com.oronaminc.join.member.security.MemberDetails; +//import io.swagger.v3.oas.annotations.tags.Tag; +//import jakarta.servlet.http.HttpServletRequest; +//import jakarta.servlet.http.HttpSession; +//import lombok.RequiredArgsConstructor; +//import org.springframework.http.HttpStatus; +//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +//import org.springframework.security.core.Authentication; +//import org.springframework.security.core.authority.SimpleGrantedAuthority; +//import org.springframework.security.core.context.SecurityContextHolder; +//import org.springframework.web.bind.annotation.*; +// +//import java.util.List; +// +/// /@Profile("local") +//@RestController +//@Tag(name = "개발용 API") +//@RequestMapping("/dev") +//@RequiredArgsConstructor +//public class DevController { +// private final MemberRepository memberRepository; +// +// @PostMapping("/join") +// @ResponseStatus(HttpStatus.OK) +// public Member devJoin(@RequestBody DevJoinRequest devJoinRequest) { +// return memberRepository.save( +// Member.builder() +// .email(devJoinRequest.email()) +// .nickname(devJoinRequest.nickname()) +// .memberType(MemberType.MEMBER) +// .build() +// ); +// } +// +// @PostMapping("/login") +// @ResponseStatus(HttpStatus.OK) +// public void devLogin(@RequestBody DevLoginRequest devLoginRequest, HttpServletRequest request) { +// Member member = memberRepository.findById(devLoginRequest.memberId()) +// .orElseThrow(() -> new IllegalArgumentException("해당 ID의 사용자가 존재하지 않습니다.")); +// +// MemberDetails memberDetails = MemberDetails.builder() +// .id(member.getId()) +// .name(member.getEmail()) +// .nickname(member.getNickname()) +// .role(member.getMemberType()) +// .build(); +// +// Authentication authentication = new UsernamePasswordAuthenticationToken( +// memberDetails, +// null, +// List.of(new SimpleGrantedAuthority(memberDetails.getRole())) +// ); +// +// SecurityContextHolder.getContext().setAuthentication(authentication); +// +// HttpSession session = request.getSession(true); +// session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext()); +// } +// +// +//} diff --git a/src/main/java/com/oronaminc/join/global/dev/HealthController.java b/src/main/java/com/oronaminc/join/global/dev/HealthController.java index b03223a..b0582ff 100644 --- a/src/main/java/com/oronaminc/join/global/dev/HealthController.java +++ b/src/main/java/com/oronaminc/join/global/dev/HealthController.java @@ -1,28 +1,28 @@ -package com.oronaminc.join.global.dev; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; - -@RestController -@Tag(name = "헬스체크 API") -public class HealthController { - - @Operation(summary = "애플리케이션 헬스체크") - @ResponseStatus(HttpStatus.OK) - @GetMapping("/health") - public String health() { - return "Server is Healthy!"; - } - - @Operation(summary = "홈") - @ResponseStatus(HttpStatus.OK) - @GetMapping("/") - public String home() { - return "It's Home!"; - } -} +//package com.oronaminc.join.global.dev; +// +//import org.springframework.http.HttpStatus; +//import org.springframework.web.bind.annotation.GetMapping; +//import org.springframework.web.bind.annotation.ResponseStatus; +//import org.springframework.web.bind.annotation.RestController; +// +//import io.swagger.v3.oas.annotations.Operation; +//import io.swagger.v3.oas.annotations.tags.Tag; +// +//@RestController +//@Tag(name = "헬스체크 API") +//public class HealthController { +// +// @Operation(summary = "애플리케이션 헬스체크") +// @ResponseStatus(HttpStatus.OK) +// @GetMapping("/health") +// public String health() { +// return "Server is Healthy!"; +// } +// +// @Operation(summary = "홈") +// @ResponseStatus(HttpStatus.OK) +// @GetMapping("/") +// public String home() { +// return "It's Home!"; +// } +//} diff --git a/src/main/java/com/oronaminc/join/member/dto/SessionInfoResponse.java b/src/main/java/com/oronaminc/join/member/dto/SessionInfoResponse.java deleted file mode 100644 index 44edbd1..0000000 --- a/src/main/java/com/oronaminc/join/member/dto/SessionInfoResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.oronaminc.join.member.dto; - -import io.swagger.v3.oas.annotations.media.Schema; - -@Schema(description = "유효한 세션으로 로그인한 사용자 정보 응답 DTO") -public record SessionInfoResponse( - @Schema(description = "Kakao 회원 ID", example = "1") - Long id, - @Schema(description = "Kakao 회원 이름", example = "카카오") - String name, - @Schema(description = "Kakao 회원 닉네임", example = "kakao") - String nickname, - @Schema(description = "Kakao 회원 역할", example = "MEMBER") - String role -) { - -} diff --git a/src/main/java/com/oronaminc/join/member/security/AuthController.java b/src/main/java/com/oronaminc/join/member/security/AuthController.java index 79e6eac..5dbd2de 100644 --- a/src/main/java/com/oronaminc/join/member/security/AuthController.java +++ b/src/main/java/com/oronaminc/join/member/security/AuthController.java @@ -1,31 +1,11 @@ package com.oronaminc.join.member.security; -import static com.oronaminc.join.member.util.MemberMapper.toSessionInfoResponse; - -import java.util.List; - -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; -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.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - import com.oronaminc.join.member.dto.GuestLoginRequest; -import com.oronaminc.join.member.dto.GuestLoginResponse; import com.oronaminc.join.member.dto.KakaoLoginRequest; -import com.oronaminc.join.member.dto.KakaoLoginResponse; -import com.oronaminc.join.member.dto.SessionInfoResponse; - +import com.oronaminc.join.member.token.AuthTokenResponse; +import com.oronaminc.join.member.token.JwtUtils; +import com.oronaminc.join.member.token.LoginResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; @@ -34,45 +14,45 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; +import java.util.Map; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/auth") @Tag(name = "Auth", description = "로그인 관련 API") @RequiredArgsConstructor public class AuthController { + private final AuthService authService; @Operation( - summary = "카카오 로그인", - description = "redirect url 에 포함된 파라미터의 code와 state를 입력해주세요. 이후 모든 요청에 세션 인증이 적용됩니다." + summary = "카카오 로그인" ) @PostMapping("/kakao") @ResponseStatus(HttpStatus.OK) - public SessionInfoResponse kakaoLogin( - @RequestBody KakaoLoginRequest kakaoLoginRequest, - HttpServletRequest request + public Map kakaoLogin( + @RequestBody KakaoLoginRequest kakaoLoginRequest, + HttpServletResponse response ) { - MemberDetails memberDetails = authService.kakaoLogin(kakaoLoginRequest.code()); + LoginResponse loginResponse = authService.kakaoLogin(kakaoLoginRequest.code()); - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( - memberDetails, null, List.of(new SimpleGrantedAuthority(memberDetails.getRole())) - ); + String refreshToken = loginResponse.refreshToken(); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authentication); - SecurityContextHolder.setContext(context); + JwtUtils.addRefreshTokenCookie(response, refreshToken, + loginResponse.refreshTokenExpiresIn()); - SecurityContextHolder.getContext().setAuthentication(authentication); - - request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); - - return toSessionInfoResponse(memberDetails); + return Map.of("token", loginResponse.authTokenResponse()); } @Operation( summary = "비회원 로그인", - description = "닉네임을 입력하면 비회원 세션이 생성되고 인증이 설정됩니다. 이후 모든 요청에 세션 인증이 적용됩니다.", responses = { @ApiResponse(responseCode = "201", description = "비회원 로그인 성공"), @ApiResponse(responseCode = "400", description = "닉네임 누락 또는 유효성 검증 실패") @@ -80,36 +60,17 @@ public SessionInfoResponse kakaoLogin( ) @PostMapping("/guest") @ResponseStatus(HttpStatus.CREATED) - public SessionInfoResponse guestLogin(@RequestBody @Valid GuestLoginRequest guestLoginRequest, HttpServletRequest request) { - MemberDetails guest = authService.loadGuest(guestLoginRequest); - - Authentication authentication = new UsernamePasswordAuthenticationToken( - guest, null, List.of(new SimpleGrantedAuthority(guest.getRole())) - ); - - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(authentication); - SecurityContextHolder.setContext(context); + public Map guestLogin( + @RequestBody @Valid GuestLoginRequest guestLoginRequest, + HttpServletResponse response) { + LoginResponse loginResponse = authService.loadGuest(guestLoginRequest); - request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); + String refreshToken = loginResponse.refreshToken(); - return toSessionInfoResponse(guest); - } - - @Operation( - summary = "현재 세션 사용자 정보 조회", - description = "로그인한 사용자의 세션 정보를 반환합니다. 로그인하지 않은 경우 403 또는 401이 발생합니다.", - responses = { - @ApiResponse(responseCode = "200", description = "세션 사용자 정보 조회 성공"), - @ApiResponse(responseCode = "401", description = "로그인되지 않은 사용자"), - @ApiResponse(responseCode = "403", description = "인증된 사용자 아님") - } - ) - @GetMapping("/session") - @ResponseStatus(HttpStatus.OK) - public SessionInfoResponse getSessionInfo(@AuthenticationPrincipal MemberDetails memberDetails) { + JwtUtils.addRefreshTokenCookie(response, refreshToken, + loginResponse.refreshTokenExpiresIn()); - return toSessionInfoResponse(memberDetails); + return Map.of("token", loginResponse.authTokenResponse()); } @Operation( diff --git a/src/main/java/com/oronaminc/join/member/security/AuthService.java b/src/main/java/com/oronaminc/join/member/security/AuthService.java index 3d64d9f..dbc5667 100644 --- a/src/main/java/com/oronaminc/join/member/security/AuthService.java +++ b/src/main/java/com/oronaminc/join/member/security/AuthService.java @@ -1,9 +1,21 @@ package com.oronaminc.join.member.security; -import static com.oronaminc.join.member.util.MemberMapper.*; +import static com.oronaminc.join.member.util.MemberMapper.toGuestMember; +import com.oronaminc.join.member.dao.MemberRepository; +import com.oronaminc.join.member.domain.Member; +import com.oronaminc.join.member.dto.GuestLoginRequest; +import com.oronaminc.join.member.dto.KakaoUserResponse; +import com.oronaminc.join.member.service.MemberReader; +import com.oronaminc.join.member.token.AuthTokenResponse; +import com.oronaminc.join.member.token.JwtMemberInfo; +import com.oronaminc.join.member.token.JwtTokenProvider; +import com.oronaminc.join.member.token.LoginResponse; +import com.oronaminc.join.member.token.TokenPair; +import com.oronaminc.join.member.util.MemberMapper; import java.util.Map; - +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -17,22 +29,14 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; -import com.oronaminc.join.member.dao.MemberRepository; -import com.oronaminc.join.member.domain.Member; -import com.oronaminc.join.member.dto.GuestLoginRequest; -import com.oronaminc.join.member.dto.KakaoUserResponse; -import com.oronaminc.join.member.service.MemberReader; -import com.oronaminc.join.member.util.MemberMapper; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; - @Service @Slf4j @RequiredArgsConstructor public class AuthService extends DefaultOAuth2UserService { + private final MemberRepository memberRepository; private final MemberReader memberReader; + private final JwtTokenProvider jwtTokenProvider; private final RestTemplate restTemplate = new RestTemplate(); @@ -67,24 +71,41 @@ public class AuthService extends DefaultOAuth2UserService { // } @Transactional - public MemberDetails loadGuest(GuestLoginRequest guestLoginRequest) { + public LoginResponse loadGuest(GuestLoginRequest guestLoginRequest) { Member guest = toGuestMember(guestLoginRequest); memberRepository.save(guest); guest.registerGuest(); - return toGuestMemberDetails(guest); + TokenPair tokenPair = jwtTokenProvider.generateTokenPair( + new JwtMemberInfo(guest.getId(), guest.getNickname(), guest.getMemberType())); + + AuthTokenResponse authTokenResponse = new AuthTokenResponse(tokenPair.accessToken(), + tokenPair.accessTokenExpiresIn(), guest.getId(), + guest.getNickname(), guest.getMemberType()); + + return new LoginResponse(authTokenResponse, tokenPair.refreshToken(), + tokenPair.refreshTokenExpiresIn()); } @Transactional - public MemberDetails kakaoLogin(String code) { + public LoginResponse kakaoLogin(String code) { String accessToken = getAccessToken(code); KakaoUserResponse kakaoUser = getUserInfo(accessToken); Member member = memberRepository.findByEmail(kakaoUser.email()) - .orElseGet(() -> memberRepository.save(MemberMapper.toNewKakaoMember(kakaoUser))); + .orElseGet(() -> memberRepository.save(MemberMapper.toNewKakaoMember(kakaoUser))); + + TokenPair tokenPair = jwtTokenProvider.generateTokenPair( + new JwtMemberInfo(member.getId(), member.getNickname(), member.getMemberType())); + + AuthTokenResponse authTokenResponse = new AuthTokenResponse(tokenPair.accessToken(), + tokenPair.accessTokenExpiresIn(), member.getId(), + member.getNickname(), member.getMemberType()); + + return new LoginResponse(authTokenResponse, tokenPair.refreshToken(), + tokenPair.refreshTokenExpiresIn()); - return toOAuth2MemberDetails(member); } private String getAccessToken(String code) { @@ -112,7 +133,8 @@ private KakaoUserResponse getUserInfo(String accessToken) { HttpEntity entity = new HttpEntity<>(headers); - ResponseEntity response = restTemplate.exchange(USER_INFO_URI, HttpMethod.GET, entity, Map.class); + ResponseEntity response = restTemplate.exchange(USER_INFO_URI, HttpMethod.GET, entity, + Map.class); Map attributes = response.getBody(); diff --git a/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java b/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java index 4f3d00d..c37b66f 100644 --- a/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java +++ b/src/main/java/com/oronaminc/join/member/security/SecurityConfig.java @@ -1,12 +1,8 @@ package com.oronaminc.join.member.security; -import static org.springframework.security.config.Customizer.*; - -import java.util.List; - +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; 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.configurers.AbstractHttpConfigurer; @@ -15,10 +11,12 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import lombok.RequiredArgsConstructor; +import java.util.List; + +import static org.springframework.security.config.Customizer.withDefaults; @Configuration -@Profile("!test") +//@Profile("!test") @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { @@ -30,29 +28,29 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .authorizeHttpRequests(auth -> auth - // .requestMatchers( - // "/api/auth/guest", - // "/api/auth/kakao", - // "/login" - // ) - // .anonymous() - .requestMatchers( - "/swagger-ui/**", - "/swagger-resources/**", - "/v3/api-docs/**", - "/oauth2/authorization/**", - "/login/oauth2/code/kakao", - "/api/auth/logout", - "/dev/**", - "/ws/**", - "/api/auth/guest", - "/api/auth/kakao", - "/login", - "/health" - ) - .permitAll() - .anyRequest() - .authenticated() + // .requestMatchers( + // "/api/auth/guest", + // "/api/auth/kakao", + // "/login" + // ) + // .anonymous() + .requestMatchers( + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/oauth2/**", + "/login/oauth2/code/kakao", + "/api/auth/logout", + "/dev/**", + "/ws/**", + "/api/auth/guest", + "/api/auth/kakao", + "/login" +// "/health" + ) + .permitAll() + .anyRequest() + .authenticated() ) .formLogin(AbstractHttpConfigurer::disable) .oauth2Login(oauth2 -> oauth2.userInfoEndpoint(userInfo -> userInfo diff --git a/src/main/java/com/oronaminc/join/member/token/AuthTokenResponse.java b/src/main/java/com/oronaminc/join/member/token/AuthTokenResponse.java new file mode 100644 index 0000000..56b58fc --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/AuthTokenResponse.java @@ -0,0 +1,13 @@ +package com.oronaminc.join.member.token; + +import com.oronaminc.join.member.domain.MemberType; + +public record AuthTokenResponse( + String accessToken, + long accessTokenExpiresIn, + Long memberId, + String nickname, + MemberType role +) { + +} diff --git a/src/main/java/com/oronaminc/join/member/token/JwtConfiguration.java b/src/main/java/com/oronaminc/join/member/token/JwtConfiguration.java new file mode 100644 index 0000000..b6d397a --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/JwtConfiguration.java @@ -0,0 +1,11 @@ +package com.oronaminc.join.member.token; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtConfiguration( + String secret, + Long accessTokenExpiration, + Long refreshTokenExpiration +) { +} diff --git a/src/main/java/com/oronaminc/join/member/token/JwtMemberInfo.java b/src/main/java/com/oronaminc/join/member/token/JwtMemberInfo.java new file mode 100644 index 0000000..d876e4b --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/JwtMemberInfo.java @@ -0,0 +1,10 @@ +package com.oronaminc.join.member.token; + +import com.oronaminc.join.member.domain.MemberType; + +public record JwtMemberInfo( + Long memberId, + String nickname, + MemberType role +) { +} diff --git a/src/main/java/com/oronaminc/join/member/token/JwtTokenProvider.java b/src/main/java/com/oronaminc/join/member/token/JwtTokenProvider.java new file mode 100644 index 0000000..f774e78 --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/JwtTokenProvider.java @@ -0,0 +1,72 @@ +package com.oronaminc.join.member.token; + +import com.oronaminc.join.member.domain.MemberType; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.util.Date; +import javax.crypto.SecretKey; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final JwtConfiguration jwtConfiguration; + + public TokenPair generateTokenPair(JwtMemberInfo jwtMemberInfo) { + + String accessToken = issueAccessToken(jwtMemberInfo); + String refreshToken = issueRefreshToken(jwtMemberInfo); + + return new TokenPair(accessToken, refreshToken, + jwtConfiguration.accessTokenExpiration(), jwtConfiguration.refreshTokenExpiration()); + } + + private String issueAccessToken(JwtMemberInfo jwtMemberInfo) { + return issue(jwtMemberInfo, jwtConfiguration.accessTokenExpiration()); + } + + private String issueRefreshToken(JwtMemberInfo jwtMemberInfo) { + return issue(jwtMemberInfo, jwtConfiguration.refreshTokenExpiration()); + } + + private String issue(JwtMemberInfo jwtMemberInfo, Long expTime) { + return Jwts.builder() + .subject(jwtMemberInfo.memberId().toString()) + .claim("nickname", jwtMemberInfo.nickname()) + .claim("role", jwtMemberInfo.role()) + .issuedAt(new Date()) + .expiration(new Date(new Date().getTime() + expTime)) + .signWith(getSecretKey(), Jwts.SIG.HS256) + .compact(); + } + + public TokenBody parseClaims(String token) { + + Jws claims = Jwts.parser() + .verifyWith(getSecretKey()) + .build() + .parseSignedClaims(token); + + Claims payload = claims.getPayload(); + + Long memberId = Long.parseLong(payload.getSubject()); + + return new TokenBody( + memberId, + payload.get("nickname").toString(), + MemberType.valueOf(payload.get("role").toString()), + payload.getIssuedAt(), + payload.getExpiration() + ); + } + + private SecretKey getSecretKey() { + return Keys.hmacShaKeyFor(jwtConfiguration.secret().getBytes()); + } +} \ No newline at end of file diff --git a/src/main/java/com/oronaminc/join/member/token/JwtUtils.java b/src/main/java/com/oronaminc/join/member/token/JwtUtils.java new file mode 100644 index 0000000..693c79f --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/JwtUtils.java @@ -0,0 +1,29 @@ +package com.oronaminc.join.member.token; + +import jakarta.servlet.http.HttpServletResponse; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JwtUtils { + + public static long toSeconds(long millis) { + return millis / 1000; + } + + public static void addRefreshTokenCookie(HttpServletResponse response, String refreshToken, + long expiresIn) { + ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", refreshToken) + .httpOnly(true) + .secure(true) + .path("/") + .sameSite("Strict") + .maxAge(JwtUtils.toSeconds(expiresIn)) + .build(); + + response.setHeader(HttpHeaders.SET_COOKIE, refreshCookie.toString()); + } + +} diff --git a/src/main/java/com/oronaminc/join/member/token/LoginResponse.java b/src/main/java/com/oronaminc/join/member/token/LoginResponse.java new file mode 100644 index 0000000..f1e32da --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/LoginResponse.java @@ -0,0 +1,9 @@ +package com.oronaminc.join.member.token; + +public record LoginResponse( + AuthTokenResponse authTokenResponse, + String refreshToken, + long refreshTokenExpiresIn +) { + +} diff --git a/src/main/java/com/oronaminc/join/member/token/TokenBody.java b/src/main/java/com/oronaminc/join/member/token/TokenBody.java new file mode 100644 index 0000000..98d5df2 --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/TokenBody.java @@ -0,0 +1,14 @@ +package com.oronaminc.join.member.token; + +import com.oronaminc.join.member.domain.MemberType; + +import java.util.Date; + +public record TokenBody( + Long memberId, + String nickname, + MemberType role, + Date issuedAt, + Date expiration +) { +} diff --git a/src/main/java/com/oronaminc/join/member/token/TokenPair.java b/src/main/java/com/oronaminc/join/member/token/TokenPair.java new file mode 100644 index 0000000..928803c --- /dev/null +++ b/src/main/java/com/oronaminc/join/member/token/TokenPair.java @@ -0,0 +1,9 @@ +package com.oronaminc.join.member.token; + +public record TokenPair( + String accessToken, + String refreshToken, + long accessTokenExpiresIn, + long refreshTokenExpiresIn +) { +} diff --git a/src/main/java/com/oronaminc/join/member/util/MemberMapper.java b/src/main/java/com/oronaminc/join/member/util/MemberMapper.java index 831d3c1..bc85917 100644 --- a/src/main/java/com/oronaminc/join/member/util/MemberMapper.java +++ b/src/main/java/com/oronaminc/join/member/util/MemberMapper.java @@ -1,78 +1,41 @@ package com.oronaminc.join.member.util; -import com.oronaminc.join.member.dto.SessionInfoResponse; -import java.util.Map; - import com.oronaminc.join.member.domain.Member; import com.oronaminc.join.member.domain.MemberType; import com.oronaminc.join.member.dto.GuestLoginRequest; import com.oronaminc.join.member.dto.KakaoUserResponse; -import com.oronaminc.join.member.security.MemberDetails; - +import java.util.Map; import lombok.AccessLevel; import lombok.AllArgsConstructor; @AllArgsConstructor(access = AccessLevel.PRIVATE) public class MemberMapper { - public static MemberDetails toOAuth2MemberDetails(Member member) { - return MemberDetails.builder() - .id(member.getId()) - .name(member.getEmail()) - .nickname(member.getNickname()) - .role(member.getMemberType()) - .build(); - } - - public static MemberDetails toGuestMemberDetails(Member guest) { - return MemberDetails.builder() - .id(guest.getId()) - .name(guest.getEmail()) - .nickname(guest.getNickname()) - .role(MemberType.GUEST) - .build(); - } public static Member toGuestMember(GuestLoginRequest guestLoginRequest) { return Member.builder() - .email(null) - .nickname(guestLoginRequest.nickname()) - .profileImage(null) - .memberType(MemberType.GUEST) - .build(); - } - - public static Member toKakaoMember(Map kakaoAccount, Map profile) { - return Member.builder() - .email(kakaoAccount.get("email").toString()) - .nickname(profile.get("nickname").toString()) - .profileImage(profile.get("profile_image_url").toString()) - .memberType(MemberType.MEMBER) - .build(); + .email(null) + .nickname(guestLoginRequest.nickname()) + .profileImage(null) + .memberType(MemberType.GUEST) + .build(); } public static Member toNewKakaoMember(KakaoUserResponse kakaoUser) { return Member.builder() - .email(kakaoUser.email()) - .nickname(kakaoUser.nickname()) - .profileImage(kakaoUser.profileImageUrl()) - .memberType(MemberType.MEMBER) - .build(); + .email(kakaoUser.email()) + .nickname(kakaoUser.nickname()) + .profileImage(kakaoUser.profileImageUrl()) + .memberType(MemberType.MEMBER) + .build(); } - public static KakaoUserResponse toKakaoUserResponse(Map kakaoAccount, Map profile) { + public static KakaoUserResponse toKakaoUserResponse(Map kakaoAccount, + Map profile) { return KakaoUserResponse.builder() - .email((String) kakaoAccount.get("email")) - .nickname((String) profile.get("nickname")) - .profileImageUrl((String) profile.get("profile_image_url")) - .build(); + .email((String) kakaoAccount.get("email")) + .nickname((String) profile.get("nickname")) + .profileImageUrl((String) profile.get("profile_image_url")) + .build(); } - public static SessionInfoResponse toSessionInfoResponse(MemberDetails memberDetails) { - return new SessionInfoResponse( - memberDetails.getId(), - memberDetails.getName(), - memberDetails.getNickname(), - memberDetails.getRole() - ); - } }