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 และแนะนำการนำเข้า () แบบไดนามิก คุณจะไม่สามารถส่งผ่านตัวแปรได้ แต่เมื่อคุณต้องใช้ตัวแปร คุณสามารถจัดเตรียมข้อมูลบางส่วนให้กับ 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":[]}]}]}}',}