admin-solution هو قالب نظام إدارة الأذونات.
|
|前端权限控制 --> 动态从后端请求路由
权限控制 --> |
|后端权限控制 --> 进行接口调用访问控制
|
يقوم vue-element-admin افتراضيًا بقراءة الرمز المميز في النص
login ( { commit } , userInfo ) {
const { username , password } = userInfo
return new Promise ( ( resolve , reject ) => {
login ( { username : username . trim ( ) , password : password } ) . then ( response => {
const { data } = response
commit ( 'SET_TOKEN' , data . token )
setToken ( data . token )
resolve ( )
} ) . catch ( error => {
reject ( error )
} )
} )
ثم استخدم الرمز المميز للتحقق من الأذونات باستخدام الواجهة الخلفية
service . interceptors . request . use (
config => {
// do something before request is sent
return config
} ,
error => {
// do something with request error
console . log ( error ) // for debug
return Promise . reject ( error )
}
)
قم الآن بتعديله إلى: تقوم الواجهة الخلفية بتخزين الرمز المميز مباشرة في رأس ملف تعريف الارتباط وتعيينه على httponly
@ Configuration
public class ShiroConfig {
// ... 其他设置
public SimpleCookie buildCookie () {
SimpleCookie simpleCookie = new SimpleCookie ( TOKEN_NAME );
simpleCookie . setPath ( "/" );
// 对服务器生成的TOKEN设置 HttpOnly 属性. 前端无法读写该TOKEN, 提供系统安全, 防止XSS攻击
simpleCookie . setHttpOnly ( true );
// 设置浏览器关闭时失效此Cookie
simpleCookie . setMaxAge (- 1 );
return simpleCookie ;
}
}
لا تزال الواجهة الأمامية بحاجة إلى التحقق من الرمز المميز في بعض السيناريوهات، ولا يمكن الحصول على الرمز المميز في ملف تعريف الارتباط، لذلك يتم إرجاع الرمز المميز أيضًا في نص رسالة الاستجابة لواجهة تسجيل الدخول، مما يشير إلى أن تسجيل الدخول ناجح فقط يتم ذلك بواسطة الواجهة الأمامية المستخدمة، ولا يشارك في أعمال التحقق الخلفية.
يتم تخزين مسارات الواجهة الأمامية في قاعدة البيانات الخلفية عندما يقوم المستخدم بتسجيل الدخول، يُطلب المسار json من الواجهة الخلفية، ثم تضيفه الواجهة الأمامية ديناميكيًا.
const actions = {
generateRoutes ( { commit } , roles ) {
return new Promise ( resolve => {
getUserFrontendPermissions ( ) . then ( response => {
let routeNodes = response . data . routeNodes
importComponent ( routeNodes )
commit ( 'SET_ROUTES' , routeNodes )
resolve ( routeNodes )
} )
} )
}
}
function importComponent ( routeNodes ) {
for ( var rn of routeNodes ) {
if ( rn . component == "Layout" ) {
rn . component = Layout
} else {
let componentPath = rn . component
rn . component = ( ) => import ( `@/views/ ${ componentPath } ` )
}
if ( rn . children && rn . children . length > 0 ) {
importComponent ( rn . children )
}
}
}
الوظيفة الرئيسية هي importComponent(routeNodes)
التي تستورد المكونات بشكل متكرر.
تجدر الإشارة هنا إلى أنه عندما يقوم webpack بتجميع es6 ويقدم import() ديناميكيًا، لا يمكنك تمرير المتغيرات، ولكن عندما يتعين عليك استخدام المتغيرات، يمكنك توفير جزء من المعلومات إلى webpack من خلال قالب سلسلة، على سبيل المثال، import(
./path/${myFile}
)، بحيث يتم تجميع كافة الوحدات الموجودة ضمن ./path أثناء التجميع، يرجى الرجوع إلى بناء جملة import() في vue.
تستخدم الواجهة الخلفية shiro للتحقق من الأذونات. هل هيكل الكود واضح وبسيط؟
@ Configuration
public class ShiroConfig {
/**
* 设置接口权限验证, 目前只针对api接口进行权限验证
*
* @param securityManager
* @return
*/
@ Bean ( name = "shiroFilter" )
public ShiroFilterFactoryBean shiroFilter ( SecurityManager securityManager ) {
LOGGER . info ( "start shiroFilter setting" );
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean ();
shiroFilterFactoryBean . setSecurityManager ( securityManager );
shiroFilterFactoryBean . setLoginUrl ( "/" );
shiroFilterFactoryBean . setSuccessUrl ( "/#/dashboard" );
shiroFilterFactoryBean . setUnauthorizedUrl ( "/403" );
Map < String , String > filterChainDefinitionMap = new LinkedHashMap <>();
Map < String , Filter > filtersMap = new LinkedHashMap <>();
filtersMap . put ( "apiAccessControlFilter" , new ApiAccessControlFilter ());
shiroFilterFactoryBean . setFilters ( filtersMap );
filterChainDefinitionMap . put ( "/static/**" , "anon" );
filterChainDefinitionMap . put ( "/#/login/**" , "anon" );
filterChainDefinitionMap . put ( "/api/user/auth/login" , "anon" );
filterChainDefinitionMap . put ( "/logout" , "logout" );
filterChainDefinitionMap . put ( "/api/**" , "apiAccessControlFilter" );
filterChainDefinitionMap . put ( "/**" , "logFilter" );
filterChainDefinitionMap . put ( "/**" , "authc" );
shiroFilterFactoryBean . setFilterChainDefinitionMap ( filterChainDefinitionMap );
LOGGER . info ( "shirFilter config fineshed" );
return shiroFilterFactoryBean ;
}
}
حاليًا، يتم تعيين إعدادات التحقق من الأذونات فقط للواجهة /api/**
.
public class ApiAccessControlFilter extends AccessControlFilter {
private static final Logger LOGGER = LoggerFactory . getSystemLogger ( ApiAccessControlFilter . class );
@ Override
protected boolean isAccessAllowed ( ServletRequest request , ServletResponse response , Object mappedValue ) throws Exception {
// 开发环境中, 如果是OPTIONS预检请求则直接返回true TODO 这里想办法做的更加优雅些, 目前就是个补丁
if (! SpringUtil . isInProduction ()
&& request instanceof HttpServletRequest
&& "OPTIONS" . equals ((( HttpServletRequest ) request ). getMethod ())) {
return true ;
}
HttpServletRequest httpServletRequest = ( HttpServletRequest ) request ;
Subject subject = SecurityUtils . getSubject ();
boolean isAuthenticated = subject . isAuthenticated ();
boolean isPermitted = subject . isPermitted ( httpServletRequest . getRequestURI ());
LOGGER . info ( "鉴权完成, isPermitted:{}, isAuthenticated:{}" , isPermitted , isAuthenticated );
return isPermitted && isAuthenticated ;
}
private void trySetUserLog () {
LoggerLocalCache . INSTANCE . setUser ( UserUtil . getCurrentUserName ());
}
@ Override
protected boolean onAccessDenied ( ServletRequest request , ServletResponse servletResponse ) throws Exception {
LOGGER . info ( "访问被拒绝" );
Response response = ResponseCode . AUTH_FAIL . build ();
String result = JSON . toJSONString ( response );
servletResponse . getOutputStream (). write ( result . getBytes ( "UTF8" ));
servletResponse . flushBuffer ();
return false ;
}
}
ستتضمن عملية تطوير الواجهة الأمامية مشكلات عبر المجالات، لذا يجب تعديل الواجهة الأمامية والخلفية معًا.
الواجهة الأمامية
if ( process . env . NODE_ENV === 'development' ) {
service . defaults . baseURL = 'http://localhost:9900/'
service . defaults . withCredentials = true
}
نهاية الطريق
@ Configuration
public class ShiroConfig {
@ Bean
public CorsFilter corsFilter () {
// CORS配置信息
CorsConfiguration config = new CorsConfiguration ();
if (! SpringUtil . isInProduction ( applicationContext )) {
LOGGER . info ( "进行非生产模式CORS配置" );
config . addAllowedOrigin ( "*" );
config . setAllowCredentials ( true );
config . addAllowedMethod ( "*" );
config . addAllowedHeader ( "*" );
config . addExposedHeader ( "Set-Cookie" );
}
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource ();
configSource . registerCorsConfiguration ( "/**" , config );
return new CorsFilter ( configSource );
}
}
لا يمثل هذا الإعداد مشكلة في طلب الحصول، ولكن في طلب النشر، سيتم فحص الطلب مسبقًا وسيتم إرسال طلب OPTIONS أولاً، لذلك يتم إجراء الإعدادات ذات الصلة في ApiAccessControlFilter
في权限验证
أعلاه.
if (! SpringUtil . isInProduction ()
&& request instanceof HttpServletRequest
&& "OPTIONS" . equals ((( HttpServletRequest ) request ). getMethod ())) {
return true ;
}
هذا ليس حلاً جيدًا، إنه مجرد تصحيح في الوقت الحالي، وسأفكر في حل أفضل لاحقًا.
User
بتخزين معلومات تسجيل دخول المستخدمRole
يخزن الأدوارUserRoleRelation
بتخزين علاقات دور المستخدمBackendPermission
بتخزين أذونات الواجهة الخلفية (سيتم حفظ جميع المسارات تلقائيًا في هذا الجدول عند بدء تشغيل الخادم)RoleBackendPermissionRelation
بتخزين أذونات الواجهة الخلفية التي يملكها الدور.FrontendPermission
بتخزين معلومات توجيه الواجهة الأمامية.RoleFrontendPermissionRelation
بتخزين معلومات توجيه الواجهة الأمامية التي يملكها الدور. من أجل استكشاف المشكلات وإصلاحها بسهولة أكبر أثناء عملية التطوير، تتم طباعة كل رسالة طلب ورسالة استجابة في HttpTraceLogFilter
(يمكنك اختيار إيقاف تشغيله في بيئة الإنتاج)
2020-04-02 11:37:51.963 [http-nio-9900-exec-2] INFO c.w.a.config.HttpTraceLogFilter(162) - [admin, /api/user/authority/getUserFrontendPermissions] Http 请求日志: HttpTraceLog{
method='GET',
path='/api/user/authority/getUserFrontendPermissions',
parameterMap='{}',
timeTaken=17,
time='2020-04-02T11:37:51.962',
status=200,
requestHeaders='
host: localhost:9900
connection: keep-alive
accept: application/json, text/plain, */*
sec-fetch-dest: empty
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36
dnt: 1
sec-fetch-site: same-origin
sec-fetch-mode: cors
referer: http://localhost:9900/
accept-encoding: gzip, deflate, br
accept-language: zh-CN,zh;q=0.9,en;q=0.8
cookie: TTOKEN=0bbfbdca-40e5-4ee3-b19e-37d9b114cb47; frontend-token=undefined',
requestBody='',
responseHeaders='
Vary: Origin
Vary: Origin
Vary: Origin',
responseBody='{"code":20000,"message":"成功","data":{"routeNodes":[{"name":"权限配置","path":"/permission","component":"Layout","redirect":"/permission/frontend","meta":{"title":"权限配置","icon":"example"},"children":[{"name":"前端权限","path":"frontend","component":"permission/frontend/index","redirect":null,"meta":{"title":"前端权限","icon":"table"},"children":[]},{"name":"后端权限","path":"backend","component":"permission/backend/index","redirect":null,"meta":{"title":"后端权限","icon":"table"},"children":[]}]}]}}',}