From 09bc871c625a40527367b96830ccd41a726d3feb Mon Sep 17 00:00:00 2001 From: youngniw <1999julie@naver.com> Date: Fri, 15 Apr 2022 22:27:26 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[feat]=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pillaroid/controller/LoginController.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/main/java/com/nadoyagsa/pillaroid/controller/LoginController.java 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..ed6afa1 --- /dev/null +++ b/src/main/java/com/nadoyagsa/pillaroid/controller/LoginController.java @@ -0,0 +1,81 @@ +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.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; + + @Autowired + public LoginController(UserService userService) { + this.userService = userService; + } + + // 카카오 로그인 (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 + } + + 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 + } + // 클라이언트의 로그인 경험 없음(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 + } + } +} From 979bf642cd5bf7ebb0a3c9164acff723a6ed110a Mon Sep 17 00:00:00 2001 From: youngniw <1999julie@naver.com> Date: Sun, 17 Apr 2022 01:43:15 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[feat]=20jwt=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 ++ .../configuration/SecurityConfiguration.java | 26 +++++++ .../pillaroid/controller/LoginController.java | 22 ++++-- .../pillaroid/jwt/AuthInterceptor.java | 30 ++++++++ .../pillaroid/jwt/AuthTokenProvider.java | 70 +++++++++++++++++++ 5 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/nadoyagsa/pillaroid/configuration/SecurityConfiguration.java create mode 100644 src/main/java/com/nadoyagsa/pillaroid/jwt/AuthInterceptor.java create mode 100644 src/main/java/com/nadoyagsa/pillaroid/jwt/AuthTokenProvider.java 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; + } +}