admin-solution es una plantilla de sistema de gestión de permisos.
|
|前端权限控制 --> 动态从后端请求路由
权限控制 --> |
|后端权限控制 --> 进行接口调用访问控制
|
vue-element-admin por defecto lee el token en el cuerpo
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 )
} )
} )
Luego use el token para verificar los permisos con el backend.
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 )
}
)
Ahora modifíquelo para: el backend almacena directamente el token en el encabezado de la cookie y lo establece en 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 ;
}
}
En algunos escenarios, la interfaz aún necesita verificar el token y no se puede obtener el token en la cookie, por lo que también se devuelve un token en el cuerpo del mensaje de respuesta de la interfaz de inicio de sesión, lo que indica que el inicio de sesión fue exitoso. realizado por el front-end Utilizado, no participa en el trabajo de verificación del back-end.
Las rutas de front-end se almacenan en la base de datos de back-end. Cuando el usuario inicia sesión, la ruta json se solicita desde el back-end y luego el front-end la agrega dinámicamente.
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 )
}
}
}
La función principal es importComponent(routeNodes)
, que importa componentes de forma recursiva.
Cabe señalar aquí que cuando webpack compila es6 e introduce dinámicamente import(), no puede pasar variables, pero cuando debe usar variables, puede proporcionar parte de la información a webpack a través de una plantilla de cadena, por ejemplo, import(
./path/${myFile}
), para que todos los módulos bajo ./path se compilen durante la compilación. Consulte la sintaxis import() en vue. ¿Por qué no se pueden pasar variables?
El backend usa shiro para verificar los permisos. Este es un marco muy interesante. ¿La estructura del código es clara y simple?
@ 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 ;
}
}
Actualmente, solo están configuradas las configuraciones de verificación de permisos para la interfaz /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 ;
}
}
El proceso de desarrollo del front-end implicará problemas entre dominios, por lo que el front-end y el back-end deben modificarse juntos.
Interfaz
if ( process . env . NODE_ENV === 'development' ) {
service . defaults . baseURL = 'http://localhost:9900/'
service . defaults . withCredentials = true
}
extremo posterior
@ 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 );
}
}
Esta configuración no es un problema en la solicitud de obtención, pero en la solicitud posterior, la solicitud se verificará previamente y la solicitud de OPCIONES se enviará primero, por lo que las configuraciones relevantes se realizan en ApiAccessControlFilter
en权限验证
anterior.
if (! SpringUtil . isInProduction ()
&& request instanceof HttpServletRequest
&& "OPTIONS" . equals ((( HttpServletRequest ) request ). getMethod ())) {
return true ;
}
Esta no es una buena solución. Por ahora es sólo un parche. Pensaré en una solución mejor más adelante.
User
almacena la información de inicio de sesión del usuario.Role
almacena rolesUserRoleRelation
almacena las relaciones de roles de usuario.BackendPermission
almacena los permisos de backend (todas las rutas se guardarán automáticamente en esta tabla cuando se inicie el servidor)RoleBackendPermissionRelation
almacena los permisos de backend que posee el rol.FrontendPermission
almacena información de enrutamiento de frontend.RoleFrontendPermissionRelation
almacena la información de enrutamiento de front-end propiedad del rol. Para solucionar problemas más fácilmente durante el proceso de desarrollo, cada mensaje de solicitud y mensaje de respuesta se imprimen en HttpTraceLogFilter
(puede optar por desactivarlo en el entorno de producción).
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":[]}]}]}}',}