일단 세종대학교 auth를 왜 제작했는가?
아 잠시 그전에 만들게 된 계기는 현재 학과 예약시스템으로 인해 제작하는 상황이다. 예약 시스템은 보통 학교마다 다 있지 않나?라고 생각하지만 이 세미나실은 학과 학생들만 쓸 수 있는 세미나실이며, 여기서 미팅, 회의를 대학원생, 교수님들도 쓸 수 있게 열어둔 세미나실이라 학교측에서 따로 시스템을 만들기 어렵기에 구축이 안되있는 상황이었다.
그리고 현재는 아래 양식을 사용하여 직접 학과사무실에서 이름, 날짜, 사용목적 등을 적고 사용하는 방식이었다. 그로인해 학생, 교수님들 불편함을 느끼고 사이트로 만들자는 취지였다.
프로젝트를 진행 중 가장 큰 어려움을 겪은 부분은 예약시스템에서 어떻게 이용자가 학생인지 외부인인지, 그 해당학과 사람인지를 구분해야되는 문제가 생겼다. 이걸 구분하지 않으면 우리 시스템은 의미가 없다고 판단했다.
그래서 결국 3가지 생각을 했는데
1. 학번, 이름을 함께 예약을 받아 관리자가 승인하는 방식. (관리자 신뢰)
2. 세종대학교 포털에서 정보를 받아와 사용자의 정보를 가져오는 방식. (학교 포털 신뢰)
3. 패스워드 방식.(이용자 신뢰)
사실 2번이 가장 좋고 보안이 철저해서 2번으로 진행하고 있었으나 학교 포털의 api방식이나 구조가 바뀌면 우리 시스템도 전부 바꿔야되는 문제점이 있었다.
근데 뭐 설마 학교 포털에서 바뀌겠어?? 라고 생각했는데 놀랍게도 2025년 1월 2일에 로직이 바뀌어 기존 작성했던 코드가 돌아가지 않게 되었다.. 기존 http였던 JSESSION방식이 아예 다 바뀌어버려 못쓰는 로직이 되었고 토큰으로 구성되어 더 이상 2번이 불가능하다고 생각해서 3번인 패스워드 방식을 사용하려고 로직을 갈아끼우고 있었는데 그러면 교내 사이트랑 연관된 사이트들은 이걸 어떻게 수정했지??를 찾아보다가 수정된 코드들을 보고 답을 찾을 수 있었다.
기존에 잘 작동하던 코드다
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(String userId, String password) {
LoginReq loginReq = new LoginReq(userId, password);
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<String, List<String>> headers = connection.getHeaderFields();
List<String> 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=" + URLEncoder.encode(loginReq.getUserId(), StandardCharsets.UTF_8.toString()) +
"&password=" + URLEncoder.encode(loginReq.getPassword(), StandardCharsets.UTF_8.toString());
try (OutputStream os = connection.getOutputStream()) {
byte[] input = postData.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
int responseCode = connection.getResponseCode();
String responseMessage = readResponse(connection);
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;
}
}
메서드를 정리하자면
fetchJsessionId: JSESSIONID를 가져오는 과정.
attemptLogin: 로그인 요청을 보내고 성공 여부를 확인.
authenticate: 위 두 과정을 통합하여 사용자 인증을 처리.
기존 잘 작동하던 로직이다. 학교 포탈에 커넥션을 맺고 JsessionId로 학교 포탈의 개인정보란에 들어가 정보를 Jsoup으로 가져오는 코드였다.
그러나
갑자기 하나만 있던 JSEESIONID는 여러개가 되었고 로그인을 하니 여러개가 뜨면서 token도 하나가 발급된 것을 볼 수 있었다.
각각을 gpt한테 물어보니
- 세션 관리:
- JSESSIONID와 같은 쿠키는 사용자의 세션을 유지하기 위해 서버에서 사용됩니다.
- 인증:
- LOGIN_INFO나 ssotoken은 인증 상태를 유지하거나, 로그인한 사용자를 식별하는 데 사용됩니다.
- 사용자 맞춤화:
- PREF와 같은 쿠키는 사용자 환경을 맞춤화하기 위해 설정될 수 있습니다.
- 보안:
- Secure 및 HttpOnly 플래그가 설정된 쿠키는 세션 하이재킹 및 XSS 공격으로부터 보호합니다.
라고 한다. 기존에는
JSESSIONID 하나로 http 세션을 유지하고 사용자 정보도 SessionId로만 관리하였으나, 해당 방식의 문제점은 교내 다른 사이트를 간다면 그 세션이 유지되지 않아 다시 로그인해야된다는 점을 발견하고 아마 SSOToken으로 바꾼 것 같다.
SSOToken을 찾아보니 Single Sign-On 사용자 인증 상태를 유지하고 여러 애플리케이션 간의 인증을 편리하게 공유하는 토큰이라고 한다.
인증 서버를 통해 SSO 토큰을 발급하면 인증 도메인 내의 여러 사이트에서 통합되는 상태를 가지며 보통 JWT 형태로 발급된다고 한다. 그래서 생각해본 결과 기존 JSESSION을 가지고 정보를 파싱하는 방법이 먹히지 않았던 것이고 이 SSO 토큰을 가지고 파싱을 하면 해결 될 것이라고 생각했다.
그리고 해당 로그인 페이지의 id 값으로 post 요청을 만든 뒤
로직을 이부분과
String cookie = connection.getHeaderField("Set-Cookie");
if (cookie != null && cookie.contains("ssotoken=")) {
ssoToken = cookie.substring(cookie.indexOf("ssotoken=") + 9, cookie.indexOf(";", cookie.indexOf("ssotoken=")));
return true;
}
return false;
이부분을 수정해주니
String postData = "mainLogin=Y" +
"&rtUrl=" + URLEncoder.encode("https://classic.sejong.ac.kr/classic/index.do", StandardCharsets.UTF_8.toString()) +
"&id=" + URLEncoder.encode(loginReq.getUserId(), StandardCharsets.UTF_8.toString()) +
"&password=" + URLEncoder.encode(loginReq.getPassword(), StandardCharsets.UTF_8.toString()) +
"&chkNos=on";
테스트가 잘 나오는 것을 확인할 수 있었다.
그래도 이번에 학교 사이트가 진화한다는 것을 처음알았고 SSOToken과 Jsession의 차이를 좀 공부할 수 있었다.
'Project' 카테고리의 다른 글
[Project] Multipart/form 파일, Dto 동시 요청 시 발생 에러 (1) | 2025.01.16 |
---|---|
[Project] 온프레미스 서버 구축기 (0) | 2025.01.16 |
[Project] Docker 적용기 (8) | 2024.10.16 |