Skip to content

Commit

Permalink
feat: support jwch user login
Browse files Browse the repository at this point in the history
  • Loading branch information
ozline committed Dec 31, 2024
1 parent c8bd809 commit ed635e0
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 42 deletions.
187 changes: 152 additions & 35 deletions app/(tabs)/user.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,62 @@
import { Buffer } from 'buffer';
import React, { useState } from 'react';
import { Alert, Button, Image, Text, View } from 'react-native';
import React, { useRef, useState } from 'react';
import { Alert, Button, Image, Text, TextInput, View } from 'react-native';

import { ThemedView } from '@/components/ThemedView';
import { get, post } from '@/modules/native-request';

const URLS = {
LOGIN_CHECK: 'https://jwcjwxt1.fzu.edu.cn/logincheck.asp',
VERIFY_CODE: 'https://jwcjwxt1.fzu.edu.cn/plus/verifycode.asp',
SSO_LOGIN: 'https://jwcjwxt2.fzu.edu.cn/Sfrz/SSOLogin',
LOGIN_CHECK_XS: 'https://jwcjwxt2.fzu.edu.cn:81/loginchk_xs.aspx',
GET: 'https://www.baidu.com',
};

const HEADERS = {
REFERER: 'https://jwch.fzu.edu.cn',
ORIGIN: 'https://jwch.fzu.edu.cn',
};

const STUDENT = {
ID: 'student-id',
PASSWORD: 'student-password-md5-16',
};

interface Cookie {
[key: string]: string;
}

let cookies: Cookie = {};

export default function HomePage() {
const [dictionary, setDictionary] = useState<{ [key: string]: string }>({});
const [imageUrl, setImageUrl] = useState<string | null>(null); // 用于显示验证码图片
const [captcha, setCaptcha] = useState<string>(''); // 用于输入验证码

const updateCookies = (newCookies: string) => {
const cookieArray = newCookies.split(',').map(cookie => cookie.trim());
const updatedCookies: { [key: string]: string } = { ...cookies };

const urlLoginCheck = 'https://jwcjwxt1.fzu.edu.cn/logincheck.asp';
const urlVerifyCode = 'https://jwcjwxt1.fzu.edu.cn/plus/verifycode.asp';
const urlGet = 'https://www.baidu.com';
const headers = {
Referer: 'https://jwch.fzu.edu.cn',
Origin: 'https://jwch.fzu.edu.cn',
Cookie: 'ASPSESSIONIDAGRSTDCC=JADPAJABIJOFHMALKENMNHCP',
cookieArray.forEach(cookie => {
const [key, value] = cookie.split(';')[0].split('=');
if (updatedCookies[key]) {
updatedCookies[key] = value; // 更新现有的 cookie
} else {
updatedCookies[key] = value; // 添加新的 cookie
}
});
cookies = updatedCookies;
console.log('Updated cookies:', updatedCookies);
// 处理后的 cookies可能长这样
// {"ASPSESSIONIDCGTRTCDD": "PDHEKIDAJILFAFPPHEDEPDKP", "Learun_ADMS_V7_Mark": "eac94f18-be04-4e74-81cd-96fa6b16251d", "Learun_ADMS_V7_Token": "a25f83b3-6147-4e4e-9d77-e718e0aa83c2"}
};
const formData = {
Verifycode: '111',
muser: 'student-id',
passwd: 'student-password',

const handleError = (error: any) => {
Alert.alert('错误', String(error));
};

const handlePress = async (
const requestPOST = async (
url: string,
headers: Record<string, string>,
formData: Record<string, string>,
Expand All @@ -35,18 +68,16 @@ export default function HomePage() {
headers: respHeaders,
} = await post(url, headers, formData);
setDictionary(respHeaders);
Alert.alert(
'结果',
respStatus +
'\n' +
JSON.stringify(Buffer.from(respData).toString('utf-8')), // 这里默认了 PSOT 返回的是 JSON 数据
);
if (respHeaders['Set-Cookie']) {
updateCookies(respHeaders['Set-Cookie']);
}
return respData;
} catch (error) {
Alert.alert('错误', String(error));
handleError(error);
}
};

const handlePressGet = async (
const requestGET = async (
url: string,
headers: Record<string, string>,
isBinary = false, // 是否为二进制数据,如果是的话转为 base64输出(只是测试,我们认为二进制数据就是图片)
Expand All @@ -58,19 +89,87 @@ export default function HomePage() {
headers: respHeaders,
} = await get(url, headers);
setDictionary(respHeaders);
// 根据 Content-Type 处理响应数据(可能需要内置一个映射表?)
if (respHeaders['Set-Cookie']) {
updateCookies(respHeaders['Set-Cookie']);
}
if (isBinary) {
// 图片
const base64Data = btoa(String.fromCharCode(...respData));
const imageUrl = `data:image/png;base64,${base64Data}`;
setImageUrl(imageUrl); // 保存图片 URL 到状态
} else {
// 其他(默认为文本)
const responseData = Buffer.from(respData).toString('utf-8'); // 使用 Buffer 解码
Alert.alert('结果', respStatus + '\n' + responseData);
setImageUrl(
`data:image/png;base64,${btoa(String.fromCharCode(...respData))}`,
);
}
return respData;
} catch (error) {
Alert.alert('错误', String(error));
handleError(error);
}
};

const handleSubmitCaptcha = async () => {
if (!captcha) {
Alert.alert('错误', '请输入验证码');
return;
}
// Login Check
const respdata = await requestPOST(
URLS.LOGIN_CHECK,
{
...HEADERS,
Cookie: Object.entries(cookies)
.map(([k, v]) => `${k}=${v}`)
.join('; '),
},
{
muser: STUDENT.ID,
passwd: STUDENT.PASSWORD,
Verifycode: captcha,
},
);
// 我们禁用了 302 重定向,因此这个提取是从 URL 中提取的(但注意到教务处的 HTTP 报文 Body 也提供了 URL,我们从 Body 中入手,不走 Header 的 Location 字段)
var dataStr = Buffer.from(respdata).toString('utf-8').replace(/\s+/g, '');
const tokenMatch = /token=(.*?)&/.exec(dataStr);
const idMatch = /id=(.*?)&/.exec(dataStr);
const numMatch = /num=(.*?)&/.exec(dataStr);
if (!tokenMatch) {
Alert.alert('错误', '缺失 Token');
return;
}
const token = tokenMatch[1];
const id = idMatch ? idMatch[1] : '';
const num = numMatch ? numMatch[1] : '';
console.log('Token:', token, 'ID:', id, 'Num:', num);

// SSOLogin
const respSSOData = await requestPOST(
URLS.SSO_LOGIN,
{
'X-Requested-With': 'XMLHttpRequest',
},
{
token: token,
},
);
// 正常响应应当为:{"code":200,"info":"登录成功","data":{}}
dataStr = Buffer.from(respSSOData).toString('utf-8').replace(/\s+/g, '');

// account conflict 是 400,正常则是 200
if (JSON.parse(dataStr).code == 200) {
const updatedCookies = Object.entries(cookies)
.map(([k, v]) => `${k}=${v}`)
.join('; ');
// LoginCheckXS
const respCookiesData = await requestGET(
URLS.LOGIN_CHECK_XS +
`?id=${id}&num=${num}&ssourl=https://jwcjwxt2.fzu.edu.cn&hosturl=https://jwcjwxt2.fzu.edu.cn:81&ssologin=`,
{
...HEADERS,
Cookie: updatedCookies,
},
);
// 后续会在 Response Header 中获取 Cookie,这个是我们所需的
const dataStr = Buffer.from(respCookiesData)
.toString('binary')
.replace(/\s+/g, '');
const idMatch = /id=(.*?)&/.exec(dataStr);
console.log('ID:', idMatch ? idMatch[1] : '');
}
};

Expand All @@ -80,15 +179,33 @@ export default function HomePage() {
<Text>User</Text>
<Button
title="获取数据POST"
onPress={() => handlePress(urlLoginCheck, headers, formData)}
onPress={() => requestPOST(URLS.LOGIN_CHECK, HEADERS, {})}
/>
<Button
title="获取数据GET"
onPress={() => handlePressGet(urlGet, headers)}
onPress={() => requestGET(URLS.GET, HEADERS)}
/>
<Button
title="获取验证码图片"
onPress={() => handlePressGet(urlVerifyCode, headers, true)}
onPress={() => requestGET(URLS.VERIFY_CODE, {}, true)}
/>
<TextInput
value={captcha}
onChangeText={setCaptcha}
placeholder="请输入验证码"
style={{
borderWidth: 1,
borderColor: '#ccc',
margin: 10,
padding: 5,
}}
/>
<Button title="尝试进行登录" onPress={handleSubmitCaptcha} />
<Button
title="清空 Cookie"
onPress={() => {
cookies = {};
}}
/>
{Object.entries(dictionary).map(([key, value]) => (
<Text key={key}>
Expand Down
8 changes: 6 additions & 2 deletions modules/native-request/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import NativeRequestModule from './src/NativeRequestModule';
// iOS 原始响应格式为 { status: Int?, data: Data?, headers: Record<string, string>, error: String? }
// Android 原始响应格式为 { status: Int, data: ByteArray?, headers: Map<String, List<String>> }
export async function get(url: string, headers: Record<string, string>) {
const response = await NativeRequestModule.get(url, headers);
const response = await NativeRequestModule.get(url, { data: headers });
if (response.error) {
throw new Error(response.error);
}
Expand All @@ -25,7 +25,11 @@ export async function post(
headers: Record<string, string>,
formData: Record<string, string>,
) {
const response = await NativeRequestModule.post(url, headers, formData);
const response = await NativeRequestModule.post(
url,
{ data: headers },
{ data: formData },
);
if (response.error) {
throw new Error(response.error);
}
Expand Down
22 changes: 17 additions & 5 deletions modules/native-request/ios/NativeRequestModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public class NativeRequestModule: Module {
AsyncFunction("get") { (url: String, headers: StringMapper) -> (ResponseMapper) in
// 创建 Alamofire 的 Session,启用 HTTPCookieStorage
let configuration = URLSessionConfiguration.af.default
configuration.httpCookieStorage = HTTPCookieStorage.shared
let session = Alamofire.Session(configuration: configuration)
configuration.httpCookieStorage = nil // 禁用系统共享的 Cookie 存储
let session = Alamofire.Session(configuration: configuration, redirectHandler: NoRedirectHandler())

var resp = ResponseMapper(status: -1, data: nil, headers: [:], error: nil)
do {
Expand All @@ -50,8 +50,8 @@ public class NativeRequestModule: Module {
AsyncFunction("post") { (url: String, headers: StringMapper, formData: StringMapper) -> (ResponseMapper) in
// 创建 Alamofire 的 Session,启用 HTTPCookieStorage
let configuration = URLSessionConfiguration.af.default
configuration.httpCookieStorage = HTTPCookieStorage.shared
let session = Alamofire.Session(configuration: configuration)
configuration.httpCookieStorage = nil // 禁用系统共享的 Cookie 存储
let session = Alamofire.Session(configuration: configuration, redirectHandler: NoRedirectHandler())
var resp = ResponseMapper(status: -1, data: nil, headers: [:], error: nil)
do{
let response = await session.request(url, method: .post, parameters: formData.data, encoder: URLEncodedFormParameterEncoder.default, headers: HTTPHeaders(headers.data)).serializingData().response
Expand All @@ -65,4 +65,16 @@ public class NativeRequestModule: Module {
}
}
}
}
}

// 自定义 RedirectHandler 来避免自动重定向
class NoRedirectHandler: RedirectHandler {
func task(_ task: URLSessionTask, willBeRedirectedTo request: URLRequest, for response: HTTPURLResponse, completion: @escaping (URLRequest?) -> Void) {
// 如果状态码为 302,则阻止重定向
if response.statusCode == 302 {
completion(nil)
} else {
completion(request)
}
}
}

0 comments on commit ed635e0

Please sign in to comment.