참고: https://www.npmjs.com/package/vue-kindergarten
https://www.tabnine.com/code/javascript/classes/vue-router/RouteRecord
서버단에서 api 통신을 할 때 요청에 담긴 user의 Role등으로 권한체크를 할 수도 있지만, 화면단에서도 권한체크를 양방향으로 걸어주는게 보안상 안전하고 클라이언트에게 불필요한 화면이나 api요청을 접근할 수 없도록 할 수 있다.
api 요청에서 권한체크는 이전 포스팅인 인터셉터나 JWT에 담긴 claim의 Role 등을 추출 해 검사하면 되므로 여기서는 화면단에서 vue-kindergarten 라이브러리를 이용해 권한체크와 화면 redirct를 진행 해 보도록 하자!
먼저 위 링크를 통해 vue-kindergarten 라이브러리를 받아준다.
디렉토리는 src 디렉토리에 kindergarten 디렉토리를 따로 만들어주고 구성은 아래와 같다.
import { HeadGoverness } from 'vue-kindergarten'
import store from '@/store'
export default class RouteGoverness extends HeadGoverness {
guard (action, { next }) {
let isUser = store.getters['users/isUser']
return this.isAllowed(action) ? next() : next({
name: isUser ? 'home-main' : 'user-login'
})
}
}
RouteGoverness는 guard 메서드를 통해 특정 action(read, update 등)이 허락되지 않으면 화면을 redirect 시켜주는 역할을 한다. store에 저장된 isUser가 true 일 때만 메인화면을 보여주고 그렇지 않으면 로그인 화면으로 이동시킨다.
vue2
import { HeadGoverness } from 'vue-kindergarten'
import store from '@/store'
export default class RoutePramGoverness extends HeadGoverness {
guard (action, { from, to, next }) {
// 새로고침&url직접 접근 불가
if (to.params === process.env.VUE_APP_APEX_ROUTE_DATA_KEY) {
let isUser = store.getters['users/isUser']
return this.isAllowed(action) ? next() : next({
name: isUser ? 'home-main' : 'user-login'
})
} else {
store.dispatch('errors/populateErrors', {
detail: {
message: '올바르지 않은 접근입니다 Incorrect approach'
}
})
next({
name: to.meta.goHome ? 'home-main' : 'user-login'
})
}
}
}
vue3
import { HeadGoverness } from 'vue-kindergarten'
import { usersStore } from '@/store/users'
export default class RoutePramGoverness extends HeadGoverness {
guard (action, { to, next }) {
next()
const isUser = usersStore().isUser
this.isAllowed(action)
? next()
: next({
name: isUser ? 'home-main' : 'user-login'
})
}
}
RouteParamGoverness는 route에 param을 실어 보내 도착한 화면에서 새로고침을 불가능 하게 하거나, 사용자가 직접 url을 입력해 접근하는 것을 방지한다.
기본 세팅이 되었으면 이제 perimeter라는 vue 프로젝트에서 정의한 권한에 따라 route를 허락할지 말지를 설정 해 보자
설정에 꼭 필요한 파일은 아니지만 여러 타입을 정해놓고 검증하기 편하게 작성되었다.
governess 구현에 사용했으므로 참고만 하자
const getType = target => { return Object.prototype.toString.call(target).slice(8, -1) }
const typeUtil = {
isString (target) { return getType(target) === 'String' },
isNumber (target) { return getType(target) === 'Boolean' },
isBoolean (target) { return getType(target) === 'Boolean' },
isNull (target) { return getType(target) === 'Null' },
isUndefined (target) { return getType(target) === 'Undefined' },
isNullOrUndefined (target) { return typeUtil.isNull(target) || typeUtil.isUndefined(target) },
isObject (target) { return getType(target) === 'Object' },
isArray (target) { return getType(target) === 'Array' },
isDate (target) { return getType(target) === 'Date' },
isRegExp (target) { return getType(target) === 'RegExp' },
isFunction (target) { return getType(target) === 'Function' },
isEmptyObject (target) {
return Object.keys(target).length === 0 && target.constructor === Object
},
isEmptyArray (target) {
return this.isArray(target) && !this.isNull(target) && target.length === 0
},
isEmptyString (target) {
return typeUtil.isString(target) && target === ''
}
}
export default typeUtil
basePerimeter에서 여러 권한을 정의한다. 위에 있는 typeUtill과 저번 시간에 포스팅한 Constants.js의 상수를 활용한다.
vue2
import {
Perimeter
} from 'vue-kindergarten'
import typeUtil from '@/utils/type'
export default class BasePerimeter extends Perimeter {
isLogin () {
return !typeUtil.isNullOrUndefined(this.child.roles)
}
isTargetRole (_targetRole) {
return typeUtil.isNullOrUndefined(this.child.roles) ? false : this.child.roles.some(role => {
return role.type === _targetRole
})
}
isAdminRole () {
return this.isTargetRole('ROLE_ADMIN')
}
isUserRole () {
return this.isTargetRole('ROLE_USER')
}
isCreator (creId) {
return this.child.userId === creId
}
}
Perimeter 라이브러리를 상속받아 구현한다. isTargetRole
은 user가 가진 Role이 Constants.js에서 어떤 타입인지 리턴 해 준다.
isTargetRole
에서 받아낸 Role로 is000Role을 각각 정의해 어떤 권한을 가진 User인지 return해 주도록 한다.
import BasePerimeter from './base-perimeter'
export default new BasePerimeter({
purpose: 'admin',
can: {
read () {
return this.isAdminRole()
}
}
})
다음은 BasePerimeter를 구현한다. 각 권한마다 한 개씩 생성 해 줘야 한다. 가령 시스템 관리자 perimeter를 구현한다면 따로 작성해 준다.
purpose의 용도는 잘 모르겠으나 can절에는 어떤 행위를 할 수 있는지 기재할 수 있다. 기본은 read이다. 애초에 읽을 수 없으면 다른 작업도 불가능하니 read만 구현해도 된다.
다음은 perimeter 디렉토리에 index.js 파일을 만들고
export { default as adminPerimeter } from './admin-perimeter'
각 perimeter 마다 이름을 선언 해 준다. 관리자 perimeter는 adminPerimeter이다.
그리고 route 디렉토리의 index.js에서
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
import { usersStore } from '@/store/users'
import * as perimeters from '@/kindergarten/perimeters'
import child from '@/kindergarten/child'
import { createSandbox } from 'vue-kindergarten'
import RouteGoverness from '@/kindergarten/governesses/RouteGoverness'
import typeUtil from '@/utils/type'
import { i18n } from '@/plugins/i18n'
const { t } = i18n.global
const router = createRouter({
history: createWebHistory(),
scrollBehavior: (to) => {
if (to.hash) {
return {
selector: to.hash
}
} else {
return {
x: 0,
y: 0
}
}
},
routes
})
// title 설정하기(전체)
router.afterEach((to) => {
document.title = t(to.meta.title)
})
router.beforeEach((to, from, next) => {
let perimeter = null
let Governess = null
let action = null
to.matched.some((routeRecord) => {
if (!typeUtil.isNullOrUndefined(routeRecord.meta.perimeter)) {
perimeter = perimeters[routeRecord.meta.perimeter]
Governess = routeRecord.meta.governess || RouteGoverness
action = routeRecord.meta.perimeterAction || 'read'
}
})
if (perimeter) {
const sandbox = createSandbox(child(usersStore()), {
governess: new Governess(),
perimeters: [perimeter]
})
sandbox.guard(action, {
from,
to,
next
})
} else {
next()
}
})
export default router
route 이동 후 perimeter를 검사할 수 있도록 설정 해 준다.
routeRecord는 route시 가져오는 정보를 담고있는 것 같다. sandBox는 perimeter 정보를 가지고 있으며 이를 통해서만 perimeter를 검사할 수 있다.
child.js
export default store => store && store.getters['users/loggedInUser']
child에는 kindergarten이 관리해야 할 대상을 적어주는데 사용자인 user정보이다.
이제 설정을 모두 마쳤으니 route가 기재되어 있는 routes.js에서 꺼내다 사용 해 주기만 하면 된다.
{
path: 'register',
name: 'user-register',
component: loadView('user-register'),
meta: {
title: 'user.register',
perimeter: userPerimeter
}
},
이렇게 route의 meta 속성에 perimeter를 적어주면 작동하는데, 해당 컴포넌트의 perimeter 옵션에 배열 형식으로 ['userPerimeter'] 기재 해 줘도 된다.
위에서 작성한 RoutePramGoverness
는
// import RoutePramGoverness from '@/kindergarten/governesses/RoutePramGoverness'
이렇게 임포트 해 준 다음 원하는 route의 meta 속성에
governess: RoutePramGoverness
로 기재 해 주면 해당 route는 새로고침 불가 및 url 직접접근을 막을 수 있다.