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 )
}
)
Теперь измените его так: серверная часть напрямую сохраняет токен в заголовке файла cookie и устанавливает для него значение 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 ;
}
}
В некоторых сценариях интерфейсному интерфейсу по-прежнему необходимо выполнять проверку токена, а токен в файле cookie невозможно получить, поэтому в теле ответного сообщения интерфейса входа также возвращается токен, указывающий, что вход в систему успешен. выполняется клиентской частью. Используется, он не участвует в работе внутренней проверки.
Маршруты внешнего интерфейса хранятся во внутренней базе данных. Когда пользователь входит в систему, 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)
— рекурсивно импортирует компоненты.
Здесь следует отметить, что когда веб-пакет компилирует es6 и динамически вводит import(), вы не можете передавать переменные, но когда вам необходимо использовать переменные, вы можете предоставить часть информации в веб-пакет через строковый шаблон, например, 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":[]}]}]}}',}