diff --git a/build.gradle b/build.gradle index cdbcb49..7efe61e 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,11 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' runtimeOnly 'mysql:mysql-connector-java' + + implementation 'io.jsonwebtoken:jjwt-api:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.2' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.2' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/src/main/java/com/nadoyagsa/pillaroid/configuration/SecurityConfiguration.java b/src/main/java/com/nadoyagsa/pillaroid/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..9e06a87 --- /dev/null +++ b/src/main/java/com/nadoyagsa/pillaroid/configuration/SecurityConfiguration.java @@ -0,0 +1,26 @@ +package com.nadoyagsa.pillaroid.configuration; + +import com.nadoyagsa.pillaroid.jwt.AuthInterceptor; +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; + +@Configuration +public class SecurityConfiguration implements WebMvcConfigurer { + private final AuthInterceptor authInterceptor; + + @Autowired + public SecurityConfiguration(AuthInterceptor authInterceptor) { + this.authInterceptor = authInterceptor; + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + //토큰 검사 안하는 경로 설정함(/login/**, 알약 검색 등) + //TODO: 알약 검색 시 patterns 사용해서 경로 추가해야 함 + registry.addInterceptor(authInterceptor) + .addPathPatterns("/**") + .excludePathPatterns(new String[]{"/login/**"}); + } +} diff --git a/src/main/java/com/nadoyagsa/pillaroid/controller/LoginController.java b/src/main/java/com/nadoyagsa/pillaroid/controller/LoginController.java index ed6afa1..0b0b36c 100644 --- a/src/main/java/com/nadoyagsa/pillaroid/controller/LoginController.java +++ b/src/main/java/com/nadoyagsa/pillaroid/controller/LoginController.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.nadoyagsa.pillaroid.entity.User; +import com.nadoyagsa.pillaroid.jwt.AuthTokenProvider; import com.nadoyagsa.pillaroid.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.*; @@ -19,10 +20,12 @@ @RequestMapping(value = "/login") public class LoginController { private final UserService userService; + private final AuthTokenProvider authTokenProvider; @Autowired - public LoginController(UserService userService) { + public LoginController(UserService userService, AuthTokenProvider authTokenProvider) { this.userService = userService; + this.authTokenProvider = authTokenProvider; } // 카카오 로그인 (Input: access token) @@ -49,7 +52,7 @@ public ResponseEntity> kakaoLogin(@RequestBody Map(response, HttpStatus.BAD_REQUEST); //Status Code=400 + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); // Status Code=400 } ObjectMapper objectMapper = new ObjectMapper(); @@ -59,23 +62,28 @@ public ResponseEntity> kakaoLogin(@RequestBody Map(response, HttpStatus.INTERNAL_SERVER_ERROR); //Status Code=500 + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); // Status Code=500 } + String authToken = authTokenProvider.createAuthToken(kakaoUserId); Optional user = userService.findUserByKakaoAccountId(kakaoUserId); // 클라이언트의 로그인 경험 있음 if (user.isPresent()) { response.put("success", true); - response.put("user", user); - return new ResponseEntity<>(response, HttpStatus.OK); //Status Code=200 + response.put("authToken", authToken); + //response.put("user", user); + return new ResponseEntity<>(response, HttpStatus.OK); // Status Code=200 } // 클라이언트의 로그인 경험 없음(DB에 사용자 추가) else { User newUser = userService.signUp(new User(kakaoUserId)); response.put("success", true); - response.put("user", newUser); - return new ResponseEntity<>(response, HttpStatus.CREATED); //Status Code=201 + response.put("authToken", authToken); + //response.put("user", newUser); + return new ResponseEntity<>(response, HttpStatus.CREATED); // Status Code=201 } } + + // 자동로그인은 시각장애인을 위해 프론트에서 authToken값 존재 여부에 따라 수행됨 } diff --git a/src/main/java/com/nadoyagsa/pillaroid/jwt/AuthInterceptor.java b/src/main/java/com/nadoyagsa/pillaroid/jwt/AuthInterceptor.java new file mode 100644 index 0000000..7d2e5b4 --- /dev/null +++ b/src/main/java/com/nadoyagsa/pillaroid/jwt/AuthInterceptor.java @@ -0,0 +1,30 @@ +package com.nadoyagsa.pillaroid.jwt; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@Component +public class AuthInterceptor implements HandlerInterceptor { + private final AuthTokenProvider authTokenProvider; + + @Autowired + public AuthInterceptor(AuthTokenProvider authTokenProvider) { + this.authTokenProvider = authTokenProvider; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String token = request.getHeader("authorization"); + if (token != null && authTokenProvider.validateToken(token)) { + return true; + } else { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + return false; + } + } +} diff --git a/src/main/java/com/nadoyagsa/pillaroid/jwt/AuthTokenProvider.java b/src/main/java/com/nadoyagsa/pillaroid/jwt/AuthTokenProvider.java new file mode 100644 index 0000000..9426510 --- /dev/null +++ b/src/main/java/com/nadoyagsa/pillaroid/jwt/AuthTokenProvider.java @@ -0,0 +1,70 @@ +package com.nadoyagsa.pillaroid.jwt; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Date; + +@Component +public class AuthTokenProvider implements InitializingBean { + private final Logger logger = LoggerFactory.getLogger(AuthTokenProvider.class); + + private final String secretKey; + private Key key; + + public AuthTokenProvider(@Value("${jwt.secret-key}") String secretKey) { + this.secretKey = secretKey; + } + + @Override + public void afterPropertiesSet() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + // 토큰의 payload에 카카오 회원번호를 삽입 + public String createAuthToken(Long kakaoAccountId) { + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setIssuer("pillaroid") + .setIssuedAt(new Date()) + .claim("accountId", kakaoAccountId) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + // 토큰으로부터 payload를 추출하는 메서드 + public Claims getClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + + return true; + } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) { + logger.info("잘못된 JWT 서명입니다."); + } catch (ExpiredJwtException e) { + logger.info("만료된 JWT 토큰입니다."); + } catch (UnsupportedJwtException e) { + logger.info("지원되지 않는 JWT 토큰입니다."); + } catch (IllegalArgumentException e) { + logger.info("JWT 토큰이 잘못되었습니다."); + } catch (Exception e) { + logger.info("서비스에 접근할 수 없는 토큰입니다."); + } + return false; + } +}