鉴权

本项目的接口均需要鉴权,鉴权方式为在请求头中添加 token 字段,值为登录后获取的 token

获取方法主要提供以下几种

通过脚本获取

# Python

import re
import base64
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

SSO_URL = "https://sso.fzu.edu.cn/login"
AUTH_URL = "https://sso.fzu.edu.cn/oauth2.0/authorize?response_type=code&client_id=wlwxt&redirect_uri=http://aiot.fzu.edu.cn/api/admin/sso/getIbsToken"
USER_AGENT = 'Mozilla/5.0 (iPad; CPU OS 18_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 appId/cn.edu.fzu.fdxypa appScheme/kysk-fdxy-app hengfeng/fdxyappzs appType/2 ruijie-facecamera'

class SSOLogin:
    """福州大学 SSO 登录类,用于获取学习中心 token"""
    
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update({'User-Agent': USER_AGENT})
    
    def extract_kv(self, raw_text, key):
        """从字符串中提取键值对"""
        if not isinstance(raw_text, str):
            raise ValueError("输入必须是字符串")
        
        pattern = rf'{key}=([^;&]+)'
        match = re.search(pattern, raw_text)
        if not match:
            raise ValueError(f"未找到键: {key}")
        return match.group(1)
    
    def encrypt_password(self, raw_password, key_base64):
        """使用 AES-ECB 模式加密密码"""
        key = base64.b64decode(key_base64)
        cipher = AES.new(key, AES.MODE_ECB)
        padded = pad(raw_password.encode('utf-8'), AES.block_size)
        encrypted = cipher.encrypt(padded)
        return base64.b64encode(encrypted).decode('utf-8')
    
    def login(self, account, password):
        """SSO 登录,返回 cookie"""
        if not account or not password:
            raise ValueError("账号密码不能为空")
        
        # 获取登录页面
        response = self.session.get(SSO_URL)
        html = response.text
        
        # 提取密钥和 execution
        crypto_match = re.search(r'"login-croypto">(.*?)<', html)
        execution_match = re.search(r'"login-page-flowkey">(.*?)<', html)
        if not crypto_match or not execution_match:
            raise ValueError("无法从页面中提取密钥")
        
        crypto = crypto_match.group(1)
        execution = execution_match.group(1)
        
        # 提取 SESSION cookie
        set_cookie = response.headers.get('Set-Cookie', '')
        session_cookie = self.extract_kv(set_cookie, 'SESSION')
        
        # 构建登录数据
        encrypted_password = self.encrypt_password(password, crypto)
        login_data = {
            'username': account,
            'type': 'UsernamePassword',
            '_eventId': 'submit',
            'geolocation': '',
            'execution': execution,
            'captcha_code': '',
            'croypto': crypto,
            'password': encrypted_password,
            'captcha_payload': self.encrypt_password('{}', crypto)
        }
        
        # 发送登录请求
        headers = {'Content-Type': 'application/x-www-form-urlencoded', 'Cookie': f'SESSION={session_cookie}'}
        response = self.session.post(SSO_URL, data=login_data, headers=headers)
        
        # 查找 SOURCEID_TGC
        for cookie in self.session.cookies:
            if cookie.name == 'SOURCEID_TGC':
                return f'SOURCEID_TGC={cookie.value}'
        raise ValueError('学号或密码错误')
    
    def get_study_token(self, sso_cookie):
        """获取学习中心 token"""
        if not sso_cookie:
            raise ValueError("SSO cookie 不能为空,请先登录")
        
        headers = {'Cookie': sso_cookie, 'User-Agent': USER_AGENT}
        response = self.session.get(AUTH_URL, headers=headers, allow_redirects=True)
        
        final_url = response.url
        if 'token=' in final_url:
            return self.extract_kv(final_url, 'token')
        raise ValueError("最终重定向URL中未找到token")
    
    def get_learning_center_token(self, account, password):
        """完整的获取学习中心 token 流程"""
        sso_cookie = self.login(account, password)
        return self.get_study_token(sso_cookie)

def main():
    import argparse
    parser = argparse.ArgumentParser(description="获取学习中心 token(SSO 登录)")
    parser.add_argument("--username", "-u", required=True, help="学号")
    parser.add_argument("--password", "-p", required=True, help="密码")
    args = parser.parse_args()
    
    sso = SSOLogin()
    try:
        token = sso.get_learning_center_token(args.username, args.password)
        print(token)
    except Exception as e:
        print('ERROR:', e)

if __name__ == "__main__":
    main()
// Node.js

const axios = require('axios');
const CryptoJS = require('crypto-js');
const { CookieJar } = require('tough-cookie');
const { wrapper: axiosCookieJarSupport } = require('axios-cookiejar-support');
const querystring = require('querystring');
const { JSDOM } = require('jsdom');

const SSO_URL = "https://sso.fzu.edu.cn/login";
const AUTH_URL = "https://sso.fzu.edu.cn/oauth2.0/authorize?response_type=code&client_id=wlwxt&redirect_uri=http://aiot.fzu.edu.cn/api/admin/sso/getIbsToken";
const USER_AGENT = 'Mozilla/5.0 (iPad; CPU OS 18_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 appId/cn.edu.fzu.fdxypa appScheme/kysk-fdxy-app hengfeng/fdxyappzs appType/2 ruijie-facecamera';

class SSOLogin {
    constructor() {
        this.jar = new CookieJar();
        this.session = axios.create({
            jar: this.jar,
            withCredentials: true,
            headers: {
                'User-Agent': USER_AGENT
            },
            maxRedirects: 10,
            validateStatus: function (status) {
                return status >= 200 && status < 400;
            },
        });
        axiosCookieJarSupport(this.session);
    }

    extractKv(rawText, key) {
        if (typeof rawText !== 'string') {
            throw new Error("Input must be a string");
        }
        const pattern = new RegExp(`${key}=([^;&]+)`);
        const match = rawText.match(pattern);
        if (!match) {
            return null;
        }
        return match[1];
    }

    encryptPassword(rawPassword, keyBase64) {
        const key = CryptoJS.enc.Base64.parse(keyBase64);
        const encrypted = CryptoJS.AES.encrypt(rawPassword, key, {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7
        });
        return encrypted.toString();
    }

    async login(account, password) {
        if (!account || !password) {
            throw new Error("Username and password cannot be empty");
        }

        const response = await this.session.get(SSO_URL);
        const html = response.data;
        const dom = new JSDOM(html);
        const document = dom.window.document;

        const cryptoEl = document.getElementById('login-croypto');
        const executionEl = document.getElementById('login-page-flowkey');

        if (!cryptoEl || !executionEl) {
            throw new Error("Could not extract crypto/execution keys from the page. The SSO page may have changed.");
        }

        const crypto = cryptoEl.textContent;
        const execution = executionEl.textContent;

        const encryptedPassword = this.encryptPassword(password, crypto);
        const captchaPayload = this.encryptPassword('{}', crypto);

        const loginData = {
            'username': account,
            'type': 'UsernamePassword',
            '_eventId': 'submit',
            'geolocation': '',
            'execution': execution,
            'captcha_code': '',
            'croypto': crypto,
            'password': encryptedPassword,
            'captcha_payload': captchaPayload
        };

        const loginResponse = await this.session.post(SSO_URL, querystring.stringify(loginData), {
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        });

        const cookies = await this.jar.getCookies(SSO_URL);
        const tgcCookie = cookies.find(cookie => cookie.key === 'SOURCEID_TGC');

        if (tgcCookie) {
            return `SOURCEID_TGC=${tgcCookie.value}`;
        }
        
        const errorDom = new JSDOM(loginResponse.data);
        const errorMsg = errorDom.window.document.getElementById('msg')?.textContent.trim();
        if (errorMsg) {
            throw new Error(`Login failed. Server message: "${errorMsg}"`);
        }
        throw new Error('Incorrect username or password, or login failed.');
    }

    async getStudyToken(ssoCookie) {
        if (!ssoCookie) {
            throw new Error("SSO cookie is required. Please log in first.");
        }

        const response = await this.session.get(AUTH_URL, {
            headers: {
                'Cookie': ssoCookie
            }
        });

        const finalUrl = response.request.res.responseUrl || response.config.url;
        const token = this.extractKv(finalUrl, 'token');

        if (token) {
            return token;
        }
        
        throw new Error("Token not found in the final redirected URL");
    }

    async getLearningCenterToken(account, password) {
        const ssoCookie = await this.login(account, password);
        return await this.getStudyToken(ssoCookie);
    }
}

async function main() {
    const args = process.argv.slice(2);
    const usernameIndex = 832403323;
    const passwordIndex = args.indexOf('--password');
    let username = null;
    let password = null;

    if (usernameIndex !== -1 && args[usernameIndex + 1]) {
        username = args[usernameIndex + 1];
    }
    
    if (passwordIndex !== -1 && args[passwordIndex + 1]) {
        password = args[passwordIndex + 1];
    }

    if (!username || !password) {
        console.log("Usage: node sso-login.js --username <your_username> --password '<your_password>'");
        return;
    }

    const sso = new SSOLogin();
    try {
        const token = await sso.getLearningCenterToken(username, password);
        console.log(token);
    } catch (e) {
        console.error('ERROR:', e.message);
    }
}

if (require.main === module) {
    main();
}

直接登录

自行访问 https://aiot.fzu.edu.cn/api/ibs ,登录后的网址会携带 token,可以从中提取。