Compare commits

...

27 Commits

Author SHA1 Message Date
8fe6a32ede Merge branch 'main' of https://git.byeori.cloud/admin/ocr-java
pull merge
2026-03-25 23:20:05 +09:00
root
fee9b12395 로그인 구조 변경 2026-02-27 18:16:26 +09:00
root
5fd0c03560 userid 타입 변경 2026-02-27 11:14:31 +09:00
89d1acf5c9 구조 변경 2026-02-25 17:53:07 +09:00
6a2405a782 ds_store제거 2026-02-25 17:51:55 +09:00
root
d9211cc12b 일부 수정 2026-02-23 19:57:09 +09:00
d8af2f71c6 Merge branch 'main' of https://git.byeori.cloud/admin/ocr-java
merge
2026-02-18 15:19:59 +09:00
root
074d4c8d63 api서버로 이미지 전송 2026-02-04 16:12:52 +09:00
a92d31e371 보안 검토 2026-02-03 00:15:27 +09:00
root
e54c219b76 이미지 수신 2026-01-28 19:15:49 +09:00
8d9d75e372 사용자로그인여부 확인하는 서비스 2025-12-29 18:02:32 +09:00
root
692c2392fd 메인 보내기 2025-12-26 20:19:38 +09:00
root
7e40126da4 jwt start 2025-12-24 18:00:24 +09:00
cb7f468d95 사용자정보 가져오기 성공 2025-12-22 23:21:00 +09:00
root
b504ac0f93 사용자정보조회2 2025-12-22 12:58:51 +09:00
root
6a0bc5e38d 토큰받기 성공, 사용자정보조회 시작 2025-12-22 12:44:10 +09:00
root
d59df7a223 토큰받기까지 성공 2025-12-17 16:58:54 +09:00
root
030d3c4ceb interceptor 2025-09-15 13:01:59 +09:00
root
ec2201278f 로그인 성공 2025-09-09 12:59:32 +09:00
root
840ce1d4ce 수정중 2025-09-04 13:01:13 +09:00
root
5762f80bcd login success 2025-08-27 13:01:00 +09:00
root
5bd68b2fcb webclient 2025-08-12 18:05:28 +09:00
root
603b992dd1 webclient 2025-08-04 12:57:54 +09:00
root
79b98f4321 login 2025-07-30 12:59:47 +09:00
root
8dc0bac98c 로그인 성공 데이터 받아야함 2025-07-25 16:25:41 +09:00
root
8132000b92 프론트에서 보내도록 수정해야함 2025-07-22 18:33:09 +09:00
hanwha
05406e7b7a CORS setting 2025-07-21 11:52:46 +09:00
20 changed files with 516 additions and 37 deletions

2
prd/.gitignore vendored
View File

@@ -34,4 +34,4 @@ out/
/.nb-gradle/
### VS Code ###
.vscode/
.vscode/*

View File

@@ -39,6 +39,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
//webclient
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
tasks.named('test') {

0
prd/gradlew vendored Normal file → Executable file
View File

View File

@@ -0,0 +1,37 @@
package site.ocr.prd;
import java.util.Enumeration;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@Component
public class SessionLoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//현재 세션이 있으면 가져오기, 없으면 null
HttpSession sessoin = request.getSession(false);
if (sessoin != null) {
StringBuilder sb = new StringBuilder("[SESSION DEBUG] :: ");
//세션ID(누구 세션인지)
sb.append("ID=").append(sessoin.getId()).append("\\n");
//세션값
Enumeration<String> attrvName = sessoin.getAttributeNames();
while (attrvName.hasMoreElements()) {
String name = attrvName.nextElement();
String value = (String) sessoin.getAttribute(name);
sb.append("NAME=").append(name).append(", ").append("VALUE=").append(value).append("\\n");
}
System.out.println(sb.toString());
} else {
System.out.println("[SESSION DEBUG] :: No session found.");
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
}

View File

@@ -1,16 +0,0 @@
package site.ocr.prd;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true);
}
}

View File

@@ -0,0 +1,37 @@
package site.ocr.prd.components;
import java.security.Key;
import java.util.Date;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
@Component
public class JwtProvider {
// jwt용 secret key
private final Key key;
// jwt토큰 유지 시간 = 1시간
private final long expireMillies = 1000 * 60 * 60;
public JwtProvider(@Value("${jwt.secret}")String secrest) {
byte[] ketByte = Decoders.BASE64.decode(secrest);
this.key = Keys.hmacShaKeyFor(ketByte);
}
public String createJwtToken(String id) {
String result = Jwts.builder()
.setSubject(id)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireMillies))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
return result;
}
}

View File

@@ -0,0 +1,36 @@
package site.ocr.prd.config;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors(cors -> {
CorsConfigurationSource configurationSource = request -> { //되게 스크립트같다...
CorsConfiguration configuration = new CorsConfiguration();
//여러개 추가할거면 addAllowd~가 아니라 setAllowedOrigins 사용하기
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(List.of("GET","POST", "PUT","DELETE","OPTION"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
return configuration;
};
cors.configurationSource(configurationSource);
})
.csrf(AbstractHttpConfigurer::disable) //대체 언제 이런 문법이 생겼냐;
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()); //외부 요청은 필터가 다 거르는 형태였기때문에 허용 해줘야함
return http.build();
}
}

View File

@@ -0,0 +1,21 @@
package site.ocr.prd.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder()
.build();
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

View File

@@ -0,0 +1,21 @@
package site.ocr.prd.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 site.ocr.prd.SessionLoggingInterceptor;
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private SessionLoggingInterceptor sessionLoggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(sessionLoggingInterceptor).addPathPatterns("/**");
}
}

View File

@@ -1,18 +1,50 @@
package site.ocr.prd.contorllers;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class ImgController {
private final RestTemplate restTemplate;
@Autowired
public ImgController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@PostMapping("img/get-ocr")
public String postGetImgOcr(@RequestBody String entity) {
//TODO: process POST request
@PostMapping("img/get-img")
public ResponseEntity<String> postGetImg(@RequestPart("image") MultipartFile image) {
System.out.println("image :: " + image.getResource().getFilename());
return entity;
//헤더 설정(이미지)
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
//바디에 이미지 파일 추가
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("image", image.getResource());
//요청 엔티티 생성
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, headers);
//요청 전송
ResponseEntity<String> response =
restTemplate.postForEntity("http://127.0.0.1:9002/ocr",
requestEntity, String.class);
return ResponseEntity.ok(response.getBody());
}
}

View File

@@ -1,18 +1,124 @@
package site.ocr.prd.contorllers;
import java.util.Map;
import org.slf4j.Logger;
import java.util.HashMap;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.PostMapping;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import jakarta.servlet.http.HttpServletRequest;
import site.ocr.prd.dto.LoginReqDTO;
import site.ocr.prd.dto.LoginResDTO;
import site.ocr.prd.dto.UserInfoInqyReqDTO;
import site.ocr.prd.dto.UserInfoInqyResDTO;
import site.ocr.prd.dto.UserInfoResDTO;
import site.ocr.prd.services.LoginService;
import org.springframework.web.bind.annotation.RequestParam;
@RestController
@Controller
public class LoginController {
@PostMapping("login/oauth-kakao")
public String postMethodName(@RequestBody String entity) {
//Logger 선언
private static final Logger logger = LoggerFactory.getLogger(LoginController.class);
//service 선언
private final LoginService loginService;
public LoginController(LoginService loginService) {
this.loginService = loginService;
}
/**
* 프론트에서 카카오로 로그인 요청 후 카카오에서 리다이렉트 해준 인가코드로 토큰 발급 및 사용자정보 조회
* @param redirectRespn 카카오에서 리다이렉트해준 인가코드
*/
@GetMapping("/oauth/oauth-kakao-authorize") //kakao에서 get으로 리다이렉트 해줌
public ResponseEntity<Map<String, String>> kakaoLoginRequest(HttpServletRequest redirectRespn) {
String code = redirectRespn.getParameter("code");
logger.info("인가코드 :: " + code);
/**
* 카카오에 토큰값 요청
* requestDTO
* @param grant_type
* @param client_id 카카오콘솔의 app key
* @param code 인가코드
*/
LoginReqDTO loginRequest = new LoginReqDTO();
loginRequest.setGrant_type("authorization_code");
loginRequest.setClient_id("a1d6afef2d4508a10a498b7069f67496");
loginRequest.setRedirect_uri("http://localhost:9001/oauth/oauth-kakao-authorize");
loginRequest.setCode(code);
/**
* 카카오로부터 받은 토큰값
* @param access_token 사용자정보 조회용 토큰
* @param expires_in 토큰 유효시간(초)
* @param refresh_token 사용자 리프레시 토큰
*/
LoginResDTO loginResult = loginService.getToken(loginRequest);
logger.info("토큰발급 결과 :: " + loginResult.toString());
UserInfoInqyReqDTO userInfoInqyRequest = new UserInfoInqyReqDTO();
userInfoInqyRequest.setAccess_token(loginResult.getAccess_token());
/**
* 사용자정보 조회
* @param access_token 사용자정보 조회용 토큰
*/
UserInfoInqyResDTO userInfoInqyResponse = loginService.getUserInfo(userInfoInqyRequest);
logger.info("사용자정보 :: " + userInfoInqyResponse.toString());
ResponseCookie cookie = loginService.createJwtCookie(userInfoInqyResponse.getId());
Map<String, String> response = new HashMap<>();
response.put("success", "true");
response.put("userId", userInfoInqyResponse.getId());
response.put("message", "Login successful");
response.put("userInfo", userInfoInqyResponse.getName());
return ResponseEntity.status(302)
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.header(HttpHeaders.LOCATION, "http://localhost:3000/pages/oauth/callback")
.body(response);
}
/**
* JWT 토큰 조회 (프론트에서 로그인 상태 확인용)
* @param userId 사용자 ID
* @return 로그인 결과 및 JWT 토큰
*/
@GetMapping("/oauth/get-jwt-token")
public ResponseEntity<Map<String, String>> getJwtToken(@RequestParam String userId) {
ResponseCookie cookie = loginService.createJwtCookie(userId);
Map<String, String> response = new java.util.HashMap<>();
response.put("token", cookie.toString());
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(response);
}
@GetMapping("login/oauth-kakao-token")
public String kakaoLoginResponseDto(@RequestBody LoginResDTO response) {
//TODO: process POST request
System.out.println(entity);
return entity;
System.out.println("response :: ");
System.out.println(response.getAccess_token());
return response.getAccess_token();
}
@GetMapping("login/get-user-info")
public ResponseEntity<UserInfoResDTO> getUserInfo(@RequestParam HttpServletRequest request) {
UserInfoResDTO result = new UserInfoResDTO();
return ResponseEntity.status(200).build();
}
}

View File

@@ -0,0 +1,25 @@
package site.ocr.prd.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@Getter
@Setter
public class LoginReqDTO {
@JsonProperty("grant_type")
String grant_type; //authorization_code
@JsonProperty("client_id")
String client_id; //앱 REST API 키
@JsonProperty("redirect_uri")
String redirect_uri; //인가코드가 리다이렉트된 uri
@JsonProperty("code")
String code; //인가코드 요청으로 얻은 인가코드
}

View File

@@ -0,0 +1,33 @@
package site.ocr.prd.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@Getter
@Setter
public class LoginResDTO {
@JsonProperty("token_type")
String token_type; //토큰타입, bearer 고정
@JsonProperty("access_token")
String access_token; //사용자 엑세스 토큰 값
@JsonProperty("id_token")
String id_token; //ID 토큰값, openID connect가 활성화된 경우만 발급
@JsonProperty("expires_in")
Integer expires_in; //엑세스토큰과 id토큰의 만료시간(초))
@JsonProperty("refresh_token")
String refresh_token; //사용자 리프레시 토큰 값
@JsonProperty("refresh_token_expires_in")
Integer refresh_token_expires_in; //리프레시 토큰 만료 시간(초)
String scope; //인증된 사용자의 정보조회 범위, 여러개일 경우 공백으로 구분
}

View File

@@ -0,0 +1,16 @@
package site.ocr.prd.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@Getter
@Setter
public class UserInfoInqyReqDTO {
@JsonProperty("access_token")
String access_token;
}

View File

@@ -0,0 +1,17 @@
package site.ocr.prd.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@Getter
@Setter
public class UserInfoInqyResDTO {
String id;
String name;
String email;
String age_range;
String birthday;
String gender;
}

View File

@@ -0,0 +1,13 @@
package site.ocr.prd.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@Getter
@Setter
public class UserInfoResDTO {
String userId;
String userName;
}

View File

@@ -0,0 +1,93 @@
package site.ocr.prd.services;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import com.fasterxml.jackson.databind.JsonNode;
import site.ocr.prd.components.JwtProvider;
import site.ocr.prd.dto.LoginReqDTO;
import site.ocr.prd.dto.LoginResDTO;
import site.ocr.prd.dto.UserInfoInqyReqDTO;
import site.ocr.prd.dto.UserInfoInqyResDTO;
@Service
public class LoginService {
//logger 선언
private static final Logger logger = LoggerFactory.getLogger(LoginService.class);
//webclient builder
private final WebClient webClient;
//JWT provider
private final JwtProvider jwtProvider;
public LoginService(WebClient.Builder builder, JwtProvider jwtProvider) {
this.webClient = builder.build();
this.jwtProvider = jwtProvider;
}
public LoginResDTO getToken(LoginReqDTO request) {
logger.info("kakao auth code :: " + request.getCode());
LoginResDTO result = webClient.post()
.uri("https://kauth.kakao.com/oauth/token")
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=utf-8")
.body(BodyInserters.fromFormData("grant_type", "authorization_code")
.with("client_id", "a1d6afef2d4508a10a498b7069f67496")
.with("redirect_uri", "http://localhost:9001/oauth/oauth-kakao-authorize")
.with("code", request.getCode()))
.retrieve()
.bodyToMono(LoginResDTO.class)
.block();
return result;
}
public UserInfoInqyResDTO getUserInfo(UserInfoInqyReqDTO request) {
// 응답 DTO
UserInfoInqyResDTO result = new UserInfoInqyResDTO();
JsonNode root = webClient.get()
.uri("https://kapi.kakao.com/v2/user/me")
.header(HttpHeaders.AUTHORIZATION, "Bearer " + request.getAccess_token())
.retrieve()
.bodyToMono(JsonNode.class)
.block();
//사용자ID
//jsonnode :: {"id":4438121341,"connected_at":"2025-09-09T03:53:23Z"}
logger.info("jsonnode :: " + root.toString());
String id = root.path("id").asText();
String name = root.path("name").asText();
String email = root.path("email").asText();
result.setId(id);
result.setName(name);
result.setEmail(email);
return result;
}
/**
* JWT 토큰을 생성하고 ResponseCookie로 변환
* @param userId 사용자 ID
* @return ResponseCookie JWT 토큰이 포함된 쿠키
*/
public ResponseCookie createJwtCookie(String userId) {
String jwt = jwtProvider.createJwtToken(userId);
return ResponseCookie.from("accessToken", jwt)
.httpOnly(true)
.secure(true)
.sameSite("Lax")
.maxAge(Duration.ofHours(1))
.build();
}
}

View File

@@ -1,7 +1,11 @@
spring:
profiles: DEV
config:
activate:
on-profile:
- DEV
server:
port: 9001
logging:
level:
root: debug
root: debug
org.springframework.security: DEBUG

View File

@@ -1,6 +1,7 @@
#로컬 실행시 기본 프로필
spring:
config:
activate:
on-profile:
- DEV
profiles:
active:
- DEV
jwt:
secret: tOrtq6iI5i2Zjs83qRVzdhyd3D4WXjmcsNZ9Gljhr+dz7cUtLWlkD9shYdpgALCdXplEGJDJoyBeTCfY5Fwb3Q==