From a3206bb0f6bfad54bc6e32cf83b26b796abe7903 Mon Sep 17 00:00:00 2001 From: jayhgq Date: Wed, 29 Apr 2026 17:59:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=B7=BB=E5=8A=A0=E6=9D=83?= =?UTF-8?q?=E9=99=90=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现权限管理模块,包括: 1. 权限模型、路由和中间件 2. 角色权限多对多关联 3. 权限验证中间件 4. 初始化权限数据 5. 权限管理API接口 --- .../auth/__pycache__/config.cpython-313.pyc | Bin 3664 -> 3654 bytes .../auth/__pycache__/database.cpython-313.pyc | Bin 1115 -> 1105 bytes .../__pycache__/init_data.cpython-313.pyc | Bin 7705 -> 13046 bytes .../__pycache__/middleware.cpython-313.pyc | Bin 2193 -> 7389 bytes .../auth/__pycache__/models.cpython-313.pyc | Bin 2609 -> 3769 bytes .../auth/__pycache__/schemas.cpython-313.pyc | Bin 6530 -> 8746 bytes .../apps/__pycache__/urls.cpython-313.pyc | Bin 436 -> 528 bytes .../__pycache__/urls.cpython-313.pyc | Bin 0 -> 8027 bytes backend/auth/apps/permissions/urls.py | 173 ++++++++++++++++++ backend/auth/apps/urls.py | 4 +- backend/auth/booksystem.db | Bin 0 -> 49152 bytes backend/auth/database.py | 2 +- backend/auth/init_data.py | 111 ++++++++++- backend/auth/middleware.py | 106 +++++++++++ backend/auth/models.py | 38 +++- backend/auth/schemas.py | 41 +++++ 16 files changed, 471 insertions(+), 4 deletions(-) create mode 100644 backend/auth/apps/permissions/__pycache__/urls.cpython-313.pyc create mode 100644 backend/auth/apps/permissions/urls.py create mode 100644 backend/auth/booksystem.db diff --git a/backend/auth/__pycache__/config.cpython-313.pyc b/backend/auth/__pycache__/config.cpython-313.pyc index b5903ef6a6be919c10a17cf36e8e0b0f51a07d48..65b581c81ca20472baec8bec391163b967a48d4f 100644 GIT binary patch delta 49 zcmca0b4-T&GcPX}0}wdx{mb3umtGcPX}0}#9|I=_*7CbOcZi&ac=eoAVLQ+|GSaAk2xYHmzYVsdtBUP?@2 NX-P)R=H1NQTmV={6*d3> diff --git a/backend/auth/__pycache__/database.cpython-313.pyc b/backend/auth/__pycache__/database.cpython-313.pyc index 3b8719384465af454fdbc69c6a04c6314c10fbf0..b9b0450ac38ee5dbb8ac6fdb34a7f69abb2bda76 100644 GIT binary patch delta 51 zcmcc3agl@fGcPX}0}zBS{g~;!k++aZTv0zGKQ~oBDL+5ExU#q;HCI2ev?N1+^L(Z~ Fi~y)+5n})V delta 61 zcmcb}ahrqpGcPX}0}vD>FUs`V$Xm#ysO@4ElboND8sn6opB-FTT#}j_la!d8otl>t PlUQ1k5wm$J(;h|uRhtz2 diff --git a/backend/auth/__pycache__/init_data.cpython-313.pyc b/backend/auth/__pycache__/init_data.cpython-313.pyc index ad1310d1ab212a34e448fd109ee42d41ada90eac..501cd715062c661d1526e0716394d1cea8d9a565 100644 GIT binary patch delta 5394 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 delta 1029 zcmaJL$pGApA&z@GmGZ#31UT_k2naL3vv z>$i0}o`Llhzm0yfWc5Zgn6)`Nt&E@Xvt8;=4c36J3ZQz0pEENTSghS+YMJqSOe3?L zJ*JgeZa;6&$-cDe6cXE$j0qBZN)&K*r)GY(=(4hGM+d)7!KZgVa}4!kN{Yk(?b(fp9V+1db$= z1Ah6wKyN5KAS5DzP&#!!Kw7m%qBobJ3gR-`T15>6UN5XfceIrV>wu)`G}gwZ)?O^E zE{s}bYF&N)aPz^<&G9EPU7vcoert}Nk|A@iVTQ5mB(*RsgwrVjvk;C|ON3s3UM*D>{bJs#PvmEP@X(RsXz>e zVj&z%CWK%T2NSU4GU~6Uu8L>`bCkI$lu#(8KokEvFC5s_nD&Y*@&F}16p2R@a$Ce) zb%t*RV-tv&N4&1e`ROW&Lwa(>F~T`%kc<3ul>fJ}24|*#HijDk{^TOBorh2hL9S}m zF|c2nceu(Gs0}`ifdoA&#WRNVd9viF*Q{;SFurOK14(!XOzdvEbjX<2hG{tbF%9|U zEwgn3SpX5c06sGrW|h@0@ve8Q`vY&7=(*v3&l;ClBRN}J)1hWmre(HtnJxJt(&xBl zZa1O3sJ)QAR{+{$)Lz8SQEgFy+KbsmWj@u55XEAOKD&;5t8EKQ`gAH96WxO$Q5;I* zi2N4#?+P)k4AVJmgD|)dK+-hFlaX{xXu&6dA`r0yU`vS*T46jZOzB6abcJzlapYB< REssOa$>Tc=Y`7cTegj2`2vGn4 diff --git a/backend/auth/__pycache__/middleware.cpython-313.pyc b/backend/auth/__pycache__/middleware.cpython-313.pyc index f4e1bb5e82ef788b20a81c6dd82443faaf2f4c5a..46a02ad282a67b453e6a2f3b5c641dc820c33ee8 100644 GIT binary patch literal 7389 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 b=PDPr?ENu*TX-|i4lG+u9>Zv*Hp_njKcLWz delta 996 zcmY+D?@Lor7{|}uue+aGI)9crv|6`vWjLac2q}Y&jF{1z!Mbi&9o}tv?!7N2#6%$k zF=kh@u%ce2NitFUG3jsUMS*xhDsp%1MG!dOcb+@5I&ePcInQ~{^PKxV=YD8dak_UM z4l5$*k2Fa_;ieaVd-nwKVC^FQ9QehXtQKnP=!+bFLbqq?Xs@=fa%xkyc$ z15RAoU&U0*&vX|lM&8tUQ;c3Q$0eR;dwEVVN%82g#NkSkV9!pFo0!jSWYxL1g}Jpw zokB;rK33wzzX9s}n))cSAE3u|WRGG_#HFDKE0*#|mV-Py+3cbYSCP4vKr!4FZM>f z5#fTJiNs(aDTrJw*vp0oxp*YVO5#8;MizO&GnB**60ZRY2=@r?qb)TE`xTFzxU_1`6NC| zR(cp6K1@bj0dNv@Y2TlJvXT2Zn@dkIdRz@l2O>x&GF2b1=U-)T12H9#JwLOady_7V zE$5e43O0n~CHhWkgvJS;^F$*B9b`BJs0Gvj>H$XxBq*&u9nGa5l&Y34hoLmhOB%Q! z7$odwj^Y-W90ME&oB+51Cjs!@uz^5_8F}1%*H4av@M*vqKr4ab;8{WB6aCS+7>#il z}Ja45yzZFFH9HGHRQ cKGSuhhN8(qbxgDu5gCduC)j(44BD}O19J)qQ2+n{ diff --git a/backend/auth/__pycache__/schemas.cpython-313.pyc b/backend/auth/__pycache__/schemas.cpython-313.pyc index ea91a5dbcff720828f902928261260bf334f9bfb..d8aee0c718c5342618b15096b46009fdc672b85c 100644 GIT binary patch delta 1633 zcmZ`&TWlLe6y0goj^9cAj!FH96M`WGs>*^A6hWHM29VWyKWJK`JQrh>*y*eTr~>Sk zwz#5E9R`6>1Wmpg@f98)D7=3V5|g5>cC(p@nUZonOaK&_l@1NLCd zy35))>jlok$ss;f&IM>@{7 zTnQ3qU)ozql6`ACl!<8|#Rf@~bdvT+1J%bGV>(IS1)I6WAQ>eSw-{o^Sfd7con%J4 zWTB?GA=Ff|oRH~sLQxW_Bt~7C0pqL{VDtUgEAQpY*H^cevYX3G^y>r&nVG-Q=NZNIeF)tM zDFnVp%UVqCGt;<6z!;T&fH#BJDiROVIKQXdSPE3dv?tIRCkS1roW0Q=7AWxqPZZLN2n(sMMGd@d2MTx5^9I(#pI z*G0TJiom5fi8HK8BxJ#&umzWwoJ9Sb(r7}!NR)o|o2$3wPmySRssK=RPxCBK)Uxzt zcA8Gdn@&%S&~(O3$992zZqA6a*bfj!nyT&l41Jqa?;X zNp|FDLODAACC7OAl)em^#4)$u`? zm2{6zrzU5o%gH#+1n# z66$PeAgQ#;p%R(wAf}(D(Bvfwkq|S|Cd(+wa^(Vr8G*QX?qp}h*<4>4I2q-O^nn5Z D^LQ#G diff --git a/backend/auth/apps/__pycache__/urls.cpython-313.pyc b/backend/auth/apps/__pycache__/urls.cpython-313.pyc index b51d6bcad2d09c5f21ec2c265fa6e9e7c127c22c..73f9639a7a8febd8429df9ab075282d2f36a2397 100644 GIT binary patch delta 301 zcmdnOJb{JxGcPX}0}$+;|1q<8BCjOliizr)^}$TR%--x?%tagu3^9yK45lC@Kv2XP z!xJ1maKKeeT&TgwJ@J%>tUy6(QEp~&ab|vAv0iCWPVp@v zgvex0Mjam|{fzwFRQ;s<{Osb&;*!){{lwCe4E@A{f?|D;2EBsHTO2mI`6;D2sdhy| zKu3ZcTI>!aJ}@&fGTvp7z005nMplzM7*)itFsR>P;JVHrd67Z#f|Slx2Hhe7pke?M CqCZyv delta 164 zcmbQhvW1!VGcPX}0|*2z%H*2JE6G?jQC(9ghEa*Z6fDfZP{ba?9waz1Po5b>O^};- zL7kCv;t!9>X^hIjIxbc*$@wX%F;4mU*};{?C8@bFNr}nXsd*_eiKQhOF^L5Q#WAHt zImLPfmA5!-a`RJ4b5iY!1b~J!0&y|OmJiH~jEr{~WbZQQ0ny~Ej4B*g8PtpTfKmVn C4lOqT diff --git a/backend/auth/apps/permissions/__pycache__/urls.cpython-313.pyc b/backend/auth/apps/permissions/__pycache__/urls.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..308c4b6a7a8652e7cee923192533a8ce7fb151c7 GIT binary patch literal 8027 zcmcgxeQ+Dcb>BN24gm2%5(Eem6d~|KqCv}qB#M?unYJlPv;~^1ONfpf!oxtsNrDQ3 z%pRCZ%5*|0X+yPjW2Lnvf3$Hr>O}6e9XFF1%dTfUlI)H<)0qN>CO~fDk4#P3`VX+2 zWb|L(+r!}i2#S$p2zTD{KThHy<<* z@;nKXFgHm;9OXhhd9Xhn&Imz(wcZXs+!$#JH${x$=1A)f zG3@k`W5O|Rs3eAZLH15vrbdD64UuLebnoE}nsg!DQX{?d4$@usv~qXESF!Te8rd5` z_L#O_y$#>H?8==nhdnj&yFmUvU9LujUE1E>W#6k;-&P~N2c*|K)AmTqJ?ywQ;;h)2 zjvCp$ci8m}_awWsYS(==vNs*s81Ub~%?KD2%h;3qXO>n{5>-rlrDZ7@lNI~p2M#>B z@A!hWoQf|c1Ds+TlTRcU!jdejeNmPY(n1PG%o9;LH9j-D7ijT_l$wtn3h+w9lM-Ew zv)PI;vy_kmg3_e(#;FudNs3eF%t-R`Qc{)_7tYrCV<6*5QfaN`YO~>;P591*6-uRK zYJNqQDDZ??XiwUL%jHGLinXEx3lx{BbU1!o5mM13vcd)7DF-dBVSaHbCMA?c{fYTl zG!<3&*dfJM`KkqgpK=j_`Y!x#egHm@7XuCun?49cN4$M{o66 zGiIHuQM05TcjR$GG?`kvA7Jt*y!%_+^M+}ojU)}Th8e3SM{hM)NeZ*VeI<4jAa~BL z%?a~iLs%H*C2kJUg%wzbL!XdJzrHELhs$6W=7P+PQB z*M23N@2Q2^o{4> zcz=zHQ5m1T{laIjo?3t7r|bWEe*Kk;ukngiPDN8I^85m5lp@BYR5YFdn-h}JMM)7D z*BX=L1sYc^&8C>7W1UHmurlnh5NbL`rFqj>qLxJb7eZS9rx9J51%8q>cuo%gb`5!39@nLLOK?p)@ZrW3P}#9{_2% z82K_(H^?d}G!a+pxskIYuN=Pc^{j7e&bKq;+xd3GJB@EPzW+#Way|opyXLdLZ)97d zIeT<4d8% z=L%+A!Jp%w8!?)`KXGbeZSE>xM6;7xtxoc@d_8p0>}JM-OQt|N4zjCHz4*?_XfsqQ;HL2PvgH9$A0YTS38o-Gt zM@A4t)uLN~P#O`>bw!lvHsEf@d81G?3Fm>cHdEcjaF)sQuY$XZ5G#8tpSK-SWZdrH`&@A&$&&wqO9 zpVb|IjZ^q|Z2HFEAr9f!sq9ZLJ*y%R?F1g+O{FVaper%O0K3d!1I`3DS3ek~LI3=k zqj6Lp4c!e7;-SbRP~pjZ=^$-=IZuDa(|>V$+S8x*Jeq5MbT#ym)m^gAeHq6@+B}iB zqJ0%j#5u;@k`Y%=dj`1|xoyHrZG+rG*)75Vx;$nS{|Mh$G7A3pHHvzQe;2pP-N~(1 zJcsC9Q)bD?5pcaFv%aG-f@_^AyHj06+2D+`MomGO>&3HWMR&d62Aegm-j*eZbEq<` z)N_{yH_wu}R&7Su7#4?(w?T$!)}*b1sj0pSQ`oEq0JA0r49CDtwlh_=FgJPq<)2-D z;q?+M&<=PH-GCM9U*^>RMDs(CtKCYYQ*PM zJF!YYLWM-Q!lFV}bLkP?aOgNp3c*i)4=M&)&Jt+-((L%_lUcJjXYR_FyDs$qB>3at z>+`vx{TcY{-v8et|1Y56goNgs0%YZybN=XnKRy%-$=^tB{TUZJ z7gvAU^=QuV=;~v6n>+2fKW*KT7Pb@{iQ{RmXd{j2R}djikMVo#++~5=V-YSJcLRNe zaC@x670!aRm4o`EDHv7LOab$}D*Z)>XYOYMc=wX~`AYP6rjB#DXPqwVDXhL!%8sET ztE&O^a#j~4kcZXH(OSskG9gLvu9Ds^@HH#XrdRRyr|o%TtFi_p9d; zXHD8#{bXu|5G(PMg9j4j3>m%|^QM}j?Q5-#v%C`>Urm`6w71;;#2mUuRv#Jcw7r13du8+2mr_3@RBr*?Y2J23m=lt5}Ot(hqZ8w=UYb$$_pMsU+_se|JjCnaC z)nnZBR=}#a;I|CFr)6FpKTJw7svD$zdg-mtfAYi6&-`?K?JsWp`LmTU#sXAzusT3} z%>Y>{-1+ zp%}=h2{T%XVm?fl79pEn1nAt4L**oy0ihz`qEs0SCo$etxMdkrCKdf11~3XzHJ%bwx9LB(f99tmkzuWc{7sPzCY^=Wm_h5_DMiU zYje)po3Zw0t(#Z(eq?Q|cG>9t-H`FLcjnr+Tx;L5X3u-t&z(Gb^1|kS@n*YsV;@TLVXV709~-{d-HI%~>1Th9f~2J)k0xzVW) zMyIl_X{ax8*W7(M_tp>GTVHSfxqC1(I+bxvLso7R3q&xAU-S{j6I`*0IG^E)4wh+q zZ+&4S?mq%xkHRKG!aZTbgnQq^m~iie&Q+0{;KZw@cBFd;pnliMO&ElCTiTGmpPMiW z?{0Yj==a+F6DHex!y?fC$#Ga4SZ!pr$q1a2jX^xM;J2AZLSo1iuD)|P4F~#FI){4( z#xi)pj+KfASe4g%cMNXI5&sCGMWjcJP)`RoE2hKn z|AFXooI?CWC}#F*mewkNGZ9T-0(fFTI-VMc;y)PTOB53Zr9mHLmq~!oAO&wOD7r<( zjB(nbDBSWR1S=xT_*BTJc!CPtJ7H=mQ@mSH4-4aTc8ZW7#$&OB^rulOQB+@wc{E&y zvVVf8fw8c>B&XOtB^|~=ApuvU>?%qz#FHs@^`RJ+S5hp~qZ23vqhE>`&kpl7auZ0R z#>(c0V1+9Q>0yfDF@Ob;zX`EO!N76c$7Co&hCU?04@oEdt&@(ANzZS2Bgfw)Q2p9Q zxcfdNo?ls<=LcT5U;IwS^2n-K1g!5kKa%bn&N@d{8v*)x^Y=SWb*zQcU4yF~X>JG1 zXxn)HTYvkVU$pzvUE8wl+lwX-*PeH8Dj1;s#Ox{{JU1YX>Rye9$&RUqlxq6L68C3Pn@k^Vjsnf6nLNtu9BFv&6f_2p@=>8 zR%PK@^DV6f1GH>a(3VyO6p9c}(S_LY`4&d20I`w4ul5n7OEaTiQTnelsZ zjwTtI52EDGQ=RF?t}NGGZ;=k}0VWh$wn%78iv$Wq^%QvI^`ay^h^Nziqj(Sx*OTVs z#C(IVV1SlMgSI3MD3ta{fk)mRuKdZJ>4Ckth5PEsYX*7trh)-lCJ)+@JfKkC1XtvN Jq!Os!{{dC2VJH9q literal 0 HcmV?d00001 diff --git a/backend/auth/apps/permissions/urls.py b/backend/auth/apps/permissions/urls.py new file mode 100644 index 0000000..cdb9adb --- /dev/null +++ b/backend/auth/apps/permissions/urls.py @@ -0,0 +1,173 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from fastcrud import FastCRUD + +from database import get_db +from models import Permission, Role +from schemas import ( + PermissionCreate, + PermissionResponse, + RolePermissionAssign, + RolePermissionResponse, +) +from middleware import get_current_user +from models import User + +router = APIRouter(prefix="/permissions", tags=["permissions"]) + +# 创建FastCRUD实例 +permission_crud = FastCRUD(Permission, PermissionResponse) + + +@router.post("/", response_model=PermissionResponse) +async def create_permission( + permission_data: PermissionCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """创建权限""" + # 检查权限编码是否已存在 + result = await db.execute(select(Permission).filter(Permission.code == permission_data.code)) + existing_permission = result.scalar_one_or_none() + if existing_permission: + raise HTTPException(status_code=400, detail="权限编码已存在") + + # 创建权限实例 + db_permission = Permission( + name=permission_data.name, + code=permission_data.code, + description=permission_data.description, + ) + + db.add(db_permission) + await db.commit() + await db.refresh(db_permission) + + return db_permission + + +@router.get("/", response_model=list[PermissionResponse]) +async def get_permissions( + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """获取所有权限""" + result = await db.execute(select(Permission)) + permissions = result.scalars().all() + return permissions + + +@router.get("/{permission_id}", response_model=PermissionResponse) +async def get_permission( + permission_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """获取单个权限""" + permission = await permission_crud.get(db, id=permission_id) + if not permission: + raise HTTPException(status_code=404, detail="权限不存在") + return permission + + +@router.put("/{permission_id}", response_model=PermissionResponse) +async def update_permission( + permission_id: int, + permission_data: PermissionCreate, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """更新权限""" + # 检查权限编码是否被其他权限使用 + result = await db.execute( + select(Permission).filter(Permission.code == permission_data.code, Permission.id != permission_id) + ) + existing_permission = result.scalar_one_or_none() + if existing_permission: + raise HTTPException(status_code=400, detail="权限编码已存在") + + # 准备更新数据 + update_data = permission_data.model_dump() + + # 使用FastCRUD更新权限 + updated_permission = await permission_crud.update( + db, update_data, id=permission_id, schema_to_select=PermissionResponse, return_as_model=True + ) + if not updated_permission: + raise HTTPException(status_code=404, detail="权限不存在") + return updated_permission + + +@router.delete("/{permission_id}") +async def delete_permission( + permission_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """删除权限""" + # 检查权限是否存在 + result = await db.execute(select(Permission).filter(Permission.id == permission_id)) + existing_permission = result.scalar_one_or_none() + if not existing_permission: + raise HTTPException(status_code=404, detail="权限不存在") + + # 使用FastCRUD删除权限 + deleted = await permission_crud.delete(db, id=permission_id) + return {"message": "权限删除成功"} + + +# 角色权限关联接口 +@router.post("/assign", response_model=RolePermissionResponse) +async def assign_permissions_to_role( + data: RolePermissionAssign, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """为角色分配权限""" + # 查找角色 + result = await db.execute(select(Role).filter(Role.id == data.role_id)) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 查找要分配的权限 + permissions = [] + for permission_id in data.permission_ids: + result = await db.execute(select(Permission).filter(Permission.id == permission_id)) + permission = result.scalar_one_or_none() + if permission: + permissions.append(permission) + + # 更新角色的权限 + role.permissions = permissions + await db.commit() + await db.refresh(role) + + # 返回角色权限信息 + return RolePermissionResponse( + role_id=role.id, + role_name=role.name, + permissions=[PermissionResponse.from_orm(p) for p in role.permissions] + ) + + +@router.get("/role/{role_id}", response_model=RolePermissionResponse) +async def get_role_permissions( + role_id: int, + db: AsyncSession = Depends(get_db), + current_user: User = Depends(get_current_user), +): + """获取角色的权限列表""" + # 查找角色 + result = await db.execute(select(Role).filter(Role.id == role_id)) + role = result.scalar_one_or_none() + if not role: + raise HTTPException(status_code=404, detail="角色不存在") + + # 返回角色权限信息 + return RolePermissionResponse( + role_id=role.id, + role_name=role.name, + permissions=[PermissionResponse.from_orm(p) for p in role.permissions] + ) \ No newline at end of file diff --git a/backend/auth/apps/urls.py b/backend/auth/apps/urls.py index d326d7a..fd4e2ee 100644 --- a/backend/auth/apps/urls.py +++ b/backend/auth/apps/urls.py @@ -1,10 +1,12 @@ from fastapi import APIRouter from .users.urls import router as users_router from .roles.urls import router as roles_router +from .permissions.urls import router as permissions_router # 创建主路由 api_router = APIRouter() # 包含子路由 api_router.include_router(users_router) -api_router.include_router(roles_router) \ No newline at end of file +api_router.include_router(roles_router) +api_router.include_router(permissions_router) \ No newline at end of file diff --git a/backend/auth/booksystem.db b/backend/auth/booksystem.db new file mode 100644 index 0000000000000000000000000000000000000000..e67a57a7eb4ac5a022ba799859d54626072808f7 GIT binary patch literal 49152 zcmeI5U2Ggz6~||G#XFt5_bwZ(QXJfXtoy19;q%YRSGl|#U z568Q1oIH_6Q~_RE)FP4sErlw5sf%bML=&k<6?uS46sRZtq(T*_s8GZM_hY?Z z-mzUok&^sJ^0_nj+_`7Y@0?kCuJ4^aa(Fmfw$0PUxqPNx4s#Lu2WY|ZccrTb z%!cziSx*NJj-}E=qpaOpvrcm?bs#mC8XZg>;TLA^&RXjlH+EHjN=?I0G&&#jYQe)@ zvXm`Y_Co1wj{bo=k(nl5Lo3g3XP1v`TBN!~oWFfm`U=StLXUK$mQ>CyeE z6K0*(JUD9d7W^VE^|{@_U{HQygolPJxbQTDp>l$uJ5Xi%LMCsU`%_wnX+BZQpfhwoIIKyNwMnrS*ulbE>kM!if6Kg zbtg0=T_tCZjiwJDOPRaZ(5*|P%JsC)I-|4GqXi!fIwcPe*hH8Y`naYAyLZU>wM3~U zBF&cCfx9_nYASX&(Y&cu@Z`C8L3Iar?2s>gnI}(8Tia`ENTXUkyOAAoX~Q&`ELydM zXihTAE=|s5XUp_MWnFYXT_`sS7`uc{qchs31&4Mx@vnJ05w7iPUsAN-j&}LjS|lnl zW`){Je=Q#=ceX2tACTR__ICO4V)c`h>0AwzMeF~P@2ARm4z*ajm1scN{jm`}7F|m3 z3ob3Vuic3rcb$LUDfD-Vhy6bB(;qePu0*brGh{p6!3F{#00JNY0w4eaAOHd&00JNY z0(TC91F9x%OD4CyfAi|nQ{Rb%BJu7}Z+E2M48`|G`u6rldcxsIU%1avlPH}pmF@fi zMf9_}_{P%W3y!Vhj+#Ev&#+6AcG2ruzOs1xt5;W^erfsgPaV(cb=1T}Z)sVRw$t8L z7Jsnv80vTIDk{1%XiQb|mPFnn?~#uPp8xNh9LJS{00@8p2!H?xfB*=900@8p z2;8Ft3}u@v20T=@E5GqW;mo7hl`^t}M z>uC2{cFr!_^-8hdau#WCG#&~&Y9iBt23yO|zP9}BAJ^FutGz}4Lb-{pkfW{8w7
rq z`sz)r#2h_}TY7^P5q54xWP`A>g=~oRx1J=;e2H0}1}h>PxD}BHYrbqD52B8~L{GV? zmF-nxA`iF~mH`z{wvqu6M;j4KYw$z_ncEPt7gn|ud#Af6)*rUoDpth2bP;>@ii1k* z>#H}8eZu*^30Z1`6%k}^MZ{iM*-Gpaj`vKW-%=W^h}d&0BKB*(Y##eW>%{>y!xD*8 zE^39vzH-q#%-jl#eSPI7yd3YyMBI`aJP|?WHbm@&m94}+;h15G82kS}349`vU4c)? zL9&k!@-3-Fy6SsiPyW89mf}g>OOy_okJ<<;M)g?quUC&n&mO(|KH-+_99h-0kln%Xa6oEhglb5RV|H=F0Pvj5e zxAc7gx5&@Q>*O`ENY&Ut00ck)1V8`;KmY_l00ck)1V8`;{tpD$jD13w3}Nw=FxO=GgHjmg&Y|6PHcM0%;f1_B@e0w4eaAOHd&00JNY0w4ea z{{sRSQ?7v2zN@>#e{r8*P9~+YT`DhM|K{o|&xpzF?D!#jA(NlY**%lRd^nm2g@o8S zduqly9qBGjWg@Zo&PabK6xtb^K3}lXeG4b@kCh82M&_r+2Ok+ZymN4RBz`)zf3P%v z?)Z!~kR7+vhjY1}@!7LS4h=?!q7N@v*_qk1$7fEaTuo~)5_==jo;L~j1vU@>0T2KI5C8!X009sH0T2KI5CDO@fPhD7m)RhT z%cBH!K0+a$|3BvQ|L+0^;Q|PN00@8p2!H?xfB*=900@8p2!KG909y-C_x^u!O(Opy ze{eY=`c#=pmw5l*6^ZTfWZs;-al2RPy75pb@Rfb V@xtH63q|FHTj7PzMcd1|>py3sx+VYs literal 0 HcmV?d00001 diff --git a/backend/auth/database.py b/backend/auth/database.py index 14f2350..188d29c 100644 --- a/backend/auth/database.py +++ b/backend/auth/database.py @@ -23,4 +23,4 @@ async def get_db(): try: yield session finally: - await session.close() \ No newline at end of file + await session.close() diff --git a/backend/auth/init_data.py b/backend/auth/init_data.py index 2229e6a..6afc64b 100644 --- a/backend/auth/init_data.py +++ b/backend/auth/init_data.py @@ -1,9 +1,116 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select -from models import Role, User +from models import Role, User, Permission from utils.password import get_password_hash +async def init_permissions(db: AsyncSession): + """初始化权限数据""" + # 定义需要初始化的权限 + required_permissions = [ + {"name": "用户管理", "code": "user:manage", "description": "用户管理权限"}, + {"name": "用户查询", "code": "user:read", "description": "用户查询权限"}, + {"name": "用户创建", "code": "user:create", "description": "用户创建权限"}, + {"name": "用户更新", "code": "user:update", "description": "用户更新权限"}, + {"name": "用户删除", "code": "user:delete", "description": "用户删除权限"}, + {"name": "角色管理", "code": "role:manage", "description": "角色管理权限"}, + {"name": "角色查询", "code": "role:read", "description": "角色查询权限"}, + {"name": "角色创建", "code": "role:create", "description": "角色创建权限"}, + {"name": "角色更新", "code": "role:update", "description": "角色更新权限"}, + {"name": "角色删除", "code": "role:delete", "description": "角色删除权限"}, + {"name": "权限管理", "code": "permission:manage", "description": "权限管理权限"}, + {"name": "权限查询", "code": "permission:read", "description": "权限查询权限"}, + {"name": "权限创建", "code": "permission:create", "description": "权限创建权限"}, + {"name": "权限更新", "code": "permission:update", "description": "权限更新权限"}, + {"name": "权限删除", "code": "permission:delete", "description": "权限删除权限"}, + ] + + # 检查现有权限 + result = await db.execute(select(Permission)) + existing_permissions = result.scalars().all() + existing_permission_codes = {perm.code for perm in existing_permissions} + + # 检查需要创建或更新的权限 + permissions_to_add = [] + permissions_to_update = [] + + for perm_data in required_permissions: + perm_code = perm_data["code"] + if perm_code not in existing_permission_codes: + # 权限不存在,需要创建 + permissions_to_add.append(Permission(**perm_data)) + else: + # 权限存在,检查是否需要更新 + existing_perm = next( + perm for perm in existing_permissions if perm.code == perm_code + ) + if existing_perm.name != perm_data["name"] or existing_perm.description != perm_data["description"]: + existing_perm.name = perm_data["name"] + existing_perm.description = perm_data["description"] + permissions_to_update.append(existing_perm) + + # 执行创建和更新操作 + if permissions_to_add: + for perm in permissions_to_add: + db.add(perm) + await db.commit() + print(f"创建了 {len(permissions_to_add)} 个权限") + + if permissions_to_update: + await db.commit() + print(f"更新了 {len(permissions_to_update)} 个权限") + + if not permissions_to_add and not permissions_to_update: + print("权限数据已存在且无需更新,跳过初始化") + + print("权限数据初始化/更新完成") + + +async def init_role_permissions(db: AsyncSession): + """初始化角色权限分配""" + from sqlalchemy.orm import selectinload + + # 获取管理员角色(使用预加载避免懒加载问题) + result = await db.execute( + select(Role) + .filter(Role.name == "管理员") + .options(selectinload(Role.permissions)) + ) + admin_role = result.scalar_one_or_none() + if not admin_role: + print("管理员角色不存在,请先初始化角色数据") + return + + # 获取所有权限 + result = await db.execute(select(Permission)) + all_permissions = result.scalars().all() + + if not all_permissions: + print("没有找到任何权限,请先初始化权限数据") + return + + # 获取管理员角色当前的权限(使用预加载的数据) + admin_permission_codes = set() + if admin_role.permissions: + admin_permission_codes = {perm.code for perm in admin_role.permissions} + + # 检查是否需要添加权限 + permissions_to_add = [] + for perm in all_permissions: + if perm.code not in admin_permission_codes: + permissions_to_add.append(perm) + + if permissions_to_add: + # 直接设置权限(替换而非追加,确保权限完整) + admin_role.permissions = all_permissions + await db.commit() + print(f"为管理员角色分配了 {len(permissions_to_add)} 个权限") + else: + print("管理员角色权限已完整,无需更新") + + print("角色权限初始化/更新完成") + + async def init_roles(db: AsyncSession): """初始化角色数据""" # 定义需要初始化的角色 @@ -187,6 +294,8 @@ async def init_all_data(db: AsyncSession): """初始化所有数据""" print("开始初始化数据库数据...") await init_roles(db) + await init_permissions(db) + await init_role_permissions(db) await init_admin_user(db) await init_test_user(db) print("数据库数据初始化完成") diff --git a/backend/auth/middleware.py b/backend/auth/middleware.py index e91864a..fed45b6 100644 --- a/backend/auth/middleware.py +++ b/backend/auth/middleware.py @@ -3,6 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select import redis.asyncio as redis +from typing import Callable from database import get_db from utils.jwt import decode_token @@ -49,3 +50,108 @@ async def get_current_user( raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") return user + + +def has_permission(required_permission_code: str) -> Callable: + """ + 权限验证依赖函数工厂 + + 使用示例: + @router.get("/protected", dependencies=[Depends(has_permission("user:read"))]) + async def protected_route(): + ... + """ + async def permission_checker( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + ): + # 获取用户角色的所有权限 + from models import Permission, Role + + result = await db.execute( + select(Permission) + .join(Role.permissions) + .filter(Role.id == current_user.role_id) + ) + permissions = result.scalars().all() + + # 检查是否有 required_permission_code 权限 + has_perm = any(perm.code == required_permission_code for perm in permissions) + if not has_perm: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"没有访问权限,需要权限: {required_permission_code}", + ) + return current_user + + return permission_checker + + +def has_any_permission(required_permission_codes: list[str]) -> Callable: + """ + 多权限验证依赖函数工厂(满足任一权限即可) + + 使用示例: + @router.get("/protected", dependencies=[Depends(has_any_permission(["user:read", "admin:read"]))]) + async def protected_route(): + ... + """ + async def permission_checker( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + ): + from models import Permission, Role + + result = await db.execute( + select(Permission) + .join(Role.permissions) + .filter(Role.id == current_user.role_id) + ) + permissions = result.scalars().all() + user_permission_codes = {perm.code for perm in permissions} + + # 检查是否有任一需要的权限 + has_perm = any(code in user_permission_codes for code in required_permission_codes) + if not has_perm: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"没有访问权限,需要以下任一权限: {', '.join(required_permission_codes)}", + ) + return current_user + + return permission_checker + + +def has_all_permissions(required_permission_codes: list[str]) -> Callable: + """ + 多权限验证依赖函数工厂(需要满足所有权限) + + 使用示例: + @router.get("/protected", dependencies=[Depends(has_all_permissions(["user:read", "user:write"]))]) + async def protected_route(): + ... + """ + async def permission_checker( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), + ): + from models import Permission, Role + + result = await db.execute( + select(Permission) + .join(Role.permissions) + .filter(Role.id == current_user.role_id) + ) + permissions = result.scalars().all() + user_permission_codes = {perm.code for perm in permissions} + + # 检查是否满足所有需要的权限 + missing_permissions = [code for code in required_permission_codes if code not in user_permission_codes] + if missing_permissions: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"缺少必要权限: {', '.join(missing_permissions)}", + ) + return current_user + + return permission_checker diff --git a/backend/auth/models.py b/backend/auth/models.py index af3cf33..420e7e5 100644 --- a/backend/auth/models.py +++ b/backend/auth/models.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func +from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, func, Table from sqlalchemy.orm import relationship from sqlalchemy.ext.declarative import declarative_base from datetime import datetime @@ -20,6 +20,12 @@ class Role(Base): # 关联到User users = relationship("User", back_populates="role_rel") + # 关联到Permission(多对多) + permissions = relationship( + "Permission", + secondary="role_permission", + back_populates="roles" + ) class User(Base): @@ -43,3 +49,33 @@ class User(Base): # 关联到Role role_rel = relationship("Role", back_populates="users") + + +class Permission(Base): + """权限数据库模型""" + + __tablename__ = "permission" + id = Column(Integer, primary_key=True, index=True, comment="权限ID") + name = Column(String(50), nullable=False, comment="权限名称") + code = Column(String(50), nullable=False, unique=True, comment="权限编码") + description = Column(String(255), nullable=True, comment="权限描述") + createtime = Column(DateTime, default=datetime.utcnow, comment="创建时间") + updatetime = Column( + DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间" + ) + + # 关联到Role(多对多) + roles = relationship( + "Role", + secondary="role_permission", + back_populates="permissions" + ) + + +# 角色权限关联表(多对多) +role_permission = Table( + "role_permission", + Base.metadata, + Column("role_id", Integer, ForeignKey("role.id"), primary_key=True, comment="角色ID"), + Column("permission_id", Integer, ForeignKey("permission.id"), primary_key=True, comment="权限ID") +) diff --git a/backend/auth/schemas.py b/backend/auth/schemas.py index 19c40f5..36e4dba 100644 --- a/backend/auth/schemas.py +++ b/backend/auth/schemas.py @@ -106,3 +106,44 @@ class RoleResponse(RoleBase): class Config: from_attributes = True + + +# 权限相关模型 +class PermissionBase(BaseModel): + """权限基础模型""" + + name: str = Field(..., max_length=50, description="权限名称") + code: str = Field(..., max_length=50, description="权限编码") + description: Optional[str] = Field(None, max_length=255, description="权限描述") + + +class PermissionCreate(PermissionBase): + """权限创建模型""" + pass + + +class PermissionResponse(PermissionBase): + """权限响应模型""" + + id: int = Field(..., description="权限ID") + createtime: datetime = Field(..., description="创建时间") + updatetime: datetime = Field(..., description="更新时间") + + class Config: + from_attributes = True + + +# 角色权限关联相关模型 +class RolePermissionAssign(BaseModel): + """角色权限分配模型""" + + role_id: int = Field(..., description="角色ID") + permission_ids: list[int] = Field(..., description="权限ID列表") + + +class RolePermissionResponse(BaseModel): + """角色权限响应模型""" + + role_id: int = Field(..., description="角色ID") + role_name: str = Field(..., description="角色名称") + permissions: list[PermissionResponse] = Field(..., description="权限列表")