-
Notifications
You must be signed in to change notification settings - Fork 0
[KW-33] feat/admin-auth #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2cbca76
4197d3f
d4571fc
090bc11
5b5c8e6
7ced5c6
9112e78
a366812
60c4058
480d95a
ea14b05
edec1dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.doubleo.adminservice.domain.admin.repository; | ||
|
|
||
| import com.doubleo.adminservice.domain.admin.domain.Admin; | ||
| import java.util.Optional; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.stereotype.Repository; | ||
|
|
||
| @Repository | ||
| public interface AdminRepository extends JpaRepository<Admin, Long> { | ||
| Optional<Admin> findAdminByUsername(String username); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.doubleo.adminservice.domain.auth.controller; | ||
|
|
||
| import com.doubleo.adminservice.domain.auth.dto.AccessTokenDto; | ||
| import com.doubleo.adminservice.domain.auth.dto.RefreshTokenDto; | ||
| import com.doubleo.adminservice.domain.auth.dto.request.LoginRequest; | ||
| import com.doubleo.adminservice.domain.auth.dto.response.LoginResponse; | ||
| import com.doubleo.adminservice.domain.auth.service.AuthService; | ||
| import com.doubleo.adminservice.domain.auth.service.JwtTokenService; | ||
| import com.doubleo.adminservice.global.util.CookieUtil; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.servlet.http.Cookie; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.HttpHeaders; | ||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseCookie; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import org.springframework.web.util.WebUtils; | ||
|
|
||
| @Tag(name = "1-2. Auth API", description = "ํ์ ๋ก๊ทธ์ธ/๋ก๊ทธ์์/Refresh Token ๊ด๋ จ API") | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| @RequestMapping("/auth") | ||
| public class AuthController { | ||
|
|
||
| private final AuthService authService; | ||
| private final CookieUtil cookieUtil; | ||
| private final JwtTokenService jwtTokenService; | ||
|
|
||
| @Operation(summary = "ํ์ ๋ก๊ทธ์ธ", description = "ํ์ ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํฉ๋๋ค.") | ||
| @PostMapping("/login") | ||
| public ResponseEntity<LoginResponse> adminLogin(@RequestBody LoginRequest request) { | ||
| LoginResponse response = authService.loginAdmin(request); | ||
| String refreshToken = response.refreshToken(); | ||
| HttpHeaders headers = cookieUtil.generateRefreshTokenCookie(refreshToken); | ||
|
|
||
| return ResponseEntity.ok().headers(headers).body(response); | ||
| } | ||
|
|
||
| @Operation(summary = "ํ์ ๋ก๊ทธ์์", description = "ํ์ ๋ก๊ทธ์์์ ์ฒ๋ฆฌํฉ๋๋ค.") | ||
| @PostMapping("/logout") | ||
| public ResponseEntity<Void> adminLogout( | ||
| @RequestHeader(HttpHeaders.AUTHORIZATION) String authorizationHeader, | ||
| @RequestHeader("X-Admin-Id") Long adminId, | ||
| HttpServletResponse response) { | ||
| authService.logoutAdmin(authorizationHeader, adminId); | ||
|
|
||
| ResponseCookie clearCookie = | ||
| ResponseCookie.from("refreshToken", "") | ||
| .httpOnly(true) | ||
| .secure(true) | ||
| .path("/") | ||
| .maxAge(0) | ||
| .sameSite("Strict") | ||
| .build(); | ||
| response.addHeader(HttpHeaders.SET_COOKIE, clearCookie.toString()); | ||
|
|
||
| return ResponseEntity.noContent().build(); | ||
| } | ||
|
|
||
| @Operation( | ||
| summary = "Access Token ์ฌ๋ฐ๊ธ", | ||
| description = "์ ํจํ RefreshToken ์ ํตํด AccessToken ์ ์ฌ๋ฐ๊ธํฉ๋๋ค.") | ||
| @PostMapping("/reissue") | ||
| public ResponseEntity<Void> tokenReissue( | ||
| HttpServletRequest request, HttpServletResponse response) { | ||
| String oldAccessToken = extractAccessTokenFromHeader(request); | ||
| String refreshToken = extractRefreshTokenFromCookie(request); | ||
|
|
||
| RefreshTokenDto refreshTokenDto = jwtTokenService.retrieveRefreshToken(refreshToken); | ||
| if (refreshTokenDto == null) { | ||
| return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); | ||
| } | ||
| AccessTokenDto newAccessTokenDto = | ||
| jwtTokenService.reissueAccessTokenIfExpired(oldAccessToken); | ||
| response.addHeader( | ||
| HttpHeaders.AUTHORIZATION, "Bearer " + newAccessTokenDto.accessTokenValue()); | ||
|
|
||
| return ResponseEntity.ok().build(); | ||
| } | ||
|
|
||
| private String extractRefreshTokenFromCookie(HttpServletRequest request) { | ||
| Cookie cookie = WebUtils.getCookie(request, "refreshToken"); | ||
| return (cookie != null) ? cookie.getValue() : null; | ||
| } | ||
|
|
||
| private String extractAccessTokenFromHeader(HttpServletRequest request) { | ||
| String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION); | ||
| if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { | ||
| return authorizationHeader.substring(7); | ||
| } | ||
| return null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.doubleo.adminservice.domain.auth.domain; | ||
|
|
||
| import lombok.AccessLevel; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import org.springframework.data.annotation.Id; | ||
| import org.springframework.data.redis.core.RedisHash; | ||
| import org.springframework.data.redis.core.TimeToLive; | ||
|
|
||
| @Getter | ||
| @RedisHash("blacklistToken") | ||
| public class BlackListToken { | ||
|
|
||
| @Id private final String token; | ||
|
|
||
| @TimeToLive private final long ttl; | ||
|
|
||
| @Builder(access = AccessLevel.PRIVATE) | ||
| private BlackListToken(String token, long ttl) { | ||
| this.token = token; | ||
| this.ttl = ttl; | ||
| } | ||
|
|
||
| public static BlackListToken createBlackListToken(String token, Long ttl) { | ||
| return builder().token(token).ttl(ttl).build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.doubleo.adminservice.domain.auth.domain; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import org.springframework.data.annotation.Id; | ||
| import org.springframework.data.redis.core.RedisHash; | ||
| import org.springframework.data.redis.core.TimeToLive; | ||
|
|
||
| @Getter | ||
| @RedisHash(value = "refreshToken") | ||
| public class RefreshToken { | ||
|
|
||
| @Id private final Long adminId; | ||
|
|
||
| private final String token; | ||
|
|
||
| @TimeToLive private final long ttl; | ||
|
|
||
| @Builder | ||
| private RefreshToken(Long adminId, String token, long ttl) { | ||
| this.adminId = adminId; | ||
| this.token = token; | ||
| this.ttl = ttl; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.doubleo.adminservice.domain.auth.dto; | ||
|
|
||
| public record AccessTokenDto(Long adminId, String accessTokenValue) { | ||
| public static AccessTokenDto of(Long adminId, String accessTokenValue) { | ||
| return new AccessTokenDto(adminId, accessTokenValue); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.doubleo.adminservice.domain.auth.dto; | ||
|
|
||
| public record RefreshTokenDto(Long adminId, String refreshTokenValue, Long ttl) { | ||
| public static RefreshTokenDto of(Long adminId, String refreshTokenValue, Long ttl) { | ||
| return new RefreshTokenDto(adminId, refreshTokenValue, ttl); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package com.doubleo.adminservice.domain.auth.dto.request; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public record LoginRequest( | ||
willjsw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @Schema(description = "๊ด๋ฆฌ์ ์์ด๋", example = "username") String username, | ||
| @Schema(description = "๊ด๋ฆฌ์ ํจ์ค์๋", example = "pw12345") String password) {} | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package com.doubleo.adminservice.domain.auth.dto.response; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonIgnore; | ||
|
|
||
| public record LoginResponse(String accessToken, @JsonIgnore String refreshToken) { | ||
| public static LoginResponse of(String accessToken, String refreshToken) { | ||
| return new LoginResponse(accessToken, refreshToken); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.doubleo.adminservice.domain.auth.repository; | ||
|
|
||
| import com.doubleo.adminservice.domain.auth.domain.BlackListToken; | ||
| import org.springframework.data.repository.CrudRepository; | ||
|
|
||
| public interface BlackListTokenRepository extends CrudRepository<BlackListToken, String> {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.doubleo.adminservice.domain.auth.repository; | ||
|
|
||
| import com.doubleo.adminservice.domain.auth.domain.RefreshToken; | ||
| import org.springframework.data.repository.CrudRepository; | ||
|
|
||
| public interface RefreshTokenRepository extends CrudRepository<RefreshToken, Long> {} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.doubleo.adminservice.domain.auth.service; | ||
|
|
||
| import com.doubleo.adminservice.domain.auth.dto.request.LoginRequest; | ||
| import com.doubleo.adminservice.domain.auth.dto.response.LoginResponse; | ||
|
|
||
| public interface AuthService { | ||
|
|
||
| LoginResponse loginAdmin(LoginRequest request); | ||
|
|
||
| void logoutAdmin(String accessTokenValue, Long adminId); | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,54 @@ | ||||||
| package com.doubleo.adminservice.domain.auth.service; | ||||||
|
|
||||||
| import com.doubleo.adminservice.domain.admin.domain.Admin; | ||||||
| import com.doubleo.adminservice.domain.admin.repository.AdminRepository; | ||||||
| import com.doubleo.adminservice.domain.auth.dto.request.LoginRequest; | ||||||
| import com.doubleo.adminservice.domain.auth.dto.response.LoginResponse; | ||||||
| import com.doubleo.adminservice.domain.auth.repository.RefreshTokenRepository; | ||||||
| import com.doubleo.adminservice.global.exception.CommonException; | ||||||
| import com.doubleo.adminservice.global.exception.errorcode.AdminErrorCode; | ||||||
| import lombok.RequiredArgsConstructor; | ||||||
| import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; | ||||||
| import org.springframework.stereotype.Service; | ||||||
|
|
||||||
| @Service | ||||||
| @RequiredArgsConstructor | ||||||
| public class AuthServiceImpl implements AuthService { | ||||||
|
|
||||||
| private final AdminRepository adminRepository; | ||||||
| private final RefreshTokenRepository refreshTokenRepository; | ||||||
| private final JwtTokenService jwtTokenService; | ||||||
| private final BCryptPasswordEncoder encoder; | ||||||
|
|
||||||
| public LoginResponse loginAdmin(LoginRequest request) { | ||||||
| Admin admin = validateAdminByEmail(request.username()); | ||||||
| if (!encoder.matches(request.password(), admin.getPassword())) { | ||||||
| throw new CommonException(AdminErrorCode.ADMIN_NOT_FOUND); | ||||||
|
||||||
| throw new CommonException(AdminErrorCode.ADMIN_NOT_FOUND); | |
| throw new CommonException(AdminErrorCode.INVALID_PASSWORD); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment: ๋น๋ฐ๋ฒํธ&์์ด๋ ์ค ์ด๋ ๊ฒ์ด ์๋ชป๋์๋์ง, ์ธ๋ถ ๊ณต๊ฒฉ์์๊ฒ ์์ธํ ๋ ธ์ถํ์ง ์๋๊ฒ ์ข๋ค๊ณ ์๊ฐํด์ ๋ก๊ทธ์ธ ๋ก์ง์ ํํด ADMIN_NOT_FOUND ์ฝ๋๋ก ํตํฉํด ๊ฑธ์ด๋์๋๋ฐ, ๋ฆฌ๋ทฐํ์๋ ๋ถ๋ค์ด INVALID_PASSWORD๋ก ๊ตฌ์ฒดํํ๋ ๊ฒ์ด ์ข๊ฒ ๋ค๋ ์๊ฒฌ ๋ด์๋ฉด ๋ฐ์ํด์ ์์ ํ๊ฒ ์ต๋๋ค.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ADMIN_NOT_FOUND๋ก ํตํฉํ๋ ๊ฒ์ด ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package com.doubleo.adminservice.domain.auth.service; | ||
|
|
||
| import com.doubleo.adminservice.domain.auth.dto.AccessTokenDto; | ||
| import com.doubleo.adminservice.domain.auth.dto.RefreshTokenDto; | ||
|
|
||
| public interface JwtTokenService { | ||
|
|
||
| // AccessToken DTO ์์ฑ | ||
| AccessTokenDto createAccessTokenDto(Long adminId); | ||
|
|
||
| // AccessToken ์์ฑ | ||
| String createAccessToken(Long adminId); | ||
|
|
||
| // RefreshToken ์์ฑ | ||
| String createRefreshToken(Long adminId); | ||
|
|
||
| // DB ์ ์ฅ๋ RefreshToken ์กฐํ ๋ฐ ๊ฒ์ฆ | ||
| RefreshTokenDto retrieveRefreshToken(String refreshTokenValue); | ||
|
|
||
| // AccessToken ๋ง๋ฃ ์ฌ๋ถ ๊ฒ์ฆ ํ ์ฌ๋ฐ๊ธ | ||
| AccessTokenDto reissueAccessTokenIfExpired(String accessTokenValue); | ||
|
|
||
| // ์ฌ์ฉํ์ง ์๋ AccessToken BlackList ๋ฑ๋ก | ||
| void putAccessTokenOnBlackList(String accessTokenValue); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.