图形验证码认证因素#
功能介绍#
对用户认证凭证表单进行扩充,插入图形验证码并实现相关验证功能
注意:图形验证码认证因素不具有认证/注册/修改密码等功能,仅对其他认证因素进行凭证元素扩充
普通用户:
- 在 “登录” 页面实现向指定表单插入图形验证码
配置指南#
实现思路#
- 普通用户:图形验证码:
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并跳转至桌面,如完成重置密码操作或者未完成注册/登录操作则提示错误
抽象方法实现#
- load
- authenticate
- register
- reset_password
- create_login_page
- create_register_page
- create_password_page
- create_other_page
- create_auth_manage_page
- check_auth_data
- fix_login_page
代码#
        
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)
#
    
  
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)
#
    
  
create_auth_manage_page(self)
#
    
  
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 | 
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 | 
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 | 
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 | 
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)
#
    
  
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
          
        
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 | 
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,
    )


