React应用权限管理的一些解决方案
随着用来构建界面的 JavaScript 库越来越流行,现在也有越来越多的 web 应用使用前后端分离的方案。但是不管原始的 web 开发模式也好,前后端分离的模式也罢,绕来绕去始终都绕不开权限管理这个坎。
前后端交换用户信息的模式
由于 HTTP 协议的限制,传统的开发方式中,服务器通过放在 Cookie 中的 Session 来区分不同的用户。而前后端分离的开发方式中,前端页面和资源可能直接由 nginx 这类 HTTP 服务器程序直接代理,或是直接放在对象存储的容器中,而不通过 web 的后端应用程序。这么做的好处可以方便前后端快速迭代,也可以节约一部分服务器资源。
因此,在这种部署的模式下,前后端交换数据只使用 ajax 请求,服务器辨认用户的方式就有可能需要使用到 token。这个 token 其实可以理解为 session 的一种实现方式,由服务器生成,里面包含了用户标识、过期时间等信息。
对于前端而言,可以把这个 token 保存在 localStorage 里(如果网站涉及到跨域名的话,也可以存在 Cookie 里面)。前端在登录时(依情况而定,也可能是除此打开网页时)向后台请求一个用户令牌(token),之后只需要每次请求时在请求内带上 token 信息即可。
打开 webapp 时判断登录状态
假定 localStorage 中存在 token 字段,即视为用户已登录,如果不存在则视为未登录状态。那么,前端页面在打开时大致需要做的事情如下:
对应的代码大致如下
class App extends React.Component {
componentWillMount() {
// 如果当前用户为登录状态
if (authState) {
// 请求用户数据
requset.fetchUserData().then(res => {
// 假定res.status是ture为token有效,false为token过期
if (res.status) {
// 保存用户信息
setUserData(res.data)
} else {
// 将用户登录状态标位未登录
removeAuthState()
// 可以可以依情况跳转到登录页面
jumpToLogin()
}
})
} else {
// 当前用户为非登录状态,可以依情况跳转到登录页面
jumpToLogin()
}
}
render() {
return null
}
}
发送请求时根据登录状态进行拦截
有一些请求需要在登录情况下进行,在发送请求之前应该先判断当前是否处于登录状态,如果是则将 token 放入请求中并发送请求,否则取消请求并跳转到登录页面。请求发出后,如果遇到 token 过期,则在收到请求后抛出异常,将本地的 token 清除,并跳转到登录页面。
这个过程的流程大致是这样的:
这一部分代码可以参考上一篇文章axios 权限拦截,大致如下:
// 先预定义好一个axios实例r
const r = axios.create({
// your config
})
const CancelToken = axios.CancelToken
const source = CancelToken.source()
r.defaults.cancelToken = source.token
r.interceptors.request.use(
config => {
// 从store中获取数据
const authState = store.getState().auth
if (!!authState.loginState) {
config.headers['token'] = authState.token
} else {
// 取消请求
source.cancel('No Permission')
// 跳转到登录页面
jumpToLogin()
}
return config
},
error => Promise.reject(error)
)
r.interceptors.response.use(
res => {
// 判断是否token无效,假定这里的标志是errCode = 1
if (!res.status && res.errCode === 1) {
// 把token移除
removeToken()
// 跳到登录页面
jumpToLogin()
// 报错并将这个请求的后续操作打断
return Promise.reject('Token expired')
}
return res
},
error => Promise.reject(error)
)
React Router 页面权限拦截
在未登录的状态下,不仅需要对请求进行拦截,还需要对一些页面进行拦截,如果未登录用户请求访问一些需要登录的页面,就需要自动跳转到指定页面。这个工作当然可以再每个页面组件的constructor()
或ComponentWillMount()
状态下进行,但是为了简便可以直接将他们封装成一个组件。
具体代码如下:
const AuthRequireRouter = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
authState ? <Component {...props} /> : <Redirect to={'/login'} />
}
/>
)
这个组件的使用方法和其他 Route 组件无异。
在实际使用中,大多数系统中不可能只有一种用户身份,这就需要根据实际业务需求进行更细粒度的权限控制。在传统的开发模式中,这一部分的内容完全由后台维护。而在现在的情况下,由于我们会把所有代码都打包再推送到前端,通过一些技术手段是可以看到那些无权限访问的内容模板的,因此前后端都需要同时维护一份用户权限的表,后台需要对一些接口进行权限拦截。
在前端向后台获取用户信息的时候,需要一并将用户的权限信息获取下来。在不同用户访问不同页面之前,需要先检查用户是否有该权限,如果有则将页面渲染出来,如果没有则跳转到其他页面。在 React Ruter 中对应的代码如下:
const PrivateRouter = ({ component: Component, authName, ...rest }) => (
<Route
{...rest}
render={props =>
permissionNamesHas(authName) ? (
<Component {...props} />
) : (
<Redirect to={`/a/welcome`} />
)
}
/>
)
这个组件在使用时,只需要在 props 中指定所需要的权限名 authName 即可。
细粒度的组件权限控制
当然,不只有 Router 组件需要进行一些权限控制,还要对更小粒度的一些组件进行权限控制,例如:管理员的菜单中就会比普通用户的菜单多出更多东西。做这个最简单方法当然就是在render()
中直接if()...else...
,当然我们也可以封装成一个组件出来,对应的代码如下:
class AuthRequire extends React.Component {
render() {
const { authName, children } = this.props
return permissionNamesHas(authName) ? children : null
}
}
使用时,在 props 中指定所需要的权限,在组件内写上渲染的内容即可,例如,一个管理员菜单:
<ul>
<li>个人资料</li>
<li>退出</li>
<AuthRequire authName="manager">
<li>管理</li>
</AuthRequire>
</ul>
最后
以上大致就是一个 React 项目进行权限管理的方法,可能有很多不足之处,希望对各位有帮助。