Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.0.0'

Expand Down
48 changes: 48 additions & 0 deletions src/main/java/roomescape/admin/AdminInterceptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package roomescape.admin;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;

@Component
public class AdminInterceptor implements HandlerInterceptor {

private static final String SECRET_KEY = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=";

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = Arrays.stream(request.getCookies())
.filter(cookie -> "token".equals(cookie.getName()))
.findFirst()
.map(Cookie::getValue)
.orElse(null);

if (token == null || !isAdmin(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
return true;
}

private boolean isAdmin(String token) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes(StandardCharsets.UTF_8)))
.build()
.parseClaimsJws(token)
.getBody();
String role = claims.get("role", String.class);
return "ADMIN".equals(role);
} catch (Exception e) {
return false;
}
}
}
20 changes: 20 additions & 0 deletions src/main/java/roomescape/auth/JwtConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package roomescape.auth;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JwtConfig {

@Value("${roomescape.auth.jwt.secret}")
private String secret;

@Value("${jwt.expiration}")
private Long expiration;

@Bean
public JwtUtil jwtUtil() {
return new JwtUtil(secret, expiration);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 이거를 고민했는데 roomescape내의 패키지로 auth를 만들 경우, 이게 맞는데 미션에서 동등한 계층이라는 말이 있어서 저는 java밑의 계층의 패키지로 만들어주었습니다. 그럴 경우 @import를 사용해서 외부 컴포넌트 스캔 범위를 벗어나는 경우를 스캔 할 수 있더라구요!

53 changes: 53 additions & 0 deletions src/main/java/roomescape/auth/JwtUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package roomescape.auth;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;

public class JwtUtil {

private static SecretKey secretKey;
private final Long expiration;

public JwtUtil(String secret, Long expiration) {
secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
this.expiration = expiration;
}

public String generateToken(String userId, String name, String role) {
Claims claims = Jwts.claims().setSubject(userId);
claims.put("name", name);
claims.put("role", role);
return Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();
}

public static Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
}

public static Long getUserIdFromToken(String token) {
Claims claims = parseToken(token);
return Long.parseLong(claims.getSubject());
}

public String getRoleFromToken(String token) {
Claims claims = parseToken(token);
return (String) claims.get("role");
}
}
20 changes: 20 additions & 0 deletions src/main/java/roomescape/config/InterceptorConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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.admin.AdminInterceptor;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

@Autowired
private AdminInterceptor adminInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(adminInterceptor)
.addPathPatterns("/admin/**");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interceptor와 argumentResolver의 역할이 맥락적으로 같다고 생각해서 같은 config안에 등록해주는 건 어떨까요?

31 changes: 31 additions & 0 deletions src/main/java/roomescape/member/LoginMember.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package roomescape.member;

public class LoginMember {
private Long id;
private String name;
private String email;
private 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;
}
}
55 changes: 55 additions & 0 deletions src/main/java/roomescape/member/LoginMemberArgumentResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package roomescape.member;

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.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

public class LoginMemberArgumentResolver implements HandlerMethodArgumentResolver {
private final MemberService memberService;
private final String secretKey = "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=";

public LoginMemberArgumentResolver(MemberService memberService) {
this.memberService = memberService;
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(LoginMember.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, org.springframework.web.bind.support.WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
Cookie[] cookies = request.getCookies();

String token = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("token".equals(cookie.getName())) {
token = cookie.getValue();
break;
}
}
}

if (token == null) {
throw new IllegalArgumentException("No token found in cookies");
}

Long memberId = Long.valueOf(Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseClaimsJws(token)
.getBody().getSubject());

Member member = memberService.findById(memberId);

return new LoginMember(member.getId(), member.getName(), member.getEmail(), member.getRole());
}
}
41 changes: 34 additions & 7 deletions src/main/java/roomescape/member/MemberController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,56 @@
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 java.net.URI;
import java.util.HashMap;
import java.util.Map;

@RestController
public class MemberController {
private MemberService memberService;
private final MemberService memberService;

public MemberController(MemberService memberService) {
this.memberService = memberService;
}

@PostMapping("/members")
public ResponseEntity createMember(@RequestBody MemberRequest memberRequest) {
public ResponseEntity<MemberResponse> createMember(@RequestBody MemberRequest memberRequest) {
MemberResponse member = memberService.createMember(memberRequest);
return ResponseEntity.created(URI.create("/members/" + member.getId())).body(member);
}

@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody MemberRequest memberRequest, HttpServletResponse response) {
memberService.login(memberRequest, response);
return ResponseEntity.ok().build();
}

@GetMapping("/login/check")
public ResponseEntity<Map<String, String>> checkLogin(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return ResponseEntity.status(401).build();
}

String name, role;
try {
name = memberService.getNameFromToken(cookies);
role = memberService.getRoleFromToken(cookies);
} catch (Exception e) {
return ResponseEntity.status(401).build();
}

Map<String, String> responseBody = new HashMap<>();
responseBody.put("name", name);
responseBody.put("role", role);

return ResponseEntity.ok(responseBody);
}

@PostMapping("/logout")
public ResponseEntity logout(HttpServletResponse response) {
public ResponseEntity<Void> logout(HttpServletResponse response) {
Cookie cookie = new Cookie("token", "");
cookie.setHttpOnly(true);
cookie.setPath("/");
Expand Down
25 changes: 21 additions & 4 deletions src/main/java/roomescape/member/MemberDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@Repository
public class MemberDao {
private JdbcTemplate jdbcTemplate;
private final JdbcTemplate jdbcTemplate;

public MemberDao(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
Expand Down Expand Up @@ -40,16 +40,33 @@ public Member findByEmailAndPassword(String email, String password) {
);
}

public Member findByName(String name) {
public Member findById(Long id) {
return jdbcTemplate.queryForObject(
"SELECT id, name, email, role FROM member WHERE name = ?",
"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")
),
name
id
);
}

public boolean existsByEmail(String email) {
Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM member WHERE email = ?", new Object[]{email}, Integer.class);
return count != null && count > 0;
}

public Member findByEmail(String email) {
return jdbcTemplate.queryForObject(
"SELECT * FROM member WHERE email = ?", new Object[]{email},
(rs, rowNum) -> new Member(
rs.getString("name"),
rs.getString("email"),
rs.getString("password"),
rs.getString("role")
));
}
}
Loading