diff --git a/src/main/java/com/kuit/kuit4serverauth/config/WebConfig.java b/src/main/java/com/kuit/kuit4serverauth/config/WebConfig.java index a0b47b4..78e96c0 100644 --- a/src/main/java/com/kuit/kuit4serverauth/config/WebConfig.java +++ b/src/main/java/com/kuit/kuit4serverauth/config/WebConfig.java @@ -1,20 +1,29 @@ package com.kuit.kuit4serverauth.config; import com.kuit.kuit4serverauth.interceptor.AuthInterceptor; +import com.kuit.kuit4serverauth.service.CustomArgumentsResolver; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + @Configuration +@RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { private final AuthInterceptor authInterceptor; + private final CustomArgumentsResolver customArgumentsResolver; - public WebConfig(AuthInterceptor authInterceptor) { - this.authInterceptor = authInterceptor; + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + .addPathPatterns("/profile", "/profile2", "/admin", "/admin2"); } @Override - public void addInterceptors(InterceptorRegistry registry) { - // TODO /profile, /admin 앞에 붙이기 + public void addArgumentResolvers(List argumentResolvers){ + argumentResolvers.add(customArgumentsResolver); } } diff --git a/src/main/java/com/kuit/kuit4serverauth/controller/AuthController.java b/src/main/java/com/kuit/kuit4serverauth/controller/AuthController.java index e8f7e24..ef17037 100644 --- a/src/main/java/com/kuit/kuit4serverauth/controller/AuthController.java +++ b/src/main/java/com/kuit/kuit4serverauth/controller/AuthController.java @@ -1,12 +1,16 @@ package com.kuit.kuit4serverauth.controller; +import com.kuit.kuit4serverauth.dto.LoginRequestDto; import com.kuit.kuit4serverauth.exception.CustomException; import com.kuit.kuit4serverauth.exception.ErrorCode; import com.kuit.kuit4serverauth.model.User; import com.kuit.kuit4serverauth.repository.UserRepository; import com.kuit.kuit4serverauth.service.JwtUtil; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpStatus; 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; @@ -34,10 +38,43 @@ public ResponseEntity> login(@RequestBody Map response = new HashMap<>(); - response.put("token", token); + Map response = generateTokenResponse(user); + return ResponseEntity.ok(response); + } + + @PostMapping("/login2") + public ResponseEntity> login2(@RequestBody LoginRequestDto loginRequestDto) { + String username = loginRequestDto.getUsername(); + String password = loginRequestDto.getPassword(); + + User user = userRepository.findByUsername(username); + if (user == null || !user.getPassword().equals(password)) { + throw new CustomException(ErrorCode.INVALID_USERNAME_OR_PASSWORD); + } + + Map response = generateTokenResponse(user); return ResponseEntity.ok(response); } + + @PostMapping("/refresh-token") + public ResponseEntity> refreshToken(@RequestBody Map tokenRequest) { + String refreshToken = tokenRequest.get("refresh-token"); + Claims claims = jwtUtil.validateToken(refreshToken); + String username = claims.getSubject(); + + User user = userRepository.findByUsername(username); + + Map response = generateTokenResponse(user); + return ResponseEntity.ok(response); + } + + private Map generateTokenResponse(User user) { + String newAccessToken = jwtUtil.generateAccessToken(user.getUsername(), user.getRole()); + String newRefreshToken = jwtUtil.generateRefreshToken(user.getUsername()); + Map response = new HashMap<>(); + response.put("access-token", newAccessToken); + response.put("refresh-token", newRefreshToken); + return response; + } } diff --git a/src/main/java/com/kuit/kuit4serverauth/controller/UserController.java b/src/main/java/com/kuit/kuit4serverauth/controller/UserController.java index 18cb7af..eb1df3d 100644 --- a/src/main/java/com/kuit/kuit4serverauth/controller/UserController.java +++ b/src/main/java/com/kuit/kuit4serverauth/controller/UserController.java @@ -1,23 +1,57 @@ package com.kuit.kuit4serverauth.controller; +import com.kuit.kuit4serverauth.model.User; +import com.kuit.kuit4serverauth.service.AccessUser; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +import static com.kuit.kuit4serverauth.enums.Role.ROLE_USER; +import static com.kuit.kuit4serverauth.exception.ErrorCode.FORBIDDEN_ACCESS; +import static com.kuit.kuit4serverauth.exception.ErrorCode.INVALID_TOKEN; + @RestController public class UserController { @GetMapping("/profile") public ResponseEntity getProfile(HttpServletRequest request) { - // TODO : 로그인 한 사용자면 username 이용해 "Hello, {username}" 반환하기 - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Unauthorized"); + String username = (String) request.getAttribute("username"); + + if (username != null) { + return ResponseEntity.status(HttpStatus.OK).body("Hello, "+username); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(INVALID_TOKEN.getMessage()); + } + + @GetMapping("/profile2") + public ResponseEntity getProfile2(@AccessUser User user) { + String username = user.getUsername(); + + if (username != null) { + return ResponseEntity.status(HttpStatus.OK).body("Hi, "+username); + } + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(INVALID_TOKEN.getMessage()); } @GetMapping("/admin") public ResponseEntity getAdmin(HttpServletRequest request) { - // TODO: role이 admin이면 "Hello, admin" 반환하기 - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Forbidden"); + String role = (String) request.getAttribute("role"); + + if (role.equals(ROLE_USER.getRole())) { + return ResponseEntity.status(HttpStatus.OK).body("Hello, admin"); + } + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(FORBIDDEN_ACCESS.getMessage()); + } + + @GetMapping("/admin2") + public ResponseEntity getAdmin2(@AccessUser User user) { + String role = user.getRole(); + + if (role.equals(ROLE_USER.getRole())) { + return ResponseEntity.status(HttpStatus.OK).body("Hi, admin"); + } + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(FORBIDDEN_ACCESS.getMessage()); } } diff --git a/src/main/java/com/kuit/kuit4serverauth/dto/LoginRequestDto.java b/src/main/java/com/kuit/kuit4serverauth/dto/LoginRequestDto.java new file mode 100644 index 0000000..00e4faf --- /dev/null +++ b/src/main/java/com/kuit/kuit4serverauth/dto/LoginRequestDto.java @@ -0,0 +1,11 @@ +package com.kuit.kuit4serverauth.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequestDto { + String username; + String password; +} diff --git a/src/main/java/com/kuit/kuit4serverauth/enums/HeaderName.java b/src/main/java/com/kuit/kuit4serverauth/enums/HeaderName.java new file mode 100644 index 0000000..8472eb7 --- /dev/null +++ b/src/main/java/com/kuit/kuit4serverauth/enums/HeaderName.java @@ -0,0 +1,14 @@ +package com.kuit.kuit4serverauth.enums; + +import lombok.Getter; + +@Getter +public enum HeaderName { + AUTHORIZATION("Authorization"); + + private final String header; + + HeaderName(String header) { + this.header = header; + } +} diff --git a/src/main/java/com/kuit/kuit4serverauth/enums/Role.java b/src/main/java/com/kuit/kuit4serverauth/enums/Role.java new file mode 100644 index 0000000..f345fc8 --- /dev/null +++ b/src/main/java/com/kuit/kuit4serverauth/enums/Role.java @@ -0,0 +1,16 @@ +package com.kuit.kuit4serverauth.enums; + +import lombok.Getter; + +@Getter +public enum Role { + ROLE_USER("ROLE_USER"), + ROLE_ADMIN("ROLE_ADMIN"); + + private final String role; + + Role(String role) { + this.role = role; + } +} + diff --git a/src/main/java/com/kuit/kuit4serverauth/exception/ErrorCode.java b/src/main/java/com/kuit/kuit4serverauth/exception/ErrorCode.java index ed89dd3..039c887 100644 --- a/src/main/java/com/kuit/kuit4serverauth/exception/ErrorCode.java +++ b/src/main/java/com/kuit/kuit4serverauth/exception/ErrorCode.java @@ -2,7 +2,9 @@ public enum ErrorCode { INVALID_USERNAME_OR_PASSWORD(401, "Invalid username or password"), - INVALID_TOKEN(401, "Invalid or expired token"), + INVALID_TOKEN(401, "Invalid token"), + EXPIRED_TOKEN(401, "Expired token"), + ACCESS_TOKEN_REQUIRED(401, "Access token required"), MISSING_AUTH_HEADER(401, "Missing or invalid Authorization header"), FORBIDDEN_ACCESS(403, "Access denied"), INTERNAL_SERVER_ERROR(500, "Internal server error"); diff --git a/src/main/java/com/kuit/kuit4serverauth/interceptor/AuthInterceptor.java b/src/main/java/com/kuit/kuit4serverauth/interceptor/AuthInterceptor.java index 7b48ce8..289d1cd 100644 --- a/src/main/java/com/kuit/kuit4serverauth/interceptor/AuthInterceptor.java +++ b/src/main/java/com/kuit/kuit4serverauth/interceptor/AuthInterceptor.java @@ -9,6 +9,8 @@ import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +import java.util.Date; + @Component public class AuthInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; @@ -23,6 +25,15 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons if (authHeader != null && authHeader.startsWith("Bearer ")) { String token = authHeader.substring(7); Claims claims = jwtUtil.validateToken(token); + + if (claims.getExpiration().before(new Date())) { + throw new CustomException(ErrorCode.EXPIRED_TOKEN); + } + + if(!claims.get("type", String.class).equals("access")){ + throw new CustomException(ErrorCode.ACCESS_TOKEN_REQUIRED); + } + request.setAttribute("username", claims.getSubject()); request.setAttribute("role", claims.get("role")); return true; diff --git a/src/main/java/com/kuit/kuit4serverauth/model/User.java b/src/main/java/com/kuit/kuit4serverauth/model/User.java index 7160884..9ed8eef 100644 --- a/src/main/java/com/kuit/kuit4serverauth/model/User.java +++ b/src/main/java/com/kuit/kuit4serverauth/model/User.java @@ -1,13 +1,24 @@ package com.kuit.kuit4serverauth.model; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; @Getter @Setter +@NoArgsConstructor public class User { private Long id; private String username; private String password; private String role; // e.g., ROLE_USER, ROLE_ADMIN + + private User(String username, String role){ + this.username = username; + this.role = role; + } + + public static User of(String username, String role){ + return new User(username, role); + } } \ No newline at end of file diff --git a/src/main/java/com/kuit/kuit4serverauth/service/AccessUser.java b/src/main/java/com/kuit/kuit4serverauth/service/AccessUser.java new file mode 100644 index 0000000..81f0902 --- /dev/null +++ b/src/main/java/com/kuit/kuit4serverauth/service/AccessUser.java @@ -0,0 +1,11 @@ +package com.kuit.kuit4serverauth.service; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AccessUser { +} diff --git a/src/main/java/com/kuit/kuit4serverauth/service/CustomArgumentsResolver.java b/src/main/java/com/kuit/kuit4serverauth/service/CustomArgumentsResolver.java new file mode 100644 index 0000000..da34794 --- /dev/null +++ b/src/main/java/com/kuit/kuit4serverauth/service/CustomArgumentsResolver.java @@ -0,0 +1,44 @@ +package com.kuit.kuit4serverauth.service; + +import com.kuit.kuit4serverauth.exception.CustomException; +import com.kuit.kuit4serverauth.exception.ErrorCode; +import com.kuit.kuit4serverauth.model.User; +import io.jsonwebtoken.Claims; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +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 static com.kuit.kuit4serverauth.enums.HeaderName.AUTHORIZATION; + +@Component +@RequiredArgsConstructor +public class CustomArgumentsResolver implements HandlerMethodArgumentResolver { + + private final JwtUtil jwtUtil; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AccessUser.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); + + String authHeader = request.getHeader(AUTHORIZATION.getHeader()); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + String token = authHeader.substring(7); + + Claims claims = jwtUtil.validateToken(token); + + return User.of(claims.getSubject(), (String)claims.get("role")); + } + throw new CustomException(ErrorCode.MISSING_AUTH_HEADER); + } +} diff --git a/src/main/java/com/kuit/kuit4serverauth/service/JwtUtil.java b/src/main/java/com/kuit/kuit4serverauth/service/JwtUtil.java index ead240e..51e594c 100644 --- a/src/main/java/com/kuit/kuit4serverauth/service/JwtUtil.java +++ b/src/main/java/com/kuit/kuit4serverauth/service/JwtUtil.java @@ -5,21 +5,37 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.util.Date; @Component public class JwtUtil { - private final String secret = "mysecretkey"; - private final long expirationMs = 3600000; // 1 hour - public String generateToken(String username, String role) { + private String secret = "mysecretkeymysecretkeymysecretkeymysecretkey"; + + private final long accessTokenExpirationMs = 3600000; // 1 hour + private final long refreshTokenExpirationMs = 3600000 * 24 * 7; // 7 day + + public String generateAccessToken(String username, String role) { return Jwts.builder() .setSubject(username) + .claim("type", "access") .claim("role", role) .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + expirationMs)) + .setExpiration(new Date(System.currentTimeMillis() + accessTokenExpirationMs)) + .signWith(SignatureAlgorithm.HS256, secret) + .compact(); + } + + public String generateRefreshToken(String username) { + return Jwts.builder() + .setSubject(username) + .claim("type", "refresh") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + refreshTokenExpirationMs)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } @@ -34,4 +50,9 @@ public Claims validateToken(String token) { throw new CustomException(ErrorCode.INVALID_TOKEN); } } + + public String getUsernameFromToken(String token) { + Claims claims = validateToken(token); + return claims.getSubject(); + } } \ No newline at end of file