Skip to content

Commit

Permalink
支持学校SSO统一认证登录
Browse files Browse the repository at this point in the history
  • Loading branch information
Airmole committed Sep 29, 2024
1 parent e5e0063 commit bf6e46c
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 20 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# tjustb-edusys

北京科技大学天津学院教务系统客户端(http://61.181.145.1:89/
北京科技大学天津学院教务系统客户端(http://jw.bkty.top:89)

# Requirement

Expand All @@ -19,7 +19,7 @@ use Airmole\TjustbEdusys\Edusys;

$usercode = '123456789'; // 系统账号
$password = '*********'; // 系统密码
$edusys = new Edusys();
$edusys = new Edusys('sso'); // old,new,sso三种登录模式
// 自动登录
$edusys->autoLogin($usercode, $password); // 自动识别验证码登录,需要在项目根目录下.env文件配置EDUSYS_OCR_URL
// 手动登录
Expand Down
5 changes: 2 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
"require": {
"php": ">=7.4",
"ext-curl": "*",
"ext-json": "*"
},
"require-dev": {
"ext-json": "*",
"ext-openssl": "*"
},
"license": "GPL-3.0-or-later",
"authors": [
Expand Down
6 changes: 3 additions & 3 deletions src/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ public function __construct()
* @param string $url
* @return void
*/
public function setEdusysUrl(string $url = 'http://111.33.33.139:89')
public function setEdusysUrl(string $url = 'http://jw.bkty.top:89')
{
if (empty($url)) $url = 'http://111.33.33.139:89';
if (empty($url)) $url = 'http://jw.bkty.top:89';
$this->edusysUrl = $url;
}

Expand Down Expand Up @@ -116,7 +116,7 @@ public function setConfigPath(string $path = '')
* @param string $url 教务系统URL
* @return void
*/
public function setEdusysUrlForce(string $url = 'http://111.33.33.139:89')
public function setEdusysUrlForce(string $url = 'http://jw.bkty.top:89')
{
$this->edusysUrl = $url;
}
Expand Down
11 changes: 7 additions & 4 deletions src/Edusys.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Edusys
public string $cookie;

/**
* @var string 登录模式: new,old
* @var string 登录模式: new,old,sso
*/
public string $mode;

Expand All @@ -36,7 +36,7 @@ public function __construct(string $mode = 'new')
*/
public function getLoginPara(): array
{
$login = new Login($this->mode);
$login = $this->mode === 'sso' ? new SsoLogin() : new Login($this->mode);
return $login->getLoginPara();
}

Expand All @@ -46,17 +46,20 @@ public function getLoginPara(): array
* @param string $password
* @param string $captcha
* @param string $cookie
* @param string $salt sso模式必填
* @param string $execution sso模式必填
* @return array
* @throws Exception
*/
public function selfLogin(string $usercode, string $password, string $captcha, string $cookie): array
public function selfLogin(string $usercode, string $password, string $captcha, string $cookie, string $salt = '', string $execution = ''): array
{
$login = new Login($this->mode);
$result = $login->login($usercode, $password, $captcha, $cookie);
$result = $login->login($usercode, $password, $captcha, $cookie, $salt, $execution);
if ($result['code'] === Base::CODE_SUCCESS) {
$this->usercode = $usercode;
$this->cookie = $result['data'];
}

return $result;
}

Expand Down
33 changes: 25 additions & 8 deletions src/Login.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Login extends Base
public string $captchaOcr;

/**
* @var string 登录模式: new,old
* @var string 登录模式: new,old,sso
*/
public string $mode;

Expand Down Expand Up @@ -57,11 +57,17 @@ public function setCaptchaOcr(string $url = '', string $path = '')
*/
public function autoLogin(string $usercode, string $password, int $retry = 1): array
{
if (empty($this->captchaOcr)) throw new Exception('未配置验证码识别服务,请使用login方法手动输入验证码登录');
if ($this->mode !== 'sso' && empty($this->captchaOcr)) throw new Exception('未配置验证码识别服务,请使用login方法手动输入验证码登录');
for ($i = 0; $i < $retry; $i++) {
$loginPara = $this->getLoginPara();
$captcha = $this->captchaOcr($loginPara['captcha']);
$response = $this->login($usercode, $password, $captcha, $loginPara['cookie']);
if ($this->mode === 'sso') {
$ssoAuth = new SsoLogin();
$loginPara = $ssoAuth->getLoginPara();
$response = $ssoAuth->login($usercode, $password, $loginPara['cookie'], $loginPara['salt'], $loginPara['execution']);
} else {
$loginPara = $this->getLoginPara();
$captcha = $this->captchaOcr($loginPara['captcha']);
$response = $this->login($usercode, $password, $captcha, $loginPara['cookie']);
}
if ($response['code'] === 200) return $response;
if ($response['code'] == 403 && strpos($response['data'], '验证码') !== false) {
continue;
Expand All @@ -78,6 +84,10 @@ public function autoLogin(string $usercode, string $password, int $retry = 1): a
*/
public function getLoginPara(): array
{
if ($this->mode === 'sso') {
$ssoAuth = new SsoLogin();
return $ssoAuth->getLoginPara();
}
$cookie = $this->getCookie();
$captcha = $this->getCaptcha($cookie);
return ['cookie' => $cookie, 'captcha' => $captcha];
Expand Down Expand Up @@ -142,10 +152,12 @@ public function captchaOcr(string $captcha): string
* @param string $password 密码
* @param string $captcha 验证码
* @param string $cookie cookie
* @param string $salt SSO登录密码salt, SSO模式必填参数
* @param string $execution SSO模式必填参数
* @return array
* @throws Exception
*/
public function login(string $usercode, string $password, string $captcha, string $cookie): array
public function login(string $usercode, string $password, string $captcha, string $cookie, string $salt = '', string $execution = ''): array
{
if ($this->mode == 'new') {
$logonSess = $this->httpPost($this->edusysUrl . '/Logon.do?method=logon&flag=sess', '', $cookie);
Expand All @@ -162,6 +174,12 @@ public function login(string $usercode, string $password, string $captcha, strin
$redirectResponse = $this->httpGetFollowLocation($this->edusysUrl, $this->loginedLocationUrl($response['data']), $cookie);
$url = $this->isStudent($usercode) ? '/jsxsd/framework/xsMain.jsp' : '/jsxsd/framework/jsMain.jsp';
$mainBoard = $this->httpGet($url, $redirectResponse['cookie'], $this->edusysUrl. '/');
$cookie = $redirectResponse['cookie'];
} elseif ($this->mode == 'sso') {
$ssoAuth = new SsoLogin();
$mainBoard = $ssoAuth->login($usercode, $password, $cookie, $salt, $execution);
if ($mainBoard['code'] !== 200) return $mainBoard;
$cookie = $mainBoard['data'];
} else {
$postString = "userAccount={$usercode}&userPassword=&RANDOMCODE={$captcha}&encoded=";
$postString = $postString . base64_encode($usercode) . "%25%25%25" . base64_encode($password);
Expand All @@ -175,8 +193,7 @@ public function login(string $usercode, string $password, string $captcha, strin
}

if ($mainBoard['code'] != 200) throw new Exception('访问主界面异常失败');
$cookie = $this->mode == 'old' ? $cookie : $redirectResponse['cookie'];
$this->cookie = $this->mode == 'old' ? $cookie : $redirectResponse['cookie'];
$this->cookie = $cookie;
$this->usercode = $usercode;
return ['code' => 200, 'data' => $cookie];
}
Expand Down
205 changes: 205 additions & 0 deletions src/SsoLogin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php

namespace Airmole\TjustbEdusys;

use Airmole\TjustbEdusys\Exception\Exception;

class SsoLogin extends Base
{
/**
* @var string SSO登录地址
*/
public string $ssoDomain = 'http://authserver.bkty.top';

/**
* 获取登录参数
* @return array
* @throws Exception
*/
public function getLoginPara(): array
{
$url = $this->ssoDomain . '/authserver/login?service=http%3A%2F%2Fjw.bkty.top%3A89%2Fjsxsd%2Fsso.jsp';
$result = $this->httpGet($url, '', '', 5, true);
$httpCode = $result['code'];
$response = $result['data'];
if ($httpCode == 0 || $httpCode >= 400) throw new Exception('获取cookie失败' . json_encode($response));
preg_match('/Set-Cookie: route=(.*?); Path=\//i', $response, $routeId);
preg_match('/Set-Cookie: JSESSIONID=(.*?); path=\//i', $response, $cookie);
$jsessionidStr = '';
$routeIdStr = '';
$separator = '';
if (!empty($routeId[1])) $routeIdStr = "route={$routeId[1]}";
if (!empty($cookie[1]) && !empty($routeId[1])) $separator = '; ';
if (!empty($cookie[1])) $jsessionidStr = "JSESSIONID={$cookie[1]}";
$cookieStr = $routeIdStr . $separator . $jsessionidStr;
if (!empty($cookieStr)) $cookieStr = $cookieStr . '; org.springframework.web.servlet.i18n.CookieLocaleResolver.LOCALE=zh_CN';
preg_match('/id="pwdEncryptSalt" value="(.*?)"/i', $response, $passwordSalt);
preg_match('/name="execution" value="(.*?)"\/></i', $response, $execution);

return [
'cookie' => $cookieStr,
'_eventId' => 'submit',
'cllt' => 'userNameLogin',
'dllt' => 'generalLogin',
'lt' => '',
'salt' => empty($passwordSalt[1]) ? '' : $passwordSalt[1],
'execution' => empty($execution[1]) ? '' : $execution[1]
];
}

/**
* 登录
* @param string $usercode
* @param string $password
* @param string $cookie
* @param string $salt
* @param string $execution
* @return array
* @throws Exception
*/
public function login(string $usercode, string $password, string $cookie, string $salt, string $execution): array
{
if (empty($salt)) throw new Exception('密码salt值不可为空');
if (empty($execution)) throw new Exception('execution值不可为空');
// TODO 后续优化支持自动识别滑动验证码
if ($this->checkNeedCaptcha($usercode, $cookie)) throw new Exception('需要识别验证码');

$url = $this->ssoDomain . '/authserver/login?service=http%3A%2F%2Fjw.bkty.top%3A89%2Fjsxsd%2Fsso.jsp';
$postData = [
'username' => $usercode,
'password' => $this->encryptPassword($password, $salt),
'captcha' => '',
'_eventId' => 'submit',
'cllt' => 'userNameLogin',
'dllt' => 'generalLogin',
'lt' => '',
'execution' => $execution
];
$postString = http_build_query($postData);
$result = $this->httpPostLogin($url, $postString, $cookie);
if ($result['code'] !== 200) throw new Exception('登录失败' . json_encode($result));
$validateResult = $this->validateLoginResult($result);
if ($validateResult !== true) return $validateResult;
preg_match_all('/Set-Cookie: JSESSIONID=(.*?); Path=\/jsxsd/', $result['data'], $newCookie);
if (!isset($newCookie[1]) || !isset($newCookie[1][0])) throw new Exception('登录获取cookie失败' . json_encode($newCookie));
$newCookieValue = $newCookie[1][0];
$newCookie = "JSESSIONID={$newCookieValue}";
$url = "/jsxsd/sso.jsp;jsessionid={$newCookieValue}";
$this->httpGet($url, $newCookie, $this->ssoDomain . '/');
return ['code' => 200, 'data' => $newCookie];
}

/**
* 根据返回结果验证是否登录成功
* @param array $response
* @return array|true
*/
public function validateLoginResult(array $response)
{
if (strpos($response['data'], '您提供的用户名或者密码有误')) return ['code' => 403, 'data' => '用户名或密码错误'];
if (strpos($response['data'], '登录凭证不可用')) return ['code' => 403, 'data' => '登录凭证不可用'];
return true;
}

/**
* 发送登录请求
* @param string $url
* @param string $postString
* @param string $cookie
* @return array
*/
public function httpPostLogin(string $url, string $postString, string $cookie): array
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Cache-Control: no-cache',
'Connection: keep-alive',
'Content-Type: application/x-www-form-urlencoded',
'Origin: http://authserver.bkty.top',
'Pragma: no-cache',
"Referer: {$url}",
'Upgrade-Insecure-Requests: 1',
'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0',
]);
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postString);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ['code' => (int)$httpCode, 'data' => $response];
}

/**
* 检查是否需要验证码
* @param string $usercode
* @param string $cookie
* @return bool
* @throws Exception
*/
public function checkNeedCaptcha(string $usercode, string $cookie): bool
{
$url = $this->ssoDomain . "/authserver/checkNeedCaptcha.htl?username={$usercode}&_=" . time() . '000';
$res = $this->httpGet($url, $cookie);
$httpCode = $res['code'];
$response = $res['data'];
if ($httpCode >= 400) throw new Exception('请求是否需要验证码失败' . json_encode($response));

$result = json_decode($response, true); // {"isNeed":true}
return (bool)$result['isNeed'];
}

/**
* 密码加密
* @param string $data
* @param string $aesKey
* @return string
*/
function encryptPassword(string $data, string $aesKey): string
{
if (!$aesKey) return $data;
$aesKey = trim($aesKey);
$randomString = $this->randomString(64);
$iv = $this->randomString(16);
$encrypted = openssl_encrypt($randomString . $data, 'AES-128-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
return base64_encode($encrypted);
}

/**
* 密码解密
* @param string $data
* @param string $aesKey
* @return string
*/
function decryptPassword(string $data, string $aesKey): string
{
$iv = $this->randomString(16);
$decrypted = openssl_decrypt(base64_decode($data), 'AES-128-CBC', $aesKey, OPENSSL_RAW_DATA, $iv);
return substr($decrypted, 64);
}

/**
* 生成随机字符串
* @param int $length
* @return string
*/
function randomString(int $length): string
{
$characters = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}

}

0 comments on commit bf6e46c

Please sign in to comment.