rainycode 1.0.9__tar.gz → 1.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rainycode-1.0.9 → rainycode-1.1.1}/PKG-INFO +1 -1
- rainycode-1.1.1/common_base/consts.py +9 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_depends/auth_depend.py +3 -2
- {rainycode-1.0.9 → rainycode-1.1.1}/common_models/base_model.py +2 -2
- rainycode-1.1.1/common_servers/api/auth_api.py +85 -0
- rainycode-1.1.1/common_servers/api/wechat_api.py +98 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/schemas/auth_schema.py +5 -5
- rainycode-1.1.1/common_servers/service/auth_svc.py +550 -0
- {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/bcrypt_util.py +2 -4
- {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/ip_util.py +4 -4
- {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/wechat_util.py +23 -1
- {rainycode-1.0.9 → rainycode-1.1.1}/core/base_config.py +4 -1
- {rainycode-1.0.9 → rainycode-1.1.1}/core/databases/aiodb.py +1 -4
- {rainycode-1.0.9 → rainycode-1.1.1}/core/databases/aioredis.py +1 -1
- {rainycode-1.0.9 → rainycode-1.1.1}/core/start.py +2 -2
- {rainycode-1.0.9 → rainycode-1.1.1}/pyproject.toml +1 -1
- {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/PKG-INFO +1 -1
- {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/SOURCES.txt +6 -9
- {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/top_level.txt +1 -1
- rainycode-1.0.9/common_base/consts.py +0 -3
- rainycode-1.0.9/common_servers/api/auth_api.py +0 -50
- rainycode-1.0.9/common_servers/api/wechat_api.py +0 -48
- rainycode-1.0.9/common_servers/service/auth_svc.py +0 -131
- rainycode-1.0.9/common_servers/strategies/base_strategy.py +0 -9
- rainycode-1.0.9/common_servers/strategies/password_strategy.py +0 -34
- rainycode-1.0.9/common_servers/strategies/wechat_stragegy.py +0 -141
- {rainycode-1.0.9 → rainycode-1.1.1}/common_base/aiorequests.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_base/exception.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_base/logging.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_base/response.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_models/__init__.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_models/user_model.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_models/wechat_model.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/router.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/schemas/__init__.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/common_servers/service/wechat_svc.py +0 -0
- {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/captcha_util.py +0 -0
- {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/jwt_util.py +0 -0
- {rainycode-1.0.9/common_utlis → rainycode-1.1.1/common_utils}/snowflake_util.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/core/middleware/http_middleware.py +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/dependency_links.txt +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/rainycode.egg-info/requires.txt +0 -0
- {rainycode-1.0.9 → rainycode-1.1.1}/setup.cfg +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
REDIS_PREFIX_REFRESH_TOKEN: str = "auth:refresh:"
|
|
2
|
+
REDIS_PREFIX_BLOCKLIST: str = "auth:blocklist:"
|
|
3
|
+
REDIS_PREFIX_USER_INFO: str = "user:info:"
|
|
4
|
+
REDIS_PREFIX_OAUTH_STATE: str = "oauth:state:"
|
|
5
|
+
REDIS_PREFIX_WECHAT_CODE: str = "wechat:code:"
|
|
6
|
+
REDIS_PREFIX_WECHAT_OAUTH_CODE: str = "wechat:oauth:code:"
|
|
7
|
+
REDIS_PREFIX_TEMP_CODE: str = "oauth:temp_code:"
|
|
8
|
+
REDIS_PREFIX_LOGIN_LOCK_USER: str = "auth:login_lock:user:"
|
|
9
|
+
REDIS_PREFIX_LOGIN_LOCK_IP: str = "auth:login_lock:ip:"
|
|
@@ -4,7 +4,7 @@ from fastapi.security import OAuth2PasswordBearer, SecurityScopes
|
|
|
4
4
|
from common_base.consts import REDIS_PREFIX_BLOCKLIST, REDIS_PREFIX_USER_INFO
|
|
5
5
|
from common_base.exception import CustomException
|
|
6
6
|
from common_models.user_model import User
|
|
7
|
-
from
|
|
7
|
+
from common_utils.jwt_util import JwtUtil
|
|
8
8
|
from core.databases.aioredis import AioRedis
|
|
9
9
|
|
|
10
10
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth_center/login")
|
|
@@ -53,6 +53,7 @@ async def get_current_user(request: Request, token: str, security_scopes: Securi
|
|
|
53
53
|
|
|
54
54
|
# 设置请求上下文
|
|
55
55
|
request.state.user = user
|
|
56
|
+
request.state.token = token
|
|
56
57
|
return user
|
|
57
58
|
|
|
58
59
|
|
|
@@ -71,5 +72,5 @@ async def login_or_none(request: Request, security_scopes: SecurityScopes, token
|
|
|
71
72
|
|
|
72
73
|
try:
|
|
73
74
|
return await get_current_user(request, token, security_scopes)
|
|
74
|
-
except
|
|
75
|
+
except CustomException:
|
|
75
76
|
return None
|
|
@@ -5,7 +5,7 @@ from typing import Optional, Any
|
|
|
5
5
|
from tortoise import fields, BaseDBAsyncClient
|
|
6
6
|
from tortoise.expressions import Q
|
|
7
7
|
from tortoise.queryset import BulkCreateQuery, QuerySet, QuerySetSingle
|
|
8
|
-
from
|
|
8
|
+
from common_utils.snowflake_util import get_snowflake_id
|
|
9
9
|
from core.databases.aiodb import MySQLUtils
|
|
10
10
|
|
|
11
11
|
|
|
@@ -86,7 +86,7 @@ class BaseModel(MySQLUtils):
|
|
|
86
86
|
force_update: bool = False,
|
|
87
87
|
**kwargs: Any
|
|
88
88
|
) -> None:
|
|
89
|
-
if not getattr(self, 'id', 0) and kwargs.get('
|
|
89
|
+
if not getattr(self, 'id', 0) and kwargs.get('id_uuid', False) and 'id' in self._meta.fields:
|
|
90
90
|
setattr(self, 'id', str(uuid.uuid4()).replace('-', ''))
|
|
91
91
|
return await super().save(using_db=using_db, update_fields=update_fields, force_create=force_create,
|
|
92
92
|
force_update=force_update)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from common_models.user_model import User
|
|
2
|
+
from fastapi import APIRouter, Security, Body, Depends, Query, Request
|
|
3
|
+
from common_base.response import SuccessReturn
|
|
4
|
+
from common_depends.auth_depend import login_require, oauth2_scheme
|
|
5
|
+
from common_servers.schemas.auth_schema import *
|
|
6
|
+
from common_servers.service.auth_svc import AuthService
|
|
7
|
+
|
|
8
|
+
router = APIRouter(prefix="/auth")
|
|
9
|
+
|
|
10
|
+
@router.post("/register", summary="用户注册")
|
|
11
|
+
async def register(user_in: UserRegister):
|
|
12
|
+
user = await AuthService.register(user_in)
|
|
13
|
+
return SuccessReturn(
|
|
14
|
+
data={"id": str(user.id), "username": user.username}, msg="注册成功"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.post("/login", summary="用户密码登录")
|
|
19
|
+
async def login(user_in: UserLogin, request: Request):
|
|
20
|
+
client_ip = request.client.host if request.client else "unknown"
|
|
21
|
+
token_data = await AuthService.login(user_in, client_ip)
|
|
22
|
+
return SuccessReturn(data=token_data, msg="登录成功")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.post("/refresh", response_model=TokenResponse, summary="刷新令牌")
|
|
26
|
+
async def refresh_token(refresh_token: str = Body(..., embed=True)):
|
|
27
|
+
token_data = await AuthService.refresh_token(refresh_token)
|
|
28
|
+
return SuccessReturn(data=token_data, msg="刷新成功")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@router.post("/logout", summary="登出")
|
|
32
|
+
async def logout(
|
|
33
|
+
request: Request,
|
|
34
|
+
refresh_token: str = Body(..., embed=True),
|
|
35
|
+
_: User = Security(login_require),
|
|
36
|
+
):
|
|
37
|
+
token = request.state.token
|
|
38
|
+
await AuthService.logout(token, refresh_token)
|
|
39
|
+
return SuccessReturn(msg="登出成功")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/me", response_model=UserInfo, summary="获取当前用户信息")
|
|
43
|
+
async def get_me(user: User = Security(login_require)):
|
|
44
|
+
return SuccessReturn(data=user)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.post("/wechat/mini", response_model=TokenResponse, summary="微信小程序登录")
|
|
48
|
+
async def login_mini(wechat_in: WechatLogin):
|
|
49
|
+
"""
|
|
50
|
+
微信小程序登录
|
|
51
|
+
|
|
52
|
+
- 静默登录: 传 code + app_id
|
|
53
|
+
- 一键登录(获取手机号): 额外传 encrypted_data + iv
|
|
54
|
+
"""
|
|
55
|
+
token_data = await AuthService.login_wechat_mini(wechat_in)
|
|
56
|
+
return SuccessReturn(data=token_data, msg="登录成功")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/wechat/oauth", summary="获取微信网页授权链接 - 回调(/oauth/callback)")
|
|
60
|
+
async def get_wechat_oauth_url(
|
|
61
|
+
app_id: str = Query(..., description="微信 AppID"),
|
|
62
|
+
redirect_uri: str = Query(..., description="授权成功后跳转地址"),
|
|
63
|
+
state: str | None = Query(default=None, description="自定义状态参数"),
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
获取微信网页授权链接(snsapi_base 静默授权)
|
|
67
|
+
|
|
68
|
+
- 首次请求返回 snsapi_base 授权链接
|
|
69
|
+
- 如果用户已注册,回调后直接返回 token
|
|
70
|
+
- 如果用户未注册,自动重定向到 snsapi_userinfo 授权
|
|
71
|
+
"""
|
|
72
|
+
authorize_url = await AuthService.get_wechat_oauth_url(app_id, redirect_uri, state)
|
|
73
|
+
return SuccessReturn(data={"authorize_url": authorize_url})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@router.get("/wechat/token", summary="通过临时授权码获取 token")
|
|
77
|
+
async def get_token_by_temp_code(code: str = Query(..., description="临时授权码")):
|
|
78
|
+
"""
|
|
79
|
+
OAuth 回调后,前端使用临时码换取 token
|
|
80
|
+
|
|
81
|
+
- 临时码有效期:120 秒
|
|
82
|
+
- 一次性使用,获取后立即失效
|
|
83
|
+
"""
|
|
84
|
+
token_data = await AuthService.get_token_by_temp_code(code)
|
|
85
|
+
return SuccessReturn(data=token_data, msg="获取成功")
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from urllib.parse import urlencode
|
|
3
|
+
from fastapi import APIRouter, Request, Query, Response
|
|
4
|
+
from fastapi.responses import RedirectResponse
|
|
5
|
+
from common_base.consts import REDIS_PREFIX_OAUTH_STATE
|
|
6
|
+
from common_base.exception import CustomException
|
|
7
|
+
from common_servers.service.auth_svc import AuthService
|
|
8
|
+
from common_servers.service.wechat_svc import WechatService
|
|
9
|
+
from core.databases.aioredis import AioRedis
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/wechat")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("/callback/{app_pk}", summary="微信回调验证")
|
|
15
|
+
async def verify_callback(
|
|
16
|
+
app_pk: int,
|
|
17
|
+
signature: str = Query(..., description="微信加密签名"),
|
|
18
|
+
timestamp: str = Query(..., description="时间戳"),
|
|
19
|
+
nonce: str = Query(..., description="随机数"),
|
|
20
|
+
echostr: str = Query(..., description="随机字符串"),
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
微信服务器配置验证接口
|
|
24
|
+
"""
|
|
25
|
+
is_valid = await WechatService.verify_signature(app_pk, signature, timestamp, nonce)
|
|
26
|
+
if is_valid:
|
|
27
|
+
# 必须直接返回 echostr 字符串,不能包含 json 格式
|
|
28
|
+
return Response(content=echostr, media_type="text/plain")
|
|
29
|
+
return Response(content="fail", media_type="text/plain")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@router.post("/callback/{app_pk}", summary="微信消息回调")
|
|
33
|
+
async def handle_callback(
|
|
34
|
+
app_pk: int,
|
|
35
|
+
request: Request,
|
|
36
|
+
signature: str = Query(..., description="微信加密签名"),
|
|
37
|
+
timestamp: str = Query(..., description="时间戳"),
|
|
38
|
+
nonce: str = Query(..., description="随机数"),
|
|
39
|
+
openid: str = Query(None, description="用户OpenID"),
|
|
40
|
+
encrypt_type: str = Query(None, description="加密类型"),
|
|
41
|
+
msg_signature: str = Query(None, description="消息体签名"),
|
|
42
|
+
):
|
|
43
|
+
"""
|
|
44
|
+
接收微信推送的消息和事件
|
|
45
|
+
"""
|
|
46
|
+
# 获取原始 XML 数据
|
|
47
|
+
body = await request.body()
|
|
48
|
+
xml_data = body.decode("utf-8")
|
|
49
|
+
|
|
50
|
+
# 目前仅实现了明文模式,加密模式需要结合 msg_secret 解密
|
|
51
|
+
# 如果 encrypt_type == "aes",需要解密 (暂未实现,TODO)
|
|
52
|
+
result = await WechatService.handle_message(
|
|
53
|
+
app_pk, xml_data, signature, timestamp, nonce
|
|
54
|
+
)
|
|
55
|
+
return Response(content=result, media_type="application/xml")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@router.get("/oauth/callback", summary="微信网页授权回调")
|
|
59
|
+
async def wechat_oauth_callback(
|
|
60
|
+
code: str | None = Query(default=None, description="微信授权码"),
|
|
61
|
+
state: str | None = Query(default=None, description="状态参数"),
|
|
62
|
+
error: str | None = Query(default=None, description="错误码"),
|
|
63
|
+
error_description: str | None = Query(default=None, description="错误描述"),
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
处理微信网页授权回调
|
|
67
|
+
|
|
68
|
+
- 用户已存在:重定向到前端(带 token)
|
|
69
|
+
- 用户不存在:自动重定向到 snsapi_userinfo 授权
|
|
70
|
+
- 用户拒绝授权:重定向到前端并附带错误信息
|
|
71
|
+
"""
|
|
72
|
+
# 处理用户拒绝授权的情况
|
|
73
|
+
if error:
|
|
74
|
+
# 获取 redirect_uri
|
|
75
|
+
cache_key = f"{REDIS_PREFIX_OAUTH_STATE}{state}"
|
|
76
|
+
cache_value = await AioRedis.get(cache_key)
|
|
77
|
+
if cache_value:
|
|
78
|
+
try:
|
|
79
|
+
cached_data = json.loads(cache_value)
|
|
80
|
+
redirect_uri = cached_data.get("redirect_uri")
|
|
81
|
+
except (json.JSONDecodeError, TypeError):
|
|
82
|
+
redirect_uri = None
|
|
83
|
+
|
|
84
|
+
if redirect_uri:
|
|
85
|
+
await AioRedis.delete(cache_key)
|
|
86
|
+
params = {
|
|
87
|
+
"error": error,
|
|
88
|
+
"error_description": error_description or "用户拒绝授权",
|
|
89
|
+
}
|
|
90
|
+
error_url = f"{redirect_uri}?{urlencode(params)}"
|
|
91
|
+
return RedirectResponse(url=error_url, status_code=302)
|
|
92
|
+
|
|
93
|
+
raise CustomException(msg="授权失败")
|
|
94
|
+
|
|
95
|
+
if not code:
|
|
96
|
+
raise CustomException(msg="缺少授权码")
|
|
97
|
+
|
|
98
|
+
return await AuthService.handle_oauth_callback(code, state)
|
|
@@ -36,8 +36,8 @@ class UserInfo(BaseModel):
|
|
|
36
36
|
from_attributes = True
|
|
37
37
|
|
|
38
38
|
class WechatLogin(BaseModel):
|
|
39
|
-
"""
|
|
40
|
-
code: str = Field(..., description="
|
|
41
|
-
app_id: str = Field(..., description="
|
|
42
|
-
encrypted_data: str | None = Field(default=None, description="加密数据(
|
|
43
|
-
iv: str | None = Field(default=None, description="
|
|
39
|
+
"""微信小程序登录模型"""
|
|
40
|
+
code: str = Field(..., description="微信登录Code (wx.login获取)")
|
|
41
|
+
app_id: str = Field(..., description="小程序AppID")
|
|
42
|
+
encrypted_data: str | None = Field(default=None, description="加密数据(一键登录手机号)")
|
|
43
|
+
iv: str | None = Field(default=None, description="加密算法初始向量")
|