Ubuntu 22.04 使用self-service-password搭建自主修改密码平台(供Openldap或者Windows AD 认证)

Ubuntu系统安装准备

供Openldap和Windows AD域认证

  • 这里安装的self-service-password服务是前提条件,不管是使用Openldap还是Windows AD域进行认证都要使用这个服务,所以这里是先搭建安装。
  • Tips:2台主机,一台专门搭建Openldap,一台专门搭建self-service-password

正式安装

  • 在下面开始正式安装之前先按照官方文档提供的操作,按照顺序执行一遍,然后再按照下面的操作执行一遍,整个过程没有报错就没有问题。

  • 我在几次测试后,发现如果不是按照官方文档顺序操作,虽然安装成功了,但是访问的时候会显示HTTP 500的内部错误,此时我解决办法就是按照官方文档安装操作重新执行一遍便解决了问题。

  • 官方文档安装流程如下:


Due to a bug in old Debian and Ubuntu smarty3 package, you may face the error syntax error, unexpected token "class". In this case, install a newer version of the package:

wget http://ftp.us.debian.org/debian/pool/main/s/smarty3/smarty3_3.1.47-2_all.deb

dpkg -i smarty3_3.1.47-2_all.deb

image

vi /etc/apt/sources.list.d/ltb-project.list

deb [arch=amd64 signed-by=/usr/share/keyrings/ltb-project.gpg] https://ltb-project.org/debian/stable stable main

wget -O - https://ltb-project.org/documentation/_static/RPM-GPG-KEY-LTB-project | gpg --dearmor | sudo tee /usr/share/keyrings/ltb-project.gpg >/dev/null

期间若安装有问题会出现下面提示,让你执行,就去执行即可。
sudo apt --fix-broken install

sudo apt update

sudo apt install php php-gd php-ldap php-mbstring

sudo apt install php php-cli php-cgi php-common

sudo apt install self-service-password smarty3

供Openldap认证使用的配置参数

  • 这里的前提是已经搭建好了Openldap服务,并配置好了相关参数之后才开始继续下面的操作,不管咋样都要先把self-service-password这个服务搭建起来,至于配置参数可根据实际情况进行安排

  • 搭建Openldap服务并配置可参考:https://cn.linux-console.net/?p=3446

  • 也可以参考我自己转载的,跟上面一样:https://www.cnblogs.com/autopwn/p/18214912

  • 按照上述步骤基本一路命令敲击下来,self-service-password就安装成功了。

  • 首先是配置apache,将其更改为self-service-password相关的目录。


cd /etc/apache2/sites-available
mv  000-default.conf  000-default.conf.bak
cp  self-service-password.conf  000-default.conf
/etc/init.d/apache2 restart 

  • 配置self-service-password参数

cd /etc/self-service-password/

vim config.inc.php

配置如下两段

# LDAP
$ldap_url = "ldap://10.10.200.180:389";
$ldap_starttls = false;
$ldap_binddn = "cn=admin,dc=vantest,dc=com";
$ldap_bindpw = "admin@111";
$ldap_base = "dc=vantest,dc=com";
$ldap_login_attribute = "uid";
$ldap_fullname_attribute = "cn";
$ldap_filter = "(&(objectClass=person)($ldap_login_attribute={login}))";

#---------------------------------
$mail_address_use_ldap = true;
$mail_from = "test@vantest.com";
$mail_from_name = "Self Service Password";
$mail_signature = "";
# Notify users anytime their password is changed
$notify_on_change = false;
# PHPMailer configuration (see https://github.com/PHPMailer/PHPMailer)
$mail_sendmailpath = '/usr/sbin/sendmail';
$mail_protocol = 'smtp';
$mail_smtp_debug = 0;
$mail_debug_format = 'error_log';
$mail_smtp_host = 'smtp.mxhichina.com';
$mail_smtp_auth = true;
$mail_smtp_user = 'gerrit@vantest.com';
$mail_smtp_pass = 'xxxx';
$mail_smtp_port = 587;
$mail_smtp_timeout = 30;
$mail_smtp_keepalive = false;
$mail_smtp_secure = 'tls';
$mail_smtp_autotls = true;
$mail_contenttype = 'text/plain';
$mail_wordwrap = 0;
$mail_charset = 'utf-8';
$mail_priority = 3;
$mail_newline = PHP_EOL;

  • 邮件配置只三个参考具体需要测试。

参考
https://kifarunix.com/setup-ldap-self-service-password-tool-on-centos-8/
http://www.jouvinio.net/wiki/index.php/Configuration_LDAP_Self_Service_Password
https://self-service-password.readthedocs.io/en/stable/config_apache.html
https://cn.linux-console.net/?p=3446


使用Windows AD域进行认证通过self-service-password进行自助修改密码

前提条件

具体安装方式,还是要把重点步骤记录下来。
服务器角色->勾选Active Directory证书服务->添加功能
image
角色服务(+勾选证书颁发机构Web注册)->添加功能
image
image
安装(可勾选如果需要,自动重新启动目标服务器)
image
image
配置证书->点击服务器管理器的通知(有感叹号提醒)->配置目标服务器上的Active Directory证书服务
image
勾选证书颁发机构和证书颁发机构Web注册->下一步
image
设置类型(根据具体需要选择)->下一步
image
image
image
image
image
image
image
导出证书
image
文件(左上角)->添加或删除管理单元->选择证书->添加->计算机账户->下一步->完成
image
image
证书->个人->证书->右键->打开->详细信息->复制到文件…(出现证书导出向导)
image
image
image
image
image
image
image

  • Tips:3台主机,一台主Windows AD域,一台备Windows AD域,一台Ubuntu Server 22.04 LTS用来搭建Openldap和self-service-password

Ubuntu Server安装Openldap

  • 这里安装Openldap只是安装,因为不提供对外认证功能,所以只需要安装成功即可,不需要详细的配置各种参数,要求就是安装成功,然后服务正常起来,开机启动开了即可,然后后面配置好Windows AD域的CA证书即可,下面会介绍。

  • 安装ladp

  • sudo apt install slapd ldap-utils

  • sudo systemctl enable slapd

  • 期间设置密码为admin@123

导出Windows AD域证书

  • 将导出来的CA证书转换为pem格式
  • 导出Windows的CA证书,然后将这个证书上传到self-service-password所在的服务器,再执行下面转换证书格式的操作。

sudo openssl x509 -inform der -in xxx11-001p.cer -out xxx11-001p.pem
sudo cat xxx11-001p.pem >> /etc/ssl/certs/xxx11-001p.pem

如果没有权限就是给目录/etc/ssl/certs 777权限
sudo chmod -R 777 /etc/ssl/certs

配置Windows AD域证书作用在ldap中


修改ldap配置文件
vim /etc/ldap/ldap.conf

注释原来的配置,添加如下配置:
TLS_CACERTDIR   /etc/ssl/certs
TLS_CACERT      /etc/ssl/certs/xxx11-001p.pem

SASL_NOCANON    on

重启apache服务

sudo /etc/init.d/apache2 restart

访问:http://110.110.200.186/index.php

供Windows AD域认证需要配置self-service-password的配置文件

  • 到了这一步操作,关于self-service-password的基础配置操作跟开头的配置方式都一样,然后配置文件按照如下格式写,下面我直接把原始的配置文件贴上来供大家参考,关键信息我写成通用的了,大家根据实际情况自行更改。

  • config.inc.php具体配置文件内容,如下,可修改为自己的信息然后直接使用

  • 配置Windows AD域进行自助改密码一定是要使用ldaps,端口是636,而且是域名的形式,所以没有DNS一定要本地绑定好hosts文件。


<?php
#==============================================================================
# LTB Self Service Password
#
# Copyright (C) 2024 Clement OUDOT
# Copyright (C) 2024 LTB-project.org
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# GPL License: http://www.gnu.org/licenses/gpl.txt
#
#==============================================================================

#==============================================================================
# All the default values are kept here, you should not modify it but use
# config.inc.local.php file instead to override the settings from here.
#==============================================================================

#==============================================================================
# Configuration
#==============================================================================

# Debug mode
# true: log and display any errors or warnings (use this in configuration/testing)
# false: log only errors and do not display them (use this in production)
$debug = false;

# LDAP
$ldap_url = "ldaps://xxx11-001p.yukongyuming.lan:636"; // change
$ldap_starttls = false;
$ldap_binddn = "CN=Administrator,CN=Users,DC=yukongyuming,DC=lan"; // change
$ldap_bindpw = '1212121212'; // change
// for GSSAPI authentication, comment out ldap_bind* and uncomment ldap_krb5ccname lines
//$ldap_krb5ccname = "/path/to/krb5cc";
$ldap_base = "dc=yukongyuming,dc=lan"; // change
$ldap_login_attribute = "sAMAccountName"; // change
# $ldap_login_attribute = "uid";
$ldap_fullname_attribute = "cn";
# $ldap_filter = "(&(objectClass=person)($ldap_login_attribute={login}))";
$ldap_filter = "(&(objectClass=user)(sAMAccountName={login})(!(userAccountControl:1.2.840.113556.1.4.803:=2)))"; // change
$ldap_use_exop_passwd = false;
$ldap_use_ppolicy_control = false;
$ldap_network_timeout = 10;

# Active Directory mode
# true: use unicodePwd as password field
# false: LDAPv3 standard behavior
$ad_mode = true; // change
$ad_options=[];
# Force account unlock when password is changed
$ad_options['force_unlock'] = true; // change
# Force user change password at next login
$ad_options['force_pwd_change'] = false;
# Allow user with expired password to change password
$ad_options['change_expired_password'] = true; // change

# Samba mode
# true: update sambaNTpassword and sambaPwdLastSet attributes too
# false: just update the password
$samba_mode = false;
$samba_options=[];
# Set password min/max age in Samba attributes
#$samba_options['min_age'] = 5;
#$samba_options['max_age'] = 45;
#$samba_options['expire_days'] = 90;

# Shadow options - require shadowAccount objectClass
$shadow_options=[];
# Update shadowLastChange
$shadow_options['update_shadowLastChange'] = false;
$shadow_options['update_shadowExpire'] = false;

# Default to -1, never expire
$shadow_options['shadow_expire_days'] = -1;

# Hash mechanism for password:
# SSHA, SSHA256, SSHA384, SSHA512
# SHA, SHA256, SHA384, SHA512
# SMD5
# MD5
# CRYPT
# ARGON2
# clear (the default)
# auto (will check the hash of current password)
# This option is not used with ad_mode = true
$hash = "clear";
$hash_options=[];

# Prefix to use for salt with CRYPT
$hash_options['crypt_salt_prefix'] = "$6$";
$hash_options['crypt_salt_length'] = "6";

# USE rate-limiting by IP and/or by user
$use_ratelimit = false;
# dir for json db's (system default tmpdir)
#$ratelimit_dbdir = '/tmp';
# block attempts for same login ?
$max_attempts_per_user = 2;
# block attempts for same IP ?
$max_attempts_per_ip = 2;
# how many time to refuse subsequent requests ?
$max_attempts_block_seconds = "60";
# Header to use for client IP (HTTP_X_FORWARDED_FOR ?)
$client_ip_header = 'REMOTE_ADDR';
# JSON file to filter by IP
#$ratelimit_filter_by_ip_jsonfile = "/usr/share/self-service-password/conf/rrl_filter_by_ip.json";

# Local password policy
# This is applied before directory password policy
# Minimal length
$pwd_min_length = 0;
# Maximal length
$pwd_max_length = 0;
# Minimal lower characters
$pwd_min_lower = 0;
# Minimal upper characters
$pwd_min_upper = 0;
# Minimal digit characters
$pwd_min_digit = 0;
# Minimal special characters
$pwd_min_special = 0;
# Definition of special characters
$pwd_special_chars = "^a-zA-Z0-9";
# Forbidden characters
#$pwd_forbidden_chars = "@%";
# Don't reuse the same password as currently
$pwd_no_reuse = true;
# Check that password is different than login
$pwd_diff_login = true;
# Check new passwords differs from old one - minimum characters count
$pwd_diff_last_min_chars = 0;
# Forbidden words which must not appear in the password
$pwd_forbidden_words = array();
# Forbidden ldap fields
# Respective values of the user's entry must not appear in the password
# example: $pwd_forbidden_ldap_fields = array('cn', 'givenName', 'sn', 'mail');
$pwd_forbidden_ldap_fields = array();
# Complexity: number of different class of character required
$pwd_complexity = 0;
# use pwnedpasswords api v2 to securely check if the password has been on a leak
$use_pwnedpasswords = false;
# show password entropy bar (require php zxcvbn module)
$pwd_display_entropy = false;
# enforce password entropy check
$pwd_check_entropy = false;
# minimum entropy level required (when $pwd_check_entropy enabled)
$pwd_min_entropy = 3;
# Show policy constraints message:
# always
# never
# onerror
$pwd_show_policy = "never";
# Position of password policy constraints message:
# above - the form
# below - the form
$pwd_show_policy_pos = "above";

# disallow use of the only special character as defined in `$pwd_special_chars` at the beginning and end
$pwd_no_special_at_ends = false;

# Who changes the password?
# Also applicable for question/answer save
# user: the user itself
# manager: the above binddn
# $who_change_password = "user";
$who_change_password = "manager"; // change

# Show extended error message returned by LDAP directory when password is refused
$show_extended_error = false;

## Standard change
# Use standard change form?
$use_change = true;

## SSH Key Change
# Allow changing of sshPublicKey?
$change_sshkey = false;

# What attribute should be changed by the changesshkey action?
$change_sshkey_attribute = "sshPublicKey";

# What objectClass is required for that attribute?
$change_sshkey_objectClass = "ldapPublicKey";

# Ensure the SSH Key submitted uses a type we trust
$ssh_valid_key_types = array('ssh-rsa', 'ssh-dss', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519');

# Who changes the sshPublicKey attribute?
# Also applicable for question/answer save
# user: the user itself
# manager: the above binddn
$who_change_sshkey = "user";

# Notify users anytime their sshPublicKey is changed
## Requires mail configuration below
$notify_on_sshkey_change = false;

## Questions/answers
# Use questions/answers?
$use_questions = true;
# Allow to register more than one answer?
$multiple_answers = false;
# Store many answers in a single string attribute
# (only used if $multiple_answers = true)
$multiple_answers_one_str = false;

# Answer attribute should be hidden to users!
$answer_objectClass = "extensibleObject";
$answer_attribute = "info";

# Crypt answers inside the directory
$crypt_answers = true;

# Extra questions (built-in questions are in lang/$lang.inc.php)
# Should the built-in questions be included?
$questions_use_default = true;
#$messages['questions']['ice'] = "What is your favorite ice cream flavor?";

# How many questions must be answered.
#  If = 1: legacy behavior
#  If > 1:
#    this many questions will be included in the page forms
#    this many questions must be set at a time
#    user must answer this many correctly to reset a password
#    $multiple_answers must be true
#    at least this many possible questions must be available (there are only 2 questions built-in)
$questions_count = 1;

# Should the user be able to select registered question(s) by entering only the login?
$question_populate_enable = false;

## Token
# Use tokens?
# true (default)
# false
$use_tokens = true;
# Crypt tokens?
# true (default)
# false
$crypt_tokens = true;
# Token lifetime in seconds
$token_lifetime = "3600";

# Reset URL (mandatory)
$reset_url = "http://ssp.example.com/";
# If inside a virtual host
#$reset_url = ($_SERVER['HTTPS'] ? "https" : "http") . "://" . $_SERVER['SERVER_NAME'] . $_SERVER['SCRIPT_NAME'];
# If behind a reverse proxy with a virtual host
#$reset_url = $_SERVER['HTTP_X_FORWARDED_PROTO'] . "://" . $_SERVER['HTTP_X_FORWARDED_HOST'] . $_SERVER['SCRIPT_NAME'];

## Mail
# LDAP mail attribute
$mail_attributes = array( "mail", "gosaMailAlternateAddress", "proxyAddresses" );
# Get mail address directly from LDAP (only first mail entry)
# and hide mail input field
# default = false
$mail_address_use_ldap = false;
# Who the email should come from
$mail_from = "admin@example.com";
$mail_from_name = "Self Service Password";
$mail_signature = "";
# Notify users anytime their password is changed
$notify_on_change = false;
# PHPMailer configuration (see https://github.com/PHPMailer/PHPMailer)
$mail_sendmailpath = '/usr/sbin/sendmail';
$mail_protocol = 'smtp';
$mail_smtp_debug = 0;
$mail_debug_format = 'error_log';
$mail_smtp_host = 'localhost';
$mail_smtp_auth = false;
$mail_smtp_user = '';
$mail_smtp_pass = '';
$mail_smtp_port = 25;
$mail_smtp_timeout = 30;
$mail_smtp_keepalive = false;
$mail_smtp_secure = 'tls';
$mail_smtp_autotls = true;
$mail_smtp_options = array();
$mail_contenttype = 'text/plain';
$mail_wordwrap = 0;
$mail_charset = 'utf-8';
$mail_priority = 3;

## SMS
# Use sms
$use_sms = true;
# Get telephone number directly from LDAP (only first number entry)
# and hide telephone number input field
# default = false
$sms_use_ldap = false;
# SMS method (mail, api)
$sms_method = "mail";
# path to SMS library to use
# currently, 3 libraries are bundled:
# - lib/smsapi-signal-cli.inc.php
# - lib/smsapi-twilio.inc.php
# - lib/smsovh/smsapi-ovh.inc.php
# The last one needs php-ovh-sms dependency, do `composer update` in lib/smsovh
# you can also write your own library
$sms_api_lib = "";
# GSM number attribute
$sms_attributes = array( "mobile", "pager", "ipPhone", "homephone" );
# Partially hide number
$sms_partially_hide_number = true;
# Send SMS mail to address. {sms_attribute} will be replaced by real sms number
$smsmailto = "{sms_attribute}@service.provider.com";
# Subject when sending email to SMTP to SMS provider
$smsmail_subject = "Provider code";
# Message
$sms_message = "{smsresetmessage} {smstoken}";
# Remove non digit characters from GSM number
$sms_sanitize_number = false;
# Truncate GSM number
$sms_truncate_number = false;
$sms_truncate_number_length = 10;
# SMS token length
$sms_token_length = 6;
# Max attempts allowed for SMS token
$sms_max_attempts_token = 3;

# Encryption, decryption keyphrase, required if $use_tokens = true and $crypt_tokens = true, or $use_sms, or $crypt_answer
# Please change it to anything long, random and complicated, you do not have to remember it
# Changing it will also invalidate all previous tokens and SMS codes
$keyphrase = "secret@2024";  // change

# Use attributes update page
$use_attributes = false;
#$attribute_mail = "mail";
#$attribute_phone = "mobile";
#$who_change_attributes = "manager";

# Display help messages
$show_help = true;

# Default language
$lang = "en";

# List of authorized languages. If empty, all language are allowed.
# If not empty and the user's browser language setting is not in that list, language from $lang will be used.
$allowed_lang = array();

# Display menu on top
$show_menu = true;

# Logo
$logo = "images/ltb-logo.png";

# Background image
$background_image = "images/unsplash-space.jpeg";

# Path is relative to htdocs/html and the custom CSS file should be created in css/ directory. For example: "css/sample.css"
$custom_css = "";
$display_footer = true;

# Where to log password resets - Make sure apache has write permission
# By default, they are logged in Apache log
#$reset_request_log = "/var/log/self-service-password";

# Invalid characters in login
# Set at least "*()&|" to prevent LDAP injection
# If empty, only alphanumeric characters are accepted
$login_forbidden_chars = "*()&|";

## Captcha
$use_captcha = false;

## Default action
# change
# sendtoken
# sendsms
# changecustompwdfield
$default_action = "change";

## default page of custom password field to display
#$default_custompwdindex = 0;

## Rest API
$use_restapi = false;

## Extra messages
# They can also be defined in lang/ files
#$messages['passwordchangedextramessage'] = NULL;
#$messages['changehelpextramessage'] = NULL;

# Audit
#$audit_log_file = "/var/log/self-service-password/audit.log";

## Pre Hook
# Launch a prehook script before changing password.
# Script should return with 0, to allow password change.
# Any other exit code would abort password modification
#$prehook = "/usr/share/self-service-password/prehook.sh";
# Display prehook error
#$display_prehook_error = true;
# Encode passwords sent to prehook script as base64. This will prevent alteration of the passwords if set to true.
# To read the actual password in the prehook script, use a base64_decode function/tool
#$prehook_password_encodebase64 = false;
# Ignore prehook error. This will allow to change password even if prehook script fails.
#$ignore_prehook_error = true;

## Post Hook
# Launch a posthook script after successful password change
#$posthook = "/usr/share/self-service-password/posthook.sh";
# Display posthook error
#$display_posthook_error = true;
# Encode passwords sent to posthook script as base64. This will prevent alteration of the passwords if set to true.
# To read the actual password in the posthook script, use a base64_decode function/tool
#$posthook_password_encodebase64 = false;

# Force setlocale if your default PHP configuration is not correct
#setlocale(LC_CTYPE, "en_US.UTF-8");

# Hide some messages to not disclose sensitive information
$obscure_usernotfound_sendtoken = true;
$obscure_notfound_sendsms = true;

# HTTP Header name that may hold a login to preset in forms
#$header_name_preset_login="Auth-User";

# The name of an HTTP Header that may hold a reference to an extra config file to include.
#$header_name_extra_config="SSP-Extra-Config";

# Cache directory
$smarty_compile_dir = "/var/cache/self-service-password/templates_c";
$smarty_cache_dir = "/var/cache/self-service-password/cache";

# Smarty debug mode - will popup debug information on web interface
$smarty_debug = false;

## Custom Password Fields
# Change Custom Password Fields
$change_custompwdfield = array();

# Allow to override current settings with local configuration
if (file_exists (__DIR__ . '/config.inc.local.php')) {
    require_once __DIR__ . '/config.inc.local.php';
}

# Smarty
if (!defined("SMARTY")) {
    define("SMARTY", "/usr/share/php/smarty3/Smarty.class.php");
}

# Set preset login from HTTP header $header_name_preset_login
$presetLogin = "";
if (isset($header_name_preset_login)) {
    $presetLoginKey = "HTTP_".strtoupper(str_replace('-','_',$header_name_preset_login));
    if (array_key_exists($presetLoginKey, $_SERVER)) {
        $presetLogin = preg_replace("/[^a-zA-Z0-9-_@\.]+/", "", filter_var($_SERVER[$presetLoginKey], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH));
    }
}

# Allow to override current settings with an extra configuration file, whose reference is passed in HTTP_HEADER $header_name_extra_config
if (isset($header_name_extra_config)) {
    $extraConfigKey = "HTTP_".strtoupper(str_replace('-','_',$header_name_extra_config));
    if (array_key_exists($extraConfigKey, $_SERVER)) {
        $extraConfig = preg_replace("/[^a-zA-Z0-9-_]+/", "", filter_var($_SERVER[$extraConfigKey], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_HIGH));
        if (strlen($extraConfig) > 0 && file_exists (__DIR__ . "/config.inc.".$extraConfig.".php")) {
            require_once  __DIR__ . "/config.inc.".$extraConfig.".php";
        }
    }
}


  • 配置文件000-default.conf,下面的apache配置文件,直接拿去用即可。

<VirtualHost *:80>
    #ServerName ssp.example.com

    DocumentRoot /usr/share/self-service-password/htdocs
    DirectoryIndex index.php

    AddDefaultCharset UTF-8

    <Directory /usr/share/self-service-password/htdocs>
        AllowOverride None
        <IfVersion >= 2.3>
            Require all granted
        </IfVersion>
        <IfVersion < 2.3>
            Order Deny,Allow
            Allow from all
        </IfVersion>
    </Directory>

    Alias /rest /usr/share/self-service-password/rest

    <Directory /usr/share/self-service-password/rest>
        AllowOverride None
        <IfVersion >= 2.3>
            Require all denied
        </IfVersion>
        <IfVersion < 2.3>
            Order Deny,Allow
            Deny from all
        </IfVersion>
    </Directory>

    LogLevel warn
    ErrorLog /var/log/apache2/ssp_error.log
    CustomLog /var/log/apache2/ssp_access.log combined
</VirtualHost>


  • 最终成功验证了就是如下显示
    image
posted @ 2024-05-23 14:50  皇帽讲绿帽带法技巧  阅读(651)  评论(0编辑  收藏  举报