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 new file mode 100644 index 0000000..0b0b36c --- /dev/null +++ b/src/main/java/com/nadoyagsa/pillaroid/controller/LoginController.java @@ -0,0 +1,89 @@ +package com.nadoyagsa.pillaroid.controller; + +import com.fasterxml.jackson.core.JsonProcessingException; +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.*; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping(value = "/login") +public class LoginController { + private final UserService userService; + private final AuthTokenProvider authTokenProvider; + + @Autowired + public LoginController(UserService userService, AuthTokenProvider authTokenProvider) { + this.userService = userService; + this.authTokenProvider = authTokenProvider; + } + + // 카카오 로그인 (Input: access token) + @PostMapping("/kakao") + public ResponseEntity> kakaoLogin(@RequestBody Map requestBody) { + String accessToken = requestBody.get("access_token"); + + HashMap response = new HashMap<>(); // 서버->클라이언트 응답 + + RestTemplate restTemplate = new RestTemplate(); // Spring의 HTTP 통신 템플릿 + // 카카오로 보낼 헤더 설정 + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); + headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + // 카카오와의 통신 + HttpEntity> kakaoUserInfoRequest = new HttpEntity<>(headers); + ResponseEntity kakaoResponse; + try { + kakaoResponse = restTemplate.exchange( + "https://kapi.kakao.com/v2/user/me", + HttpMethod.POST, + kakaoUserInfoRequest, + String.class + ); + } catch (Exception e) { // 카카오로 요청 실패 + response.put("success", false); + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); // Status Code=400 + } + + ObjectMapper objectMapper = new ObjectMapper(); + Long kakaoUserId; + try { + JsonNode userInfo = objectMapper.readTree(kakaoResponse.getBody()); + kakaoUserId = userInfo.path("id").asLong(); + } catch (JsonProcessingException e) { + response.put("success", false); + 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("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("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; + } +}