feat: 接入阿里云短信告警通知(dysmsapi)
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m10s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m10s
异常检测触发时,在飞书告警基础上同时发送短信通知。 签名:广州气元科技,模板:SMS_503445109。 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6a0311d599
commit
e04712cc79
@ -79,6 +79,8 @@ jobs:
|
|||||||
--from-literal=DB_HOST='${{ secrets.DB_HOST }}' \
|
--from-literal=DB_HOST='${{ secrets.DB_HOST }}' \
|
||||||
--from-literal=DB_USER='${{ secrets.DB_USER }}' \
|
--from-literal=DB_USER='${{ secrets.DB_USER }}' \
|
||||||
--from-literal=DB_PASSWORD='${{ secrets.DB_PASSWORD }}' \
|
--from-literal=DB_PASSWORD='${{ secrets.DB_PASSWORD }}' \
|
||||||
|
--from-literal=ALIYUN_SMS_ACCESS_KEY='${{ secrets.ALIYUN_SMS_ACCESS_KEY }}' \
|
||||||
|
--from-literal=ALIYUN_SMS_ACCESS_SECRET='${{ secrets.ALIYUN_SMS_ACCESS_SECRET }}' \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
kubectl apply -f /tmp/backend-deployment.yaml
|
kubectl apply -f /tmp/backend-deployment.yaml
|
||||||
|
|||||||
@ -193,3 +193,11 @@ ARK_API_KEY = os.environ.get('ARK_API_KEY', '')
|
|||||||
ARK_BASE_URL = os.environ.get('ARK_BASE_URL', 'https://ark.cn-beijing.volces.com/api/v3')
|
ARK_BASE_URL = os.environ.get('ARK_BASE_URL', 'https://ark.cn-beijing.volces.com/api/v3')
|
||||||
# Set to True when Seedance model is activated on ARK platform
|
# Set to True when Seedance model is activated on ARK platform
|
||||||
SEEDANCE_ENABLED = os.environ.get('SEEDANCE_ENABLED', 'false').lower() == 'true'
|
SEEDANCE_ENABLED = os.environ.get('SEEDANCE_ENABLED', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Aliyun SMS (短信告警)
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
ALIYUN_SMS_ACCESS_KEY = os.environ.get('ALIYUN_SMS_ACCESS_KEY', '')
|
||||||
|
ALIYUN_SMS_ACCESS_SECRET = os.environ.get('ALIYUN_SMS_ACCESS_SECRET', '')
|
||||||
|
ALIYUN_SMS_SIGN_NAME = os.environ.get('ALIYUN_SMS_SIGN_NAME', '广州气元科技')
|
||||||
|
ALIYUN_SMS_TEMPLATE_CODE = os.environ.get('ALIYUN_SMS_TEMPLATE_CODE', 'SMS_503445109')
|
||||||
|
|||||||
@ -222,6 +222,95 @@ def send_feishu_alert(anomaly):
|
|||||||
logger.error('Feishu alert error for %s: %s', mobile, e)
|
logger.error('Feishu alert error for %s: %s', mobile, e)
|
||||||
|
|
||||||
|
|
||||||
|
def send_sms_alert(anomaly):
|
||||||
|
"""发送短信告警到配置的接收人。"""
|
||||||
|
from apps.generation.models import QuotaConfig
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = QuotaConfig.objects.get(pk=1)
|
||||||
|
except QuotaConfig.DoesNotExist:
|
||||||
|
logger.warning('QuotaConfig not found, skip SMS alert')
|
||||||
|
return
|
||||||
|
|
||||||
|
mobiles = [m.strip() for m in config.sms_alert_mobiles.split(',') if m.strip()]
|
||||||
|
if not mobiles:
|
||||||
|
return
|
||||||
|
|
||||||
|
access_key = django_settings.ALIYUN_SMS_ACCESS_KEY
|
||||||
|
access_secret = django_settings.ALIYUN_SMS_ACCESS_SECRET
|
||||||
|
sign_name = django_settings.ALIYUN_SMS_SIGN_NAME
|
||||||
|
template_code = django_settings.ALIYUN_SMS_TEMPLATE_CODE
|
||||||
|
|
||||||
|
if not all([access_key, access_secret, template_code]):
|
||||||
|
logger.warning('Aliyun SMS credentials not configured, skip SMS alert')
|
||||||
|
return
|
||||||
|
|
||||||
|
rule_name = _RULE_NAMES.get(anomaly.rule, anomaly.rule)
|
||||||
|
auto_action = '已自动封禁' if anomaly.auto_disabled else '仅告警'
|
||||||
|
|
||||||
|
template_param = json.dumps({
|
||||||
|
'team_name': anomaly.team.name[:20],
|
||||||
|
'rule_name': rule_name[:20],
|
||||||
|
'username': anomaly.user.username[:20],
|
||||||
|
'city': anomaly.login_record.geo_city or '未知',
|
||||||
|
'auto_action': auto_action,
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 使用阿里云 SMS HTTP API
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import base64
|
||||||
|
import urllib.parse
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
def _percent_encode(s):
|
||||||
|
return urllib.parse.quote(s, safe='', encoding='utf-8')
|
||||||
|
|
||||||
|
for mobile in mobiles:
|
||||||
|
try:
|
||||||
|
params = {
|
||||||
|
'AccessKeyId': access_key,
|
||||||
|
'Action': 'SendSms',
|
||||||
|
'Format': 'JSON',
|
||||||
|
'PhoneNumbers': mobile,
|
||||||
|
'RegionId': 'cn-hangzhou',
|
||||||
|
'SignName': sign_name,
|
||||||
|
'SignatureMethod': 'HMAC-SHA1',
|
||||||
|
'SignatureNonce': str(uuid.uuid4()),
|
||||||
|
'SignatureVersion': '1.0',
|
||||||
|
'TemplateCode': template_code,
|
||||||
|
'TemplateParam': template_param,
|
||||||
|
'Timestamp': datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ'),
|
||||||
|
'Version': '2017-05-25',
|
||||||
|
}
|
||||||
|
|
||||||
|
sorted_params = sorted(params.items())
|
||||||
|
query_string = '&'.join(f'{_percent_encode(k)}={_percent_encode(v)}' for k, v in sorted_params)
|
||||||
|
string_to_sign = f'GET&{_percent_encode("/")}&{_percent_encode(query_string)}'
|
||||||
|
|
||||||
|
sign_key = (access_secret + '&').encode('utf-8')
|
||||||
|
signature = base64.b64encode(
|
||||||
|
hmac.new(sign_key, string_to_sign.encode('utf-8'), hashlib.sha1).digest()
|
||||||
|
).decode('utf-8')
|
||||||
|
|
||||||
|
params['Signature'] = signature
|
||||||
|
|
||||||
|
resp = requests.get(
|
||||||
|
'https://dysmsapi.aliyuncs.com/',
|
||||||
|
params=params,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
data = resp.json()
|
||||||
|
if data.get('Code') == 'OK':
|
||||||
|
logger.info('SMS alert sent to %s for rule %s', mobile, anomaly.rule)
|
||||||
|
else:
|
||||||
|
logger.error('SMS send failed to %s: %s', mobile, data)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error('SMS alert error for %s: %s', mobile, e)
|
||||||
|
|
||||||
|
|
||||||
def send_feishu_test(mobile):
|
def send_feishu_test(mobile):
|
||||||
"""发送测试消息到指定手机号。Returns (success, message)。"""
|
"""发送测试消息到指定手机号。Returns (success, message)。"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -305,7 +305,8 @@ def _send_alert_safe(anomaly_pk):
|
|||||||
from apps.accounts.models import LoginAnomaly
|
from apps.accounts.models import LoginAnomaly
|
||||||
anomaly = LoginAnomaly.objects.select_related('team', 'user', 'login_record').get(pk=anomaly_pk)
|
anomaly = LoginAnomaly.objects.select_related('team', 'user', 'login_record').get(pk=anomaly_pk)
|
||||||
|
|
||||||
from utils.alert_service import send_feishu_alert
|
from utils.alert_service import send_feishu_alert, send_sms_alert
|
||||||
send_feishu_alert(anomaly)
|
send_feishu_alert(anomaly)
|
||||||
|
send_sms_alert(anomaly)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error('Failed to send alert for anomaly %s: %s', anomaly_pk, e)
|
logger.error('Failed to send alert for anomaly %s: %s', anomaly_pk, e)
|
||||||
|
|||||||
@ -91,6 +91,21 @@ spec:
|
|||||||
key: ARK_API_KEY
|
key: ARK_API_KEY
|
||||||
- name: SEEDANCE_ENABLED
|
- name: SEEDANCE_ENABLED
|
||||||
value: "true"
|
value: "true"
|
||||||
|
# Aliyun SMS
|
||||||
|
- name: ALIYUN_SMS_ACCESS_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: video-backend-secrets
|
||||||
|
key: ALIYUN_SMS_ACCESS_KEY
|
||||||
|
- name: ALIYUN_SMS_ACCESS_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: video-backend-secrets
|
||||||
|
key: ALIYUN_SMS_ACCESS_SECRET
|
||||||
|
- name: ALIYUN_SMS_SIGN_NAME
|
||||||
|
value: "广州气元科技"
|
||||||
|
- name: ALIYUN_SMS_TEMPLATE_CODE
|
||||||
|
value: "SMS_503445109"
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /healthz/
|
path: /healthz/
|
||||||
|
|||||||
@ -316,14 +316,12 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>短信告警手机号(Coming soon)</label>
|
<label>短信告警手机号</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={settings.sms_alert_mobiles}
|
value={settings.sms_alert_mobiles}
|
||||||
onChange={(e) => setSettings({ ...settings, sms_alert_mobiles: e.target.value })}
|
onChange={(e) => setSettings({ ...settings, sms_alert_mobiles: e.target.value })}
|
||||||
placeholder="暂未开放"
|
placeholder="多个手机号用逗号分隔,如 13800138000,13900139000"
|
||||||
disabled
|
|
||||||
style={{ opacity: 0.5 }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user