From 6fabc042f939bfb4c97377618e80be74ea5663d3 Mon Sep 17 00:00:00 2001 From: heejung Date: Thu, 3 Jul 2025 07:56:27 +0100 Subject: [PATCH 1/4] =?UTF-8?q?1=EB=8B=A8=EA=B3=84=20:=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/LoginController.java | 90 +++++++++++++++++++ .../{ => controller}/PageController.java | 2 +- .../java/roomescape/login/LoginRequest.java | 14 +++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/main/java/roomescape/controller/LoginController.java rename src/main/java/roomescape/{ => controller}/PageController.java (96%) create mode 100644 src/main/java/roomescape/login/LoginRequest.java diff --git a/src/main/java/roomescape/controller/LoginController.java b/src/main/java/roomescape/controller/LoginController.java new file mode 100644 index 000000000..29f79af24 --- /dev/null +++ b/src/main/java/roomescape/controller/LoginController.java @@ -0,0 +1,90 @@ +package roomescape.controller; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import roomescape.login.LoginRequest; +import roomescape.member.Member; +import roomescape.member.MemberDao; +import roomescape.member.MemberResponse; + +@RestController +public class LoginController { + private final String SECRET_KEY = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; + private final MemberDao memberDao; + + public LoginController(MemberDao memberDao) { + this.memberDao = memberDao; + } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) { + try { + Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(), loginRequest.getPassword()); + + String accessToken = Jwts.builder() + .setSubject(member.getId().toString()) + .claim("name", member.getName()) + .claim("role", member.getRole()) + .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) + .compact(); + + Cookie cookie = new Cookie("token", accessToken); + cookie.setHttpOnly(true); + cookie.setPath("/"); + response.addCookie(cookie); + + return ResponseEntity.ok().build(); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + @GetMapping("/login/check") + public ResponseEntity checkLogin(HttpServletRequest request) { + try { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return ResponseEntity.badRequest().build(); + } + + String token = extractTokenFromCookie(cookies); + if (token.isEmpty()) { + return ResponseEntity.badRequest().build(); + } + + Claims claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) + .build() + .parseClaimsJws(token) + .getBody(); + + String name = claims.get("name", String.class); + + return ResponseEntity.ok(new MemberResponse( + Long.valueOf(claims.getSubject()), + name, + null + )); + } catch (Exception e) { + return ResponseEntity.badRequest().build(); + } + } + + private String extractTokenFromCookie(Cookie[] cookies) { + for (Cookie cookie : cookies) { + if (cookie.getName().equals("token")) { + return cookie.getValue(); + } + } + return ""; + } +} diff --git a/src/main/java/roomescape/PageController.java b/src/main/java/roomescape/controller/PageController.java similarity index 96% rename from src/main/java/roomescape/PageController.java rename to src/main/java/roomescape/controller/PageController.java index ac8ef9408..5f3f8ef7e 100644 --- a/src/main/java/roomescape/PageController.java +++ b/src/main/java/roomescape/controller/PageController.java @@ -1,4 +1,4 @@ -package roomescape; +package roomescape.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/roomescape/login/LoginRequest.java b/src/main/java/roomescape/login/LoginRequest.java new file mode 100644 index 000000000..c9ffb9cbe --- /dev/null +++ b/src/main/java/roomescape/login/LoginRequest.java @@ -0,0 +1,14 @@ +package roomescape.login; + +public class LoginRequest { + private String email; + private String password; + + public String getEmail() { + return email; + } + + public String getPassword() { + return password; + } +} From f085f191982abb34016e241e6328801cd7da751c Mon Sep 17 00:00:00 2001 From: heejung Date: Thu, 3 Jul 2025 08:30:55 +0100 Subject: [PATCH 2/4] =?UTF-8?q?2=EB=8B=A8=EA=B3=84=20:=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/roomescape/config/WebMvcConfig.java | 27 ++++++++ .../java/roomescape/login/LoginMember.java | 20 ++++++ .../login/LoginMemberArgumentResolver.java | 65 +++++++++++++++++++ .../java/roomescape/member/MemberDao.java | 17 +++++ src/test/java/roomescape/MissionStepTest.java | 55 +++++++++++++--- 5 files changed, 174 insertions(+), 10 deletions(-) create mode 100644 src/main/java/roomescape/config/WebMvcConfig.java create mode 100644 src/main/java/roomescape/login/LoginMember.java create mode 100644 src/main/java/roomescape/login/LoginMemberArgumentResolver.java diff --git a/src/main/java/roomescape/config/WebMvcConfig.java b/src/main/java/roomescape/config/WebMvcConfig.java new file mode 100644 index 000000000..addb0c628 --- /dev/null +++ b/src/main/java/roomescape/config/WebMvcConfig.java @@ -0,0 +1,27 @@ +package roomescape.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import roomescape.login.LoginMemberArgumentResolver; +import roomescape.member.MemberDao; + +import java.util.List; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final MemberDao memberDao; + private final String secretKey = System.getenv() + .getOrDefault("JWT_SECRET_KEY", + "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="); + + public WebMvcConfig(MemberDao memberDao) { + this.memberDao = memberDao; + } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(new LoginMemberArgumentResolver(secretKey, memberDao)); + } +} diff --git a/src/main/java/roomescape/login/LoginMember.java b/src/main/java/roomescape/login/LoginMember.java new file mode 100644 index 000000000..528b835a7 --- /dev/null +++ b/src/main/java/roomescape/login/LoginMember.java @@ -0,0 +1,20 @@ +package roomescape.login; + +public class LoginMember { + private final Long id; + private final String name; + private final String email; + private final String role; + + public LoginMember(Long id, String name, String email, String role) { + this.id = id; + this.name = name; + this.email = email; + this.role = role; + } + + public Long getId() { return id; } + public String getName() { return name; } + public String getEmail() { return email; } + public String getRole() { return role; } +} diff --git a/src/main/java/roomescape/login/LoginMemberArgumentResolver.java b/src/main/java/roomescape/login/LoginMemberArgumentResolver.java new file mode 100644 index 000000000..913559d84 --- /dev/null +++ b/src/main/java/roomescape/login/LoginMemberArgumentResolver.java @@ -0,0 +1,65 @@ +package roomescape.login; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; +import roomescape.member.Member; +import roomescape.member.MemberDao; + +public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver { + + private final String secretKey; + private final MemberDao memberDao; + + public LoginMemberArgumentResolver(String secretKey, MemberDao memberDao) { + this.secretKey = secretKey; + this.memberDao = memberDao; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return LoginMember.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + if (request == null || request.getCookies() == null) { + return null; + } + + String token = ""; + for (Cookie cookie : request.getCookies()) { + if ("token".equals(cookie.getName())) { + token = cookie.getValue(); + break; + } + } + if (token.isBlank()) { + return null; + } + + Claims claims = Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes())) + .build() + .parseClaimsJws(token) + .getBody(); + + Long memberId = Long.valueOf(claims.getSubject()); + Member member = memberDao.findById(memberId) + .orElseThrow(() -> new IllegalStateException("회원을 찾을 수 없습니다")); + + return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole()); + } +} diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java index 81f77f4cd..bee0ed14a 100644 --- a/src/main/java/roomescape/member/MemberDao.java +++ b/src/main/java/roomescape/member/MemberDao.java @@ -5,6 +5,9 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public class MemberDao { private JdbcTemplate jdbcTemplate; @@ -13,6 +16,20 @@ public MemberDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } + public Optional findById(Long id) { + List results = jdbcTemplate.query( + "SELECT id, name, email, role FROM member WHERE id = ?", + (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getString("role") + ), + id + ); + return results.stream().findFirst(); + } + public Member save(Member member) { KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 6add784bd..578ebf3ff 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; +import roomescape.reservation.ReservationResponse; import java.util.HashMap; import java.util.Map; @@ -17,22 +18,56 @@ @DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) public class MissionStepTest { + private String createToken(String email, String password) { + Map loginParams = new HashMap<>(); + loginParams.put("email", email); + loginParams.put("password", password); + + ExtractableResponse loginResponse = RestAssured.given() + .log().all() + .contentType(ContentType.JSON) + .body(loginParams) + .post("/login") + .then().log().all() + .statusCode(200) + .extract(); + + String setCookie = loginResponse.header("Set-Cookie"); + return setCookie.split(";")[0].split("=")[1]; + } + @Test - void 일단계() { - Map params = new HashMap<>(); - params.put("email", "admin@email.com"); - params.put("password", "password"); + void 이단계() { + String token = createToken("admin@email.com", "password"); + + Map params = new HashMap<>(); + params.put("name", "어드민"); + params.put("date", "2024-03-01"); + params.put("time", 1); + params.put("theme", 1); ExtractableResponse response = RestAssured.given().log().all() - .contentType(ContentType.JSON) .body(params) - .when().post("/login") + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") .then().log().all() - .statusCode(200) .extract(); - String token = response.headers().get("Set-Cookie").getValue().split(";")[0].split("=")[1]; + assertThat(response.statusCode()).isEqualTo(201); + assertThat(response.as(ReservationResponse.class).getName()).isEqualTo("어드민"); + + params.put("name", "브라운"); + + ExtractableResponse adminResponse = RestAssured.given().log().all() + .body(params) + .cookie("token", token) + .contentType(ContentType.JSON) + .post("/reservations") + .then().log().all() + .extract(); - assertThat(token).isNotBlank(); + assertThat(adminResponse.statusCode()).isEqualTo(201); + assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); } -} \ No newline at end of file +} From 6557024bf7218b0f25d7d3087facb8d5fdb7cda3 Mon Sep 17 00:00:00 2001 From: heejung Date: Sun, 31 Aug 2025 23:47:44 +0900 Subject: [PATCH 3/4] =?UTF-8?q?3=EB=8B=A8=EA=B3=84=20:=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes src/.DS_Store | Bin 0 -> 6148 bytes src/main/.DS_Store | Bin 0 -> 6148 bytes src/main/java/.DS_Store | Bin 0 -> 6148 bytes .../java/roomescape/auth/JwtProvider.java | 58 +++++++++++++ .../java/roomescape/config/WebConfig.java | 24 ++++++ .../java/roomescape/config/WebMvcConfig.java | 27 ------ .../controller/LoginController.java | 81 ++++-------------- .../interceptor/AdminInterceptor.java | 41 +++++++++ .../java/roomescape/member/MemberDao.java | 14 +++ .../java/roomescape/member/MemberService.java | 15 +++- src/test/java/roomescape/MissionStepTest.java | 39 +++------ 12 files changed, 180 insertions(+), 119 deletions(-) create mode 100644 .DS_Store create mode 100644 src/.DS_Store create mode 100644 src/main/.DS_Store create mode 100644 src/main/java/.DS_Store create mode 100644 src/main/java/roomescape/auth/JwtProvider.java create mode 100644 src/main/java/roomescape/config/WebConfig.java delete mode 100644 src/main/java/roomescape/config/WebMvcConfig.java create mode 100644 src/main/java/roomescape/interceptor/AdminInterceptor.java diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..6a6de73130e1ac964d340ea18f1a373fb9f66c87 GIT binary patch literal 6148 zcmeHKL2uJA6n<{IHC;jKfl0d{S>jr4?WhnFm(Z<;Nr(eM>@ZMCmTE+VcxqB|s4C?= z{sVu3E5C&QveS6aeico+-4NQ4pY;4W_V*?B7sZYNKn!PLAD{yO4klq^3yT|q`YG3< z#w{&Gq3;nM$LcJIRj|gj4b}ncz}<6zzTFK7U3&pno40et;|jOA%7wgqXYsAVZkO3OssP87Ra93MRTEypF&x4OUV{gt zGP`zOVb>wWnVywd+MDky-Y_f3(+ge!e+CzElxDr&FHx&EHa441(`h>&yiY3kGC!N8 zLw|ZoS8tVygJs{m>?Dx8I@buYY(Q>w*?7et1{v3Zv)FsXjL*SaN<00b`$YY+(S8tlc zDj`Rf>?O z%MR`TqubB_Ws$wI4p;~ND+fg5$U7S1lz4Aln;h-667wS_1@Q`lT7m^{$GSpW@eZaA a?2}YMbQueS*n(ky1hfsdunzoF2Yv%#f}$?~ literal 0 HcmV?d00001 diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e15cf5ee1c55d828fa565aa081f2be7e60226e0f GIT binary patch literal 6148 zcmeHKy-ve05I(norb0*^KV31rcdnH7OmcN}1tdSa}j& z2kvaQ;&%j5bSIrZ`+R4|pCj7`0HQhU)Bs8VV51V|idcLh)KB_Ja>imI3L4`ul2<__ z18mcPEPtN?+B*w+;6n)UhyB~bzV#O+Y9TYfV3hRDK0gKhC`{67^;zWd*1}@JF4#-< zg?A%IUh1dAr0EaNsC6u56in-Wa1wS$?b6D=j8i|1y8{&vcDm?tej3IdIcmyb+(}fd zr#tMTU2K=i<8h-=t2njo`lRBFck0!O)7adaOp5mE+Q#lt`!?#u@?KLf@V`{DY;Xz> zSedQo&Kty$jIYr~{uZRrg#^+Md^}>`TRyPK!VEA2%)neSV9rK+X)eE>Kbjd}27ZzO zIv*TVLf2rXQ5_vvs1yK^o{>thuAcsg4jB+#gPBIOpa>m`s6&NGF@(t+gbv4k*Z5}| zbvOt$Gmc|s7A8XxCh-uCatGmRMBNGx{AxFO3?051<^H_X+#SOKLivFTrdN_%D^i& CK46Cc literal 0 HcmV?d00001 diff --git a/src/main/.DS_Store b/src/main/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7c2b567eefaeff62287570987d596aceae05633a GIT binary patch literal 6148 zcmeHKO;6)65FMAYBtl3%uu_kaxK?Zn71|Y-P~gCotl+SVNr3=AEj{Rp-Af zcnjX5cNCrJaWssFqoNZJesS-o)>-mk$H_t3A9riZ+d3b{Y2F{0gtXU3$nV26@9A+z zkMdq&aw9w7`JUgc)hCm7yVVF<-`A#%VDe+F*$CR}8`G)peO>vsx!?Vh{mu22rC#B` zG_vII0j^=p=5rYh@=WKa@Nv9P1L{*jnntir*nY;$4Wb@1gRY1$APfitABX|BH+qX7 z=$qwJg#lsUZ5ZJFA%HT54oiz>>p){l03cr@jlecne_+54VCb;42oFTrRG>{&uEbET za!@uM6umKDmmKNcG$VI@= LAdN8arVRW8TXtfe literal 0 HcmV?d00001 diff --git a/src/main/java/.DS_Store b/src/main/java/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..63d3ba0c67f913dd67579b81839c0868fe731735 GIT binary patch literal 6148 zcmeHK!A{#i5SCwTT5*jW>!Da)=`N3_rw| z@H_BkcY{;~jzy)KiDutqcb>g|Ywdc7NcARz4pEbc0ytx>f#wkm9J2gy*sY2Z91vYp|qlaDDbBOT>HVAeE#phT>sl7X+!~0;Qv%W)%T*k z9-hsgtxHeIXRU_+g0pd6q!9&PRe?vJ CI$N9o literal 0 HcmV?d00001 diff --git a/src/main/java/roomescape/auth/JwtProvider.java b/src/main/java/roomescape/auth/JwtProvider.java new file mode 100644 index 000000000..e2fb1ca6c --- /dev/null +++ b/src/main/java/roomescape/auth/JwtProvider.java @@ -0,0 +1,58 @@ +package roomescape.auth; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +public class JwtProvider { + + @Value("${roomescape.auth.jwt.secret}") + private String SECRET_KEY; + + private static final long EXPIRATION_MS = 1000 * 60 * 60 * 24; + + public String generateToken(Long memberId, String email, String role) { + return Jwts.builder() + .setSubject(String.valueOf(memberId)) + .claim("email", email) + .claim("role", role) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_MS)) + .signWith(SignatureAlgorithm.HS256, SECRET_KEY) + .compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser() + .setSigningKey(SECRET_KEY) + .parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + public String getRoleFromToken(String token) { + Claims claims = Jwts.parser() + .setSigningKey(SECRET_KEY) + .parseClaimsJws(token) + .getBody(); + + return claims.get("role", String.class); + } + + public String getEmailFromToken(String token) { + Claims claims = Jwts.parser() + .setSigningKey(SECRET_KEY) + .parseClaimsJws(token) + .getBody(); + + return claims.get("email", String.class); + } +} diff --git a/src/main/java/roomescape/config/WebConfig.java b/src/main/java/roomescape/config/WebConfig.java new file mode 100644 index 000000000..b9d2d56b5 --- /dev/null +++ b/src/main/java/roomescape/config/WebConfig.java @@ -0,0 +1,24 @@ +package roomescape.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import roomescape.interceptor.AdminInterceptor; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + private final AdminInterceptor adminInterceptor; + + @Autowired + public WebConfig(AdminInterceptor adminInterceptor) { + this.adminInterceptor = adminInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(adminInterceptor) + .addPathPatterns("/admin/**"); + } +} diff --git a/src/main/java/roomescape/config/WebMvcConfig.java b/src/main/java/roomescape/config/WebMvcConfig.java deleted file mode 100644 index addb0c628..000000000 --- a/src/main/java/roomescape/config/WebMvcConfig.java +++ /dev/null @@ -1,27 +0,0 @@ -package roomescape.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import roomescape.login.LoginMemberArgumentResolver; -import roomescape.member.MemberDao; - -import java.util.List; - -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - - private final MemberDao memberDao; - private final String secretKey = System.getenv() - .getOrDefault("JWT_SECRET_KEY", - "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="); - - public WebMvcConfig(MemberDao memberDao) { - this.memberDao = memberDao; - } - - @Override - public void addArgumentResolvers(List resolvers) { - resolvers.add(new LoginMemberArgumentResolver(secretKey, memberDao)); - } -} diff --git a/src/main/java/roomescape/controller/LoginController.java b/src/main/java/roomescape/controller/LoginController.java index 29f79af24..383b41e70 100644 --- a/src/main/java/roomescape/controller/LoginController.java +++ b/src/main/java/roomescape/controller/LoginController.java @@ -1,90 +1,47 @@ package roomescape.controller; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; +import roomescape.auth.JwtProvider; import roomescape.login.LoginRequest; import roomescape.member.Member; import roomescape.member.MemberDao; -import roomescape.member.MemberResponse; @RestController public class LoginController { - private final String SECRET_KEY = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E="; + private final MemberDao memberDao; + private final JwtProvider jwtProvider; - public LoginController(MemberDao memberDao) { + public LoginController(MemberDao memberDao, JwtProvider jwtProvider) { this.memberDao = memberDao; + this.jwtProvider = jwtProvider; } @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) { try { - Member member = memberDao.findByEmailAndPassword(loginRequest.getEmail(), loginRequest.getPassword()); - - String accessToken = Jwts.builder() - .setSubject(member.getId().toString()) - .claim("name", member.getName()) - .claim("role", member.getRole()) - .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) - .compact(); - - Cookie cookie = new Cookie("token", accessToken); + Member member = memberDao.findByEmailAndPassword( + loginRequest.getEmail(), + loginRequest.getPassword() + ); + + String token = jwtProvider.generateToken( + member.getId(), + member.getEmail(), + member.getRole() + ); + + Cookie cookie = new Cookie("token", token); cookie.setHttpOnly(true); cookie.setPath("/"); response.addCookie(cookie); return ResponseEntity.ok().build(); } catch (Exception e) { - return ResponseEntity.badRequest().build(); - } - } - - @GetMapping("/login/check") - public ResponseEntity checkLogin(HttpServletRequest request) { - try { - Cookie[] cookies = request.getCookies(); - if (cookies == null) { - return ResponseEntity.badRequest().build(); - } - - String token = extractTokenFromCookie(cookies); - if (token.isEmpty()) { - return ResponseEntity.badRequest().build(); - } - - Claims claims = Jwts.parserBuilder() - .setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes())) - .build() - .parseClaimsJws(token) - .getBody(); - - String name = claims.get("name", String.class); - - return ResponseEntity.ok(new MemberResponse( - Long.valueOf(claims.getSubject()), - name, - null - )); - } catch (Exception e) { - return ResponseEntity.badRequest().build(); - } - } - - private String extractTokenFromCookie(Cookie[] cookies) { - for (Cookie cookie : cookies) { - if (cookie.getName().equals("token")) { - return cookie.getValue(); - } + return ResponseEntity.status(401).build(); } - return ""; } } diff --git a/src/main/java/roomescape/interceptor/AdminInterceptor.java b/src/main/java/roomescape/interceptor/AdminInterceptor.java new file mode 100644 index 000000000..9bc6caf5f --- /dev/null +++ b/src/main/java/roomescape/interceptor/AdminInterceptor.java @@ -0,0 +1,41 @@ +package roomescape.interceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import roomescape.auth.JwtProvider; + +@Component +public class AdminInterceptor implements HandlerInterceptor { + + private final JwtProvider jwtProvider; + + public AdminInterceptor(JwtProvider jwtProvider) { + this.jwtProvider = jwtProvider; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String token = extractTokenFromCookies(request); + + if (token == null || !jwtProvider.validateToken(token) || !"ADMIN".equals(jwtProvider.getRoleFromToken(token))) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + return false; + } + + return true; + } + + private String extractTokenFromCookies(HttpServletRequest request) { + if (request.getCookies() == null) return null; + + for (var cookie : request.getCookies()) { + if ("token".equals(cookie.getName())) { + return cookie.getValue(); + } + } + + return null; + } +} diff --git a/src/main/java/roomescape/member/MemberDao.java b/src/main/java/roomescape/member/MemberDao.java index bee0ed14a..0aa992301 100644 --- a/src/main/java/roomescape/member/MemberDao.java +++ b/src/main/java/roomescape/member/MemberDao.java @@ -15,6 +15,20 @@ public class MemberDao { public MemberDao(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } + public Optional findByEmail(String email) { + List results = jdbcTemplate.query( + "SELECT id, name, email, role FROM member WHERE email = ?", + (rs, rowNum) -> new Member( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getString("role") + ), + email + ); + return results.stream().findFirst(); + } + public Optional findById(Long id) { List results = jdbcTemplate.query( diff --git a/src/main/java/roomescape/member/MemberService.java b/src/main/java/roomescape/member/MemberService.java index ccaa8cba5..2e6ae1c9a 100644 --- a/src/main/java/roomescape/member/MemberService.java +++ b/src/main/java/roomescape/member/MemberService.java @@ -1,17 +1,28 @@ package roomescape.member; import org.springframework.stereotype.Service; +import roomescape.auth.JwtProvider; @Service public class MemberService { - private MemberDao memberDao; + private final MemberDao memberDao; + private final JwtProvider jwtProvider; - public MemberService(MemberDao memberDao) { + public MemberService(MemberDao memberDao, JwtProvider jwtProvider) { this.memberDao = memberDao; + this.jwtProvider = jwtProvider; } public MemberResponse createMember(MemberRequest memberRequest) { Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER")); return new MemberResponse(member.getId(), member.getName(), member.getEmail()); } + + public Member findByToken(String token) { + String email = jwtProvider.getEmailFromToken(token); + if (email == null) { + return null; + } + return memberDao.findByEmail(email).orElse(null); + } } diff --git a/src/test/java/roomescape/MissionStepTest.java b/src/test/java/roomescape/MissionStepTest.java index 578ebf3ff..ed2d90852 100644 --- a/src/test/java/roomescape/MissionStepTest.java +++ b/src/test/java/roomescape/MissionStepTest.java @@ -35,39 +35,22 @@ private String createToken(String email, String password) { String setCookie = loginResponse.header("Set-Cookie"); return setCookie.split(";")[0].split("=")[1]; } - @Test - void 이단계() { - String token = createToken("admin@email.com", "password"); - - Map params = new HashMap<>(); - params.put("name", "어드민"); - params.put("date", "2024-03-01"); - params.put("time", 1); - params.put("theme", 1); + void 삼단계() { + String brownToken = createToken("brown@email.com", "password"); - ExtractableResponse response = RestAssured.given().log().all() - .body(params) - .cookie("token", token) - .contentType(ContentType.JSON) - .post("/reservations") + RestAssured.given().log().all() + .cookie("token", brownToken) + .get("/admin") .then().log().all() - .extract(); + .statusCode(401); - assertThat(response.statusCode()).isEqualTo(201); - assertThat(response.as(ReservationResponse.class).getName()).isEqualTo("어드민"); + String adminToken = createToken("admin@email.com", "password"); - params.put("name", "브라운"); - - ExtractableResponse adminResponse = RestAssured.given().log().all() - .body(params) - .cookie("token", token) - .contentType(ContentType.JSON) - .post("/reservations") + RestAssured.given().log().all() + .cookie("token", adminToken) + .get("/admin") .then().log().all() - .extract(); - - assertThat(adminResponse.statusCode()).isEqualTo(201); - assertThat(adminResponse.as(ReservationResponse.class).getName()).isEqualTo("브라운"); + .statusCode(200); } } From fb157969fd65b1bd11233e7490795ae9502e956b Mon Sep 17 00:00:00 2001 From: heejung Date: Wed, 3 Sep 2025 23:49:57 +0900 Subject: [PATCH 4/4] =?UTF-8?q?gitignore=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c2065bc26..217351608 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ build/ !gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ +.DS_Store +.application.properties ### STS ### .apt_generated