From 031b85ffa8aef6db5d541eb66e135db3f1c31772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 11:31:26 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=ED=8C=8C=EC=8B=B1=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index f9f019d..87b29dc 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,7 @@ dependencyManagement { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2' + implementation 'org.jsoup:jsoup:1.14.3' compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok:1.18.26' testImplementation 'org.springframework.boot:spring-boot-starter-test' From 705fea5f04a3e2d1ce926cc172885101cc5bd7c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 11:31:57 +0900 Subject: [PATCH 02/10] =?UTF-8?q?test:=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/yj/sejongauth/Test.java | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/main/java/org/yj/sejongauth/Test.java diff --git a/src/main/java/org/yj/sejongauth/Test.java b/src/main/java/org/yj/sejongauth/Test.java deleted file mode 100644 index 5d9d84e..0000000 --- a/src/main/java/org/yj/sejongauth/Test.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.yj.sejongauth; - -public class Test { - public static void Console(){ - System.out.println("test"); - } -} From d27ce9d9f800b118e356170be3ce97020bf4c5c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:53:17 +0900 Subject: [PATCH 03/10] =?UTF-8?q?docs:=20=EC=84=A4=EB=AA=85=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c41d33 --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +# SejongAuth + +SejongAuth는 세종대학교 통합 로그인 페이지에 접근하여 사용자 인증과 프로필 정보를 가져오는 Java 기반의 패키지입니다. `Sj`를 통해 세종대학교 포털에 간편하게 로그인하고 사용자의 학적 정보를 조회할 수 있습니다. + +## 주요 기능 + +- **로그인 기능**: `LoginReq`를 사용하여 세종대학교 포털에 로그인하고 `JSESSIONID`를 획득합니다. +- **프로필 조회 기능**: 로그인 성공 시 `ProfileService`를 통해 사용자의 학적 정보를 가져옵니다. + +--- + +## 설치 방법 + +gradle +```properties +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + maven { url 'https://jitpack.io' } + } + } +dependencies { + implementation 'com.github.urinaner:sejong-auth:Tag' + } +``` +maven +```yaml + + + jitpack.io + https://jitpack.io + + + + + com.github.urinaner + sejong-auth + Tag + +``` + + +--- + +## 프로젝트 구조 + +``` +SejongAuth/ +├── src/ +│ ├── main/ +│ │ ├── java/org/yj/sejongauth/controller/Sj.java +│ │ ├── java/org/yj/sejongauth/domain/AuthService.java +│ │ ├── java/org/yj/sejongauth/domain/LoginReq.java +│ │ ├── java/org/yj/sejongauth/domain/ProfileRes.java +│ │ └── java/org/yj/sejongauth/domain/ProfileService.java +│ └── test/ +│ └── java/org/yj/sejongauth/controller/AuthControllerTest.java +└── build.gradle +``` + +--- + +## 사용법 + +### 1. `LoginRequestDto` 생성 + +로그인 요청 객체를 생성합니다. + +```java +LoginRequestDto loginRequestDto = new LoginRequestDto("userId", "password"); +``` + +### 2. 로그인 및 프로필 조회 + +`Sj.login()` 메서드를 사용하여 로그인 및 프로필 정보를 조회할 수 있습니다. + +```java +ProfileRes profile = Sj.login(loginReq); +System.out.println("User profile: " + profile); +``` + +--- + +## 예제 + +아래는 `Sj.login` 메서드를 호출하여 간단히 로그인과 프로필 조회를 수행하는 예제입니다. + +```java + +public class Main { + public static void main(String[] args) { + LoginReq loginReq = new LoginReq("testUser", "testPassword"); + + try { + ProfileRes profile = Sj.login(loginReq); + System.out.println("Login successful. User profile: " + profile); + } catch (RuntimeException e) { + System.err.println("Login failed: " + e.getMessage()); + } + } +} +``` + +--- + +## 테스트 + +`AuthService` 클래스에서 로그인에 대한 테스트를 실행할 수 있습니다. + + +--- + +## 주의사항 + +- **네트워크 연결**: 이 프로그램은 네트워크 연결이 필요합니다. +- **예외 처리**: 네트워크 오류, 로그인 실패 등의 상황에서 `RuntimeException`이 발생할 수 있습니다. \ No newline at end of file From c1f409dbd87b4325165a0f1546001f96533d721c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:53:29 +0900 Subject: [PATCH 04/10] feat: LoginReq --- .../org/yj/sejongauth/domain/LoginReq.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/org/yj/sejongauth/domain/LoginReq.java diff --git a/src/main/java/org/yj/sejongauth/domain/LoginReq.java b/src/main/java/org/yj/sejongauth/domain/LoginReq.java new file mode 100644 index 0000000..11a29dd --- /dev/null +++ b/src/main/java/org/yj/sejongauth/domain/LoginReq.java @@ -0,0 +1,19 @@ +package org.yj.sejongauth.domain; + +public class LoginReq { + private final String userId; + private final String password; + + public LoginReq(String userId, String password) { + this.userId = userId; + this.password = password; + } + + public String getUserId() { + return userId; + } + + public String getPassword() { + return password; + } +} \ No newline at end of file From ab1e2790f350558c702cc54c589302cade19ab8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:53:38 +0900 Subject: [PATCH 05/10] feat: ProfileRes --- .../org/yj/sejongauth/domain/ProfileRes.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/main/java/org/yj/sejongauth/domain/ProfileRes.java diff --git a/src/main/java/org/yj/sejongauth/domain/ProfileRes.java b/src/main/java/org/yj/sejongauth/domain/ProfileRes.java new file mode 100644 index 0000000..ef297bc --- /dev/null +++ b/src/main/java/org/yj/sejongauth/domain/ProfileRes.java @@ -0,0 +1,31 @@ +package org.yj.sejongauth.domain; + +public class ProfileRes { + private final String major; + private final String studentCode; + private final String name; + private final int gradeLevel; + private final String userStatus; + private final int completedSemesters; + private final int verifiedSemesters; + + public ProfileRes(String major, String studentCode, String name, + int gradeLevel, String userStatus, + int completedSemesters, int verifiedSemesters) { + this.major = major; + this.studentCode = studentCode; + this.name = name; + this.gradeLevel = gradeLevel; + this.userStatus = userStatus; + this.completedSemesters = completedSemesters; + this.verifiedSemesters = verifiedSemesters; + } + + public String getMajor() { return major; } + public String getStudentCode() { return studentCode; } + public String getName() { return name; } + public int getGradeLevel() { return gradeLevel; } + public String getUserStatus() { return userStatus; } + public int getCompletedSemesters() { return completedSemesters; } + public int getVerifiedSemesters() { return verifiedSemesters; } +} \ No newline at end of file From f30dcb4957d697a092aa3c6b5cc09e8a7cc3c0c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:53:59 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat:=20Auth=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yj/sejongauth/domain/AuthService.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/main/java/org/yj/sejongauth/domain/AuthService.java diff --git a/src/main/java/org/yj/sejongauth/domain/AuthService.java b/src/main/java/org/yj/sejongauth/domain/AuthService.java new file mode 100644 index 0000000..2c975b4 --- /dev/null +++ b/src/main/java/org/yj/sejongauth/domain/AuthService.java @@ -0,0 +1,97 @@ +package org.yj.sejongauth.domain; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.OutputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; + +public class AuthService { + private String jsessionId; + private final String SJ_LOGIN_URL = "https://classic.sejong.ac.kr/userLogin.do"; + private final String SJ_POTAL_URL = "https://classic.sejong.ac.kr"; + private final String INVALID_AUTH = "인증이 실패하였습니다."; + private final String INVALID_SESSION = "SESSION_ID 가져오는 것을 실패하였습니다."; + private final String INVALID_URL = "URL이 유효하지 않습니다."; + private final String CONTAINS_HTML = "로그인 정보가 올바르지 않습니다."; + + public boolean authenticate(LoginReq loginReq) { + try { + fetchJsessionId(); + return attemptLogin(loginReq); + } catch (IOException e) { + throw new RuntimeException(INVALID_AUTH); + } + } + + void fetchJsessionId() throws IOException { + try { + URI uri = new URI(SJ_POTAL_URL); + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + connection.setRequestMethod("GET"); + connection.connect(); + + Map> headers = connection.getHeaderFields(); + List cookies = headers.get("Set-Cookie"); + + if (cookies != null) { + for (String cookie : cookies) { + if (cookie.startsWith("JSESSIONID")) { + jsessionId = cookie.split(";")[0].split("=")[1]; + break; + } + } + } + + if (jsessionId == null) { + throw new RuntimeException(INVALID_SESSION); + } + } catch (URISyntaxException e) { + throw new RuntimeException(INVALID_URL); + } + } + + private boolean attemptLogin(LoginReq loginReq) throws IOException { + try { + URI uri = new URI(SJ_LOGIN_URL); + HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Cookie", "JSESSIONID=" + jsessionId); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + connection.setDoOutput(true); + + String postData = "userId=" + loginReq.getUserId() + "&password=" + loginReq.getPassword(); + try (OutputStream os = connection.getOutputStream()) { + byte[] input = postData.getBytes("utf-8"); + os.write(input, 0, input.length); + } + + int responseCode = connection.getResponseCode(); + System.out.println(responseCode); + String responseMessage = readResponse(connection); + System.out.println(responseMessage); + return responseCode == 302 && !responseMessage.contains(CONTAINS_HTML); + } catch (URISyntaxException e) { + throw new RuntimeException(INVALID_URL); + } + } + + private String readResponse(HttpURLConnection connection) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + return response.toString(); + } + } + + public String getJsessionId() { + return jsessionId; + } +} From 734f1bcbe979bcf9eaabc640b1a25fdf028bcd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:54:03 +0900 Subject: [PATCH 07/10] =?UTF-8?q?test:=20Auth=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yj/sejongauth/domain/AuthServiceTest.java | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/test/java/org/yj/sejongauth/domain/AuthServiceTest.java diff --git a/src/test/java/org/yj/sejongauth/domain/AuthServiceTest.java b/src/test/java/org/yj/sejongauth/domain/AuthServiceTest.java new file mode 100644 index 0000000..ebffb62 --- /dev/null +++ b/src/test/java/org/yj/sejongauth/domain/AuthServiceTest.java @@ -0,0 +1,44 @@ +package org.yj.sejongauth.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.*; + +class AuthServiceTest { + + private AuthService authService; + + @BeforeEach + void setUp() { + authService = new AuthService(); + } + + @Test + void testAuthenticate_Success() throws IOException { + // Given + LoginReq loginReq = new LoginReq("20003210", "991027zZ!"); + authService.fetchJsessionId(); + + //when + boolean isAuthenticated = authService.authenticate(loginReq); + + // then + assertTrue(isAuthenticated); + } + + @Test + void testAuthenticate_Failure() throws IOException { + // Given + LoginReq loginReq = new LoginReq("invalidUser", "invalidPassword"); + + // When + boolean isAuthenticated = authService.authenticate(loginReq); + + // Then + assertFalse(isAuthenticated); + } +} From a83bc066724297daaa5167d4e557f36474198046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:54:16 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20profile=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yj/sejongauth/domain/ProfileService.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/main/java/org/yj/sejongauth/domain/ProfileService.java diff --git a/src/main/java/org/yj/sejongauth/domain/ProfileService.java b/src/main/java/org/yj/sejongauth/domain/ProfileService.java new file mode 100644 index 0000000..67b9c8a --- /dev/null +++ b/src/main/java/org/yj/sejongauth/domain/ProfileService.java @@ -0,0 +1,51 @@ +package org.yj.sejongauth.domain; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +public class ProfileService { + private final String PROFILE_URL = "http://classic.sejong.ac.kr/userCertStatus.do?menuInfoId=MAIN_02_05"; + private final String FAIDED_PROFILE = "정보 조회에 실패하였습니다."; + + public ProfileRes fetchUserProfile(String jsessionId) { + try { + URL url = new URL(PROFILE_URL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Cookie", "JSESSIONID=" + jsessionId); + + Document doc = Jsoup.parse(readResponse(connection)); + return parseProfileFromHtml(doc); + } catch (IOException e) { + throw new RuntimeException(FAIDED_PROFILE); + } + } + + private String readResponse(HttpURLConnection connection) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + response.append(line); + } + return response.toString(); + } + } + + private ProfileRes parseProfileFromHtml(Document document) { + String major = document.select("div.contentWrap li dl dd").get(0).text(); + String studentCode = document.select("div.contentWrap li dl dd").get(1).text(); + String name = document.select("div.contentWrap li dl dd").get(2).text(); + int gradeLevel = Integer.parseInt(document.select("div.contentWrap li dl dd").get(3).text().split(" ")[0]); + String userStatus = document.select("div.contentWrap li dl dd").get(4).text(); + int completedSemesters = Integer.parseInt(document.select("div.contentWrap li dl dd").get(5).text().split(" ")[0]); + int verifiedSemesters = Integer.parseInt(document.select("div.contentWrap li dl dd").get(6).text().split(" ")[0]); + + return new ProfileRes(major, studentCode, name, gradeLevel, userStatus, completedSemesters, verifiedSemesters); + } +} From 17832afc35b239e52d0635b6b85fed1bc99564b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:54:19 +0900 Subject: [PATCH 09/10] =?UTF-8?q?test:=20profile=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sejongauth/domain/ProfileServiceTest.java | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/test/java/org/yj/sejongauth/domain/ProfileServiceTest.java diff --git a/src/test/java/org/yj/sejongauth/domain/ProfileServiceTest.java b/src/test/java/org/yj/sejongauth/domain/ProfileServiceTest.java new file mode 100644 index 0000000..da82637 --- /dev/null +++ b/src/test/java/org/yj/sejongauth/domain/ProfileServiceTest.java @@ -0,0 +1,42 @@ +package org.yj.sejongauth.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class ProfileServiceTest { + + private ProfileService profileService; + + @BeforeEach + void setUp() { + profileService = new ProfileService(); + } + + @Test + @Disabled + void testFetchUserProfile_Success() { + // Given + String jsessionId = "valid-session-id"; + + // When + ProfileRes profile = profileService.fetchUserProfile(jsessionId); + + // Then + assertNotNull(profile); + assertEquals("testMajor", profile.getMajor()); + } + + @Test + void testFetchUserProfile_InvalidSession() { + // Given + String jsessionId = "invalid-session-id"; + + // When & Then + assertThrows(RuntimeException.class, () -> { + profileService.fetchUserProfile(jsessionId); + }); + } +} From 8a5382422261b75f53ea396f014e2deb5287141f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=A5=EC=98=81=EC=9E=AC?= Date: Tue, 5 Nov 2024 16:54:47 +0900 Subject: [PATCH 10/10] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/yj/sejongauth/controller/Sj.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/org/yj/sejongauth/controller/Sj.java diff --git a/src/main/java/org/yj/sejongauth/controller/Sj.java b/src/main/java/org/yj/sejongauth/controller/Sj.java new file mode 100644 index 0000000..84671da --- /dev/null +++ b/src/main/java/org/yj/sejongauth/controller/Sj.java @@ -0,0 +1,21 @@ +package org.yj.sejongauth.controller; + +import org.yj.sejongauth.domain.AuthService; +import org.yj.sejongauth.domain.LoginReq; +import org.yj.sejongauth.domain.ProfileRes; +import org.yj.sejongauth.domain.ProfileService; + +public class Sj { + + private static final AuthService authService = new AuthService(); + private static final ProfileService PROFILE_SERVICE = new ProfileService(); + + public static ProfileRes login(LoginReq loginReq) { + if (authService.authenticate(loginReq)) { + String jsessionId = authService.getJsessionId(); + return PROFILE_SERVICE.fetchUserProfile(jsessionId); + } else { + throw new RuntimeException("인증에 실패하였습니다."); + } + } +} \ No newline at end of file