-
Notifications
You must be signed in to change notification settings - Fork 0
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
Feature/member #1
Changes from 5 commits
a81fbcf
4681f02
4929524
f01bae9
c731216
4e23bb1
114c3e0
54dfe3b
69f4a7f
c073c4d
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,28 @@ | ||
dependencies { | ||
// database | ||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||
implementation 'com.mysql:mysql-connector-j' | ||
|
||
//encryption | ||
implementation 'com.google.guava:guava:31.1-jre' | ||
|
||
// validator | ||
implementation 'org.springframework.boot:spring-boot-starter-validation' | ||
|
||
//jwt | ||
implementation 'io.jsonwebtoken:jjwt:0.9.1' | ||
implementation 'com.sun.xml.bind:jaxb-impl:4.0.1' | ||
implementation 'com.sun.xml.bind:jaxb-core:4.0.1' | ||
implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' | ||
} | ||
|
||
bootJar { | ||
archivesBaseName = 'Admin' | ||
archiveFileName = 'admin.jar' | ||
destinationDirectory = file(project.rootProject.projectDir) | ||
enabled = true | ||
} | ||
|
||
jar { | ||
enabled = false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package co.kr.ticketing.member; | ||
|
||
import org.springframework.boot.SpringApplication; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing; | ||
|
||
@EnableJpaAuditing | ||
@SpringBootApplication | ||
public class MemberApplication { | ||
|
||
public static void main(String[] args) { | ||
SpringApplication.run(MemberApplication.class, args); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package co.kr.ticketing.member.auth.aop; | ||
|
||
import java.util.Arrays; | ||
|
||
import org.aspectj.lang.JoinPoint; | ||
import org.aspectj.lang.annotation.Aspect; | ||
import org.aspectj.lang.annotation.Before; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.context.request.RequestContextHolder; | ||
import org.springframework.web.context.request.ServletRequestAttributes; | ||
|
||
import co.kr.ticketing.member.auth.config.AuthConfig; | ||
import co.kr.ticketing.member.auth.util.TokenUtil; | ||
import co.kr.ticketing.member.exception.UnAuthorized; | ||
import jakarta.servlet.http.Cookie; | ||
import lombok.AccessLevel; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.experimental.FieldDefaults; | ||
import lombok.extern.slf4j.Slf4j; | ||
|
||
@Slf4j | ||
@Aspect | ||
@Component | ||
@RequiredArgsConstructor | ||
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) | ||
public class AuthAspect { | ||
TokenUtil tokenUtil; | ||
|
||
@Before("@annotation(LoginCheck)") | ||
public void loginCheck(JoinPoint jp) { | ||
String token = getToken(); | ||
if (!tokenUtil.isValidToken(token)) { | ||
throw new UnAuthorized(); | ||
} | ||
} | ||
|
||
private String getToken() { | ||
Cookie[] cookies = ((ServletRequestAttributes)(RequestContextHolder.currentRequestAttributes())).getRequest() | ||
.getCookies(); | ||
|
||
return cookies == null ? "" : Arrays.stream(cookies) | ||
.filter(cookie -> cookie.getName().equals(AuthConfig.LOGIN_COOKIE_NAME)) | ||
.findAny() | ||
.orElseThrow(UnAuthorized::new) | ||
.getValue(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package co.kr.ticketing.member.auth.aop; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.METHOD) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface LoginCheck { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package co.kr.ticketing.member.auth.config; | ||
|
||
import lombok.experimental.UtilityClass; | ||
|
||
@UtilityClass | ||
public class AuthConfig { | ||
public static String SECRET_KEY = "test_secret_key"; | ||
public static String PHONE_NUMBER = "phoneNumber"; | ||
public static int TOKEN_VALID_TIME = 86400; | ||
public static String LOGIN_COOKIE_NAME = "token"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package co.kr.ticketing.member.auth.util; | ||
|
||
import java.time.LocalDateTime; | ||
import java.time.ZoneId; | ||
import java.time.ZonedDateTime; | ||
import java.util.Date; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import co.kr.ticketing.member.auth.config.AuthConfig; | ||
import io.jsonwebtoken.JwtException; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import lombok.AccessLevel; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.experimental.FieldDefaults; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) | ||
public class JwtUtil { | ||
|
||
public String generateJwt(Map<String, Object> valueMap, int expireSeconds) { | ||
return generateToken(valueMap, expireSeconds, AuthConfig.SECRET_KEY); | ||
} | ||
|
||
public boolean isValidJwt(String jwt) { | ||
if (jwt.isEmpty()) { | ||
return false; | ||
} | ||
|
||
try { | ||
Jwts.parser() | ||
.setSigningKey(AuthConfig.SECRET_KEY.getBytes()) | ||
.parseClaimsJws(jwt) | ||
.getBody(); | ||
} catch (JwtException e) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private String generateToken(Map<String, Object> valueMap, int expireSeconds, String secretKey) { | ||
Map<String, Object> headers = new HashMap<>(); | ||
headers.put("typ", "JWT"); | ||
headers.put("alg", "HS256"); | ||
|
||
Map<String, Object> payload = new HashMap<>(); | ||
payload.putAll(valueMap); | ||
|
||
return Jwts.builder() | ||
.setHeader(headers) | ||
.setClaims(payload) | ||
.setIssuedAt( | ||
Date.from( | ||
ZonedDateTime.now().toInstant() | ||
)) | ||
.setExpiration( | ||
getDate(expireSeconds) | ||
) | ||
.signWith(SignatureAlgorithm.HS256, secretKey.getBytes()).compact(); | ||
} | ||
|
||
private Date getDate(int seconds) { | ||
LocalDateTime localDateTime = LocalDateTime.now().plusSeconds(seconds); | ||
return Date.from( | ||
localDateTime.atZone( | ||
ZoneId.systemDefault() | ||
) | ||
.toInstant() | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package co.kr.ticketing.member.auth.util; | ||
|
||
import java.util.Map; | ||
|
||
import org.springframework.stereotype.Component; | ||
|
||
import co.kr.ticketing.member.auth.config.AuthConfig; | ||
import co.kr.ticketing.member.domain.model.Member; | ||
import lombok.AccessLevel; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.experimental.FieldDefaults; | ||
|
||
@Component | ||
@RequiredArgsConstructor(access = AccessLevel.PROTECTED) | ||
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) | ||
public class TokenUtil { | ||
JwtUtil jwtUtil; | ||
|
||
public String generateToken(Member member) { | ||
Map<String, Object> payload = Map.of( | ||
AuthConfig.PHONE_NUMBER, member.getPhoneNumber() | ||
); | ||
|
||
return jwtUtil.generateJwt(payload, AuthConfig.TOKEN_VALID_TIME); | ||
} | ||
|
||
public boolean isValidToken(String token) { | ||
return jwtUtil.isValidJwt(token); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package co.kr.ticketing.member.controller; | ||
|
||
import org.springframework.http.ResponseCookie; | ||
import org.springframework.validation.annotation.Validated; | ||
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.RestController; | ||
|
||
import co.kr.ticketing.member.auth.aop.LoginCheck; | ||
import co.kr.ticketing.member.auth.config.AuthConfig; | ||
import co.kr.ticketing.member.auth.util.TokenUtil; | ||
import co.kr.ticketing.member.controller.request.LoginRequest; | ||
import co.kr.ticketing.member.controller.request.SignUpRequest; | ||
import co.kr.ticketing.member.domain.model.Member; | ||
import co.kr.ticketing.member.usecase.LoginUseCase; | ||
import co.kr.ticketing.member.usecase.SignUpUseCase; | ||
import co.kr.ticketing.member.util.CookieUtil; | ||
import co.kr.ticketing.member.util.dto.CookieDto; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import jakarta.validation.Valid; | ||
import lombok.AccessLevel; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.experimental.FieldDefaults; | ||
|
||
@Validated | ||
@RestController | ||
@RequestMapping(value = "/members") | ||
@RequiredArgsConstructor | ||
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) | ||
public class MemberController { | ||
CookieUtil cookieUtil; | ||
TokenUtil tokenUtil; | ||
SignUpUseCase signupUseCase; | ||
LoginUseCase loginUseCase; | ||
|
||
@PostMapping | ||
public void SignUp(@RequestBody @Valid SignUpRequest request) { | ||
signupUseCase.execute(request); | ||
} | ||
|
||
@PostMapping("/login") | ||
public void Login(@RequestBody @Valid LoginRequest request, HttpServletResponse response) { | ||
Member member = loginUseCase.execute(request); | ||
|
||
ResponseCookie responseCookie = createLoginTokenCookie(member); | ||
|
||
response.addHeader("Set-Cookie", responseCookie.toString()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 응답이 200 으로만 나가는게 확장성이 떨어져보입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. void 로 주는 부분들 전체 포함입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ResponseDto를 사용해서 반환하도록 수정했습니다! |
||
} | ||
|
||
@LoginCheck | ||
@PostMapping("/logout") | ||
public void Logout(HttpServletResponse response) { | ||
ResponseCookie responseCookie = invalidLoginTokenCookie(); | ||
|
||
response.addHeader("Set-Cookie", responseCookie.toString()); | ||
} | ||
|
||
private ResponseCookie createLoginTokenCookie(Member member) { | ||
String loginToken = tokenUtil.generateToken(member); | ||
|
||
return cookieUtil.createTokenCookie(CookieDto.builder() | ||
.name(AuthConfig.LOGIN_COOKIE_NAME) | ||
.value(loginToken) | ||
.isEncode(true) | ||
.httpOnly(true) | ||
.maxAge(AuthConfig.TOKEN_VALID_TIME) | ||
.build() | ||
); | ||
} | ||
|
||
private ResponseCookie invalidLoginTokenCookie() { | ||
return cookieUtil.createTokenCookie(CookieDto.builder() | ||
.name(AuthConfig.LOGIN_COOKIE_NAME) | ||
.value(null) | ||
.isEncode(false) | ||
.httpOnly(true) | ||
.maxAge(0) | ||
.build() | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package co.kr.ticketing.member.controller.request; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
|
||
public record LoginRequest( | ||
@NotBlank | ||
String phoneNumber, | ||
@NotBlank | ||
String 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.
UseCase 가 직관적인 네이밍은 아닌거 같습니다.
계층 구조를 만들고자 한다면 Processor 나 Application 이 좋을 것 같습니다.
판단해주시면 좋을 것 같습니다.
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.
이 부분은 일단 UseCase로 유지해서 사용해보도록 하겠습니다