Skip to content

图形验证码认证因素#

功能介绍#

对用户认证凭证表单进行扩充,插入图形验证码并实现相关验证功能

注意:图形验证码认证因素不具有认证/注册/修改密码等功能,仅对其他认证因素进行凭证元素扩充

普通用户:

  • 在 “登录” 页面实现向指定表单插入图形验证码

配置指南#

经由左侧菜单栏依次进入【租户管理】->【插件管理】,在插件租赁页面中找到图形验证码认证因素插件卡片,点击租赁 vEHa3q.png

租赁完成后,进入已租赁列表,找到图形验证码认证因素插件卡片,点击租户配置,配置相关数据 vEbAx0.md.png

经由左侧菜单栏依次进入【认证管理】-> 【认证因素】,点击创建按钮,类型选择"authcode", 无须配置相关参数,至此配置完成 vEbpVg.md.png

实现思路#

  • 普通用户:图形验证码:
sequenceDiagram
    participant D as 用户
    participant C as 平台核心
    participant A as 图形验证码认证因素插件

    C->>A: 加载插件
    A->>C: 注册并监听事件AUTHRULE_FIX_LOGIN_PAGE,AUTHRULE_CHECK_AUTH_DATA
    D->>C: 访问注册/登录/重置密码页面
    C->>A: 发出AUTHRULE_FIX_LOGIN_PAGE事件
    A->>C: 响应事件,向注册/登录/重置密码页面注入元素
    C->>D: 渲染注册/登录/重置密码页面
    D->>C: 输入认证凭证,发起认证请求
    C->>A: 触发认证凭证检查事件AUTHRULE_CHECK_AUTH_DATA
    A->>C: 响应事件,完成认证凭证检查,返回结果
    C->>D: 检查结果,如完成注册/登录相关操作则生成token并跳转至桌面,如完成重置密码操作或者未完成注册/登录操作则提示错误

抽象方法实现#

代码#

extension_root.com_longgui_auth_factor_authcode.AuthCodeAuthFactorExtension (AuthFactorExtension) #

图形验证码认证因素插件

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
class AuthCodeAuthFactorExtension(AuthFactorExtension):
    """图形验证码认证因素插件
    """
    def load(self):
        """加载插件
        """
        self.create_extension_config_schema()
        self.create_extension_settings_schema()
        self.register_extension_api()

        super().load()

        # 初始化部分配置数据
        tenant = Tenant.platform_tenant()
        if self.get_settings(tenant):
            settings = {
                "width": 180, 
                "height": 60, 
                "auth_code_length": 4
            }

            self.update_or_create_settings(tenant, settings, True, False)

        if not self.get_tenant_configs(tenant):
            config = {
                "login_enabled": False, 
                "register_enabled": False, 
                "reset_password_enabled": False
            }

            self.create_tenant_config(tenant, config, "图形验证码", "authcode")

    def authenticate(self, event, **kwargs):
        pass

    @transaction.atomic()
    def register(self, event, **kwargs):
        pass

    def reset_password(self, event, **kwargs):
        pass

    def create_login_page(self, event, config, config_data):
        pass

    def fix_login_page(self, event, **kwargs):
        items = [
            {
                "type": "text",
                "name": "authcode",
                "append":{
                    "type": "image",
                    "http": {
                        "url": self.generate_code_path,
                        "method": "get",
                    },
                },
                "placeholder":_("图形验证码")
            },
            {
                "type": "hidden",
                "name": "authcode_key",
            },
        ]
        for login_pages,ext in event.data["login_pages"]:
            if login_pages and isinstance(login_pages,dict):
                for config_id,login_page in login_pages.items():
                    if config_id == uuid.UUID(event.data["main_auth_factor_id"]).hex:
                        for form in login_page[self.LOGIN]["forms"]:
                            form["items"].extend(items)

    def check_auth_data(self, event, **kwargs):
        """ 响应检查认证凭证事件

        Args:
            event: 事件
        """
        tenant = event.tenant
        request = event.request

        data = request.POST or json.load(request.body)

        authcode = data.get('authcode')
        authcode_key = data.get('authcode_key')

        if not self.check_authcode(event.tenant,authcode,authcode_key):

            settings = self.get_settings(tenant)
            key, code, image = self.get_authcode_picture(
                settings.settings.get("auth_code_length",4),
                settings.settings.get("width",180),
                settings.settings.get("height",60)
            )

            cache.set(event.tenant,key,code,expired=settings.settings.get("expired",10)*60)
            rs = self.error(ErrorCode.AUTHCODE_NOT_MATCH)
            rs["data"] = {
                "image": str(image, 'utf8'),
                "authcode_key": key
            }

            return False,rs
        return True,None

    def create_register_page(self, event, config, config_data):
        pass

    def create_password_page(self, event, config, config_data):
        pass

    def create_other_page(self, event, config, config_data):
        pass

    def create_auth_manage_page(self):
        pass

    def create_extension_config_schema(self):
        """创建插件运行时配置schema描述
        """
        AuthCodeAuthFactorSchema = create_extension_schema(
            'AuthCodeAuthFactorSchema',
            __file__,
            [
                (
                    'login_enabled', 
                    bool, 
                    Field(
                        default=False, 
                        title=_('login_enabled', '启用登录'),
                        readonly=True
                    )
                ),
                (
                    'register_enabled', 
                    bool, 
                    Field(
                        default=False, 
                        title=_('register_enabled', '启用注册'),
                        readonly=True
                    )
                ),
                (
                    'reset_password_enabled', 
                    bool, 
                    Field(
                        default=False, 
                        title=_('reset_password_enabled', '启用重置密码'),
                        readonly=True
                    )
                ),
            ],
            BaseAuthFactorSchema,
        )
        self.register_auth_factor_schema(AuthCodeAuthFactorSchema, 'authcode')

    def create_extension_settings_schema(self):
        """创建租户配置schama
        """
        AuthCodeAuthFactorSettingsSchema = create_extension_schema(
            'AuthCodeAuthFactorSettingsSchema',
            __file__,
            [
                ('width', int, Field(title=_("验证码图片宽度"),default=180)),
                ('height',  int, Field(title=_("验证码图片高度"),default=60)),
                ('auth_code_length',  int, Field(title=_("验证码长度"),default=4)),
                (
                    'expired', 
                    Optional[int],
                    Field(
                        title=_('expired', '有效期/分钟'),
                        default=10,
                    )
                ),
            ]
        )

        self.register_settings_schema(AuthCodeAuthFactorSettingsSchema)

    def get_random_char(self,auth_code_length=4)->str:
        """获取随机字符组合

        Args:
            auth_code_length (int, optional): 图形验证码长度. Defaults to 4.

        Returns:
            str: 随机字符串
        """
        chr_all = string.ascii_letters + string.digits
        str_random = ''.join(random.sample(chr_all, auth_code_length))
        return str_random

    def get_random_color(self, low, high):
        """获取随机颜色

        Args:
            low (int): 下限
            high (int): 上限

        Returns:
            tuple(int,int,int): RGB
        """
        return (
            random.randint(low, high),
            random.randint(low, high),
            random.randint(low, high),
        )

    def get_authcode_picture(self,auth_code_length=4,width=180,height=60):
        """制作验证码图片

        Args:
            auth_code_length (int, optional): 验证码长度. Defaults to 4.
            width (int, optional): 图形宽度. Defaults to 180.
            height (int, optional): 图形高度. Defaults to 60.

        Returns:
            tuple(str,str,image): 缓存key,图形验证码,图片
        """
        # 创建空白画布
        image = Image.new('RGB', (width, height), self.get_random_color(20, 100))
        # 验证码的字体
        font = ImageFont.truetype(
            os.path.join(
                os.path.dirname(
                    os.path.abspath(
                        __file__
                    )
                ),
                'assets/stxinwei.ttf'
            ),
            40
        )
        # 创建画笔
        draw = ImageDraw.Draw(image)
        # 获取验证码
        char_4 = self.get_random_char(auth_code_length)
        # 向画布上填写验证码
        for i in range(auth_code_length):
            draw.text(
                (40 * i + 10, 0),
                char_4[i],
                font=font,
                fill=self.get_random_color(100, 200),
            )
        # 绘制干扰点
        for x in range(random.randint(200, 600)):
            x = random.randint(1, width - 1)
            y = random.randint(1, height - 1)
            draw.point((x, y), fill=self.get_random_color(50, 150))
        # 模糊处理
        image = image.filter(ImageFilter.BLUR)
        key = self.generate_key()
        buf = BytesIO()
        # 将图片保存在内存中,文件类型为png
        image.save(buf, 'png')
        byte_data = buf.getvalue()
        base64_str = base64.b64encode(byte_data)
        return key, char_4, base64_str

    def generate_key(self):
        """生成随机key

        Returns:
            str: 随机key
        """
        key = '{}'.format(uuid.uuid4().hex)
        return key

    @operation(GenrateAuthCodeOut, roles=[TENANT_ADMIN, PLATFORM_ADMIN, NORMAL_USER])
    def get_authcode(self, request, tenant_id: str):
        """ 获取图形验证码
        """
        tenant = Tenant.active_objects.get(id=tenant_id)
        settings = self.get_settings(tenant)
        key, code, image = self.get_authcode_picture(
            settings.settings.get("auth_code_length",4),
            settings.settings.get("width",180),
            settings.settings.get("height",60)
        )

        cache.set(request.tenant,key,code,expired=settings.settings.get("expired",10)*60)
        return {
            "data": {
                "image": str(image, 'utf8'),
                "authcode_key": key
            }
        }

    @operation(CheckAuthCodeOut, roles=[TENANT_ADMIN, PLATFORM_ADMIN, NORMAL_USER])
    def check_auth_code(self,request,tenant_id:str,data:CheckAuthCodeIn):
        """ 校验图形验证码
        """
        if self.check_authcode(request.tenant,data.authcode, data.authcode_key):
            return self.success()
        else:
            return self.error(
                ErrorCode.AUTHCODE_NOT_MATCH
            )

    def check_authcode(self,tenant,authcode,authcode_key):
        """校验图形验证码
        """
        return authcode_key and cache.get(tenant,authcode_key).lower() == authcode.lower()

    def register_extension_api(self):
        """注册插件API
        """
        self.generate_code_path = self.register_api(
            '/auth_code/',
            'GET',
            self.get_authcode,
            tenant_path=True,
            auth=None,
            response=GenrateAuthCodeOut,
        )

        self.check_code_path = self.register_api(
            '/auth_code/',
            'POST',
            self.check_auth_code,
            tenant_path=True,
            auth=None,
            response=CheckAuthCodeOut,
        )

authenticate(self, event, **kwargs) #

抽象方法:认证

Parameters:

Name Type Description Default
event Event

认证事件

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def authenticate(self, event, **kwargs):
    pass

check_auth_code(self, request, tenant_id, data) #

校验图形验证码

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
@operation(CheckAuthCodeOut, roles=[TENANT_ADMIN, PLATFORM_ADMIN, NORMAL_USER])
def check_auth_code(self,request,tenant_id:str,data:CheckAuthCodeIn):
    """ 校验图形验证码
    """
    if self.check_authcode(request.tenant,data.authcode, data.authcode_key):
        return self.success()
    else:
        return self.error(
            ErrorCode.AUTHCODE_NOT_MATCH
        )

check_auth_data(self, event, **kwargs) #

响应检查认证凭证事件

Parameters:

Name Type Description Default
event

事件

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def check_auth_data(self, event, **kwargs):
    """ 响应检查认证凭证事件

    Args:
        event: 事件
    """
    tenant = event.tenant
    request = event.request

    data = request.POST or json.load(request.body)

    authcode = data.get('authcode')
    authcode_key = data.get('authcode_key')

    if not self.check_authcode(event.tenant,authcode,authcode_key):

        settings = self.get_settings(tenant)
        key, code, image = self.get_authcode_picture(
            settings.settings.get("auth_code_length",4),
            settings.settings.get("width",180),
            settings.settings.get("height",60)
        )

        cache.set(event.tenant,key,code,expired=settings.settings.get("expired",10)*60)
        rs = self.error(ErrorCode.AUTHCODE_NOT_MATCH)
        rs["data"] = {
            "image": str(image, 'utf8'),
            "authcode_key": key
        }

        return False,rs
    return True,None

check_authcode(self, tenant, authcode, authcode_key) #

校验图形验证码

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def check_authcode(self,tenant,authcode,authcode_key):
    """校验图形验证码
    """
    return authcode_key and cache.get(tenant,authcode_key).lower() == authcode.lower()

create_auth_manage_page(self) #

认证管理页面描述

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def create_auth_manage_page(self):
    pass

create_extension_config_schema(self) #

创建插件运行时配置schema描述

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def create_extension_config_schema(self):
    """创建插件运行时配置schema描述
    """
    AuthCodeAuthFactorSchema = create_extension_schema(
        'AuthCodeAuthFactorSchema',
        __file__,
        [
            (
                'login_enabled', 
                bool, 
                Field(
                    default=False, 
                    title=_('login_enabled', '启用登录'),
                    readonly=True
                )
            ),
            (
                'register_enabled', 
                bool, 
                Field(
                    default=False, 
                    title=_('register_enabled', '启用注册'),
                    readonly=True
                )
            ),
            (
                'reset_password_enabled', 
                bool, 
                Field(
                    default=False, 
                    title=_('reset_password_enabled', '启用重置密码'),
                    readonly=True
                )
            ),
        ],
        BaseAuthFactorSchema,
    )
    self.register_auth_factor_schema(AuthCodeAuthFactorSchema, 'authcode')

create_extension_settings_schema(self) #

创建租户配置schama

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def create_extension_settings_schema(self):
    """创建租户配置schama
    """
    AuthCodeAuthFactorSettingsSchema = create_extension_schema(
        'AuthCodeAuthFactorSettingsSchema',
        __file__,
        [
            ('width', int, Field(title=_("验证码图片宽度"),default=180)),
            ('height',  int, Field(title=_("验证码图片高度"),default=60)),
            ('auth_code_length',  int, Field(title=_("验证码长度"),default=4)),
            (
                'expired', 
                Optional[int],
                Field(
                    title=_('expired', '有效期/分钟'),
                    default=10,
                )
            ),
        ]
    )

    self.register_settings_schema(AuthCodeAuthFactorSettingsSchema)

create_login_page(self, event, config, config_data) #

抽象方法:组装登录页面表单

Parameters:

Name Type Description Default
event Event

CREATE_LOGIN_PAGE_AUTH_FACTOR事件

required
config TenantExtensionConfig

插件运行时配置

required
config_data dict

运行时配置数据

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def create_login_page(self, event, config, config_data):
    pass

create_other_page(self, event, config, config_data) #

抽象方法:组装登录页上其他操作表单

Parameters:

Name Type Description Default
event Event

CREATE_LOGIN_PAGE_AUTH_FACTOR事件

required
config TenantExtensionConfig

插件运行时配置

required
config_data dict

运行时配置数据

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def create_other_page(self, event, config, config_data):
    pass

create_password_page(self, event, config, config_data) #

抽象方法:组装重置密码页面表单

Parameters:

Name Type Description Default
event Event

CREATE_LOGIN_PAGE_AUTH_FACTOR事件

required
config TenantExtensionConfig

插件运行时配置

required
config_data dict

运行时配置数据

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def create_password_page(self, event, config, config_data):
    pass

create_register_page(self, event, config, config_data) #

抽象方法:组装注册页面表单

Parameters:

Name Type Description Default
event Event

CREATE_LOGIN_PAGE_AUTH_FACTOR事件

required
config TenantExtensionConfig

插件运行时配置

required
config_data dict

运行时配置数据

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def create_register_page(self, event, config, config_data):
    pass

fix_login_page(self, event, **kwargs) #

向login_pages填入认证元素

Parameters:

Name Type Description Default
event

AUTHRULE_FIX_LOGIN_PAGE事件

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def fix_login_page(self, event, **kwargs):
    items = [
        {
            "type": "text",
            "name": "authcode",
            "append":{
                "type": "image",
                "http": {
                    "url": self.generate_code_path,
                    "method": "get",
                },
            },
            "placeholder":_("图形验证码")
        },
        {
            "type": "hidden",
            "name": "authcode_key",
        },
    ]
    for login_pages,ext in event.data["login_pages"]:
        if login_pages and isinstance(login_pages,dict):
            for config_id,login_page in login_pages.items():
                if config_id == uuid.UUID(event.data["main_auth_factor_id"]).hex:
                    for form in login_page[self.LOGIN]["forms"]:
                        form["items"].extend(items)

generate_key(self) #

生成随机key

Returns:

Type Description
str

随机key

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def generate_key(self):
    """生成随机key

    Returns:
        str: 随机key
    """
    key = '{}'.format(uuid.uuid4().hex)
    return key

get_authcode(self, request, tenant_id) #

获取图形验证码

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
@operation(GenrateAuthCodeOut, roles=[TENANT_ADMIN, PLATFORM_ADMIN, NORMAL_USER])
def get_authcode(self, request, tenant_id: str):
    """ 获取图形验证码
    """
    tenant = Tenant.active_objects.get(id=tenant_id)
    settings = self.get_settings(tenant)
    key, code, image = self.get_authcode_picture(
        settings.settings.get("auth_code_length",4),
        settings.settings.get("width",180),
        settings.settings.get("height",60)
    )

    cache.set(request.tenant,key,code,expired=settings.settings.get("expired",10)*60)
    return {
        "data": {
            "image": str(image, 'utf8'),
            "authcode_key": key
        }
    }

get_authcode_picture(self, auth_code_length=4, width=180, height=60) #

制作验证码图片

Parameters:

Name Type Description Default
auth_code_length int

验证码长度. Defaults to 4.

4
width int

图形宽度. Defaults to 180.

180
height int

图形高度. Defaults to 60.

60

Returns:

Type Description
tuple(str,str,image)

缓存key,图形验证码,图片

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def get_authcode_picture(self,auth_code_length=4,width=180,height=60):
    """制作验证码图片

    Args:
        auth_code_length (int, optional): 验证码长度. Defaults to 4.
        width (int, optional): 图形宽度. Defaults to 180.
        height (int, optional): 图形高度. Defaults to 60.

    Returns:
        tuple(str,str,image): 缓存key,图形验证码,图片
    """
    # 创建空白画布
    image = Image.new('RGB', (width, height), self.get_random_color(20, 100))
    # 验证码的字体
    font = ImageFont.truetype(
        os.path.join(
            os.path.dirname(
                os.path.abspath(
                    __file__
                )
            ),
            'assets/stxinwei.ttf'
        ),
        40
    )
    # 创建画笔
    draw = ImageDraw.Draw(image)
    # 获取验证码
    char_4 = self.get_random_char(auth_code_length)
    # 向画布上填写验证码
    for i in range(auth_code_length):
        draw.text(
            (40 * i + 10, 0),
            char_4[i],
            font=font,
            fill=self.get_random_color(100, 200),
        )
    # 绘制干扰点
    for x in range(random.randint(200, 600)):
        x = random.randint(1, width - 1)
        y = random.randint(1, height - 1)
        draw.point((x, y), fill=self.get_random_color(50, 150))
    # 模糊处理
    image = image.filter(ImageFilter.BLUR)
    key = self.generate_key()
    buf = BytesIO()
    # 将图片保存在内存中,文件类型为png
    image.save(buf, 'png')
    byte_data = buf.getvalue()
    base64_str = base64.b64encode(byte_data)
    return key, char_4, base64_str

get_random_char(self, auth_code_length=4) #

获取随机字符组合

Parameters:

Name Type Description Default
auth_code_length int

图形验证码长度. Defaults to 4.

4

Returns:

Type Description
str

随机字符串

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def get_random_char(self,auth_code_length=4)->str:
    """获取随机字符组合

    Args:
        auth_code_length (int, optional): 图形验证码长度. Defaults to 4.

    Returns:
        str: 随机字符串
    """
    chr_all = string.ascii_letters + string.digits
    str_random = ''.join(random.sample(chr_all, auth_code_length))
    return str_random

get_random_color(self, low, high) #

获取随机颜色

Parameters:

Name Type Description Default
low int

下限

required
high int

上限

required

Returns:

Type Description
tuple(int,int,int)

RGB

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def get_random_color(self, low, high):
    """获取随机颜色

    Args:
        low (int): 下限
        high (int): 上限

    Returns:
        tuple(int,int,int): RGB
    """
    return (
        random.randint(low, high),
        random.randint(low, high),
        random.randint(low, high),
    )

load(self) #

加载插件

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def load(self):
    """加载插件
    """
    self.create_extension_config_schema()
    self.create_extension_settings_schema()
    self.register_extension_api()

    super().load()

    # 初始化部分配置数据
    tenant = Tenant.platform_tenant()
    if self.get_settings(tenant):
        settings = {
            "width": 180, 
            "height": 60, 
            "auth_code_length": 4
        }

        self.update_or_create_settings(tenant, settings, True, False)

    if not self.get_tenant_configs(tenant):
        config = {
            "login_enabled": False, 
            "register_enabled": False, 
            "reset_password_enabled": False
        }

        self.create_tenant_config(tenant, config, "图形验证码", "authcode")

register_extension_api(self) #

注册插件API

Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def register_extension_api(self):
    """注册插件API
    """
    self.generate_code_path = self.register_api(
        '/auth_code/',
        'GET',
        self.get_authcode,
        tenant_path=True,
        auth=None,
        response=GenrateAuthCodeOut,
    )

    self.check_code_path = self.register_api(
        '/auth_code/',
        'POST',
        self.check_auth_code,
        tenant_path=True,
        auth=None,
        response=CheckAuthCodeOut,
    )

reset_password(self, event, **kwargs) #

抽象方法:响应重置密码事件

Parameters:

Name Type Description Default
event Event

重置密码事件

required
Source code in extension_root/com_longgui_auth_factor_authcode/__init__.py
def reset_password(self, event, **kwargs):
    pass

评论