问题
不需要授权的查询成功,但需要 JWT 授权的查询失败。
错误
在浏览器控制台中,我收到以下错误消息:
[GraphQL error]: Message: permission denied for function get_account_info, Location: [object Object], Path: getAccountInfo
这是我在服务器控制台中遇到的错误:
1 error(s) as guest in 101.18ms :: { getAccountInfo { username interface native customNative tutorial email __typename } }
错误说的事实as guest
意味着角色没有正确设置(否则会说as loggedin
)。我相当确定这个错误不是由于 SQL 方面的问题,而是在我的 JS 代码中,但我在下面提供了一些 SQL 代码以防万一。
请求
我安装了 GraphQL 开发人员工具,发现这是请求中发送的内容:
要求
- 请求网址:http://localhost:3000/graphql
- 方法:POST
- HTTP版本:HTTP/1.1
- 标题:
- 来源:http://localhost:3000
- 接受编码:gzip、deflate、br
- 主机:本地主机:3000
- 接受语言:pl-PL,pl;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6,lt;q=0.5,es;q=0.4
- 用户代理:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
- 内容类型:应用程序/json
- 接受:/
- 引用者:http://localhost:3000/login
- Cookie: authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjaWQiOjgsInN1YiI6InN0YXNAbXJzd29yZHNtaXRoLmNvbSIsImlzcyI6Imh0dHA6Ly9td3MtbWxhLmNvbSIsInBlcm1pc3Npb25zIjoxLCJpYXQiOjE1MjIwNzA4NzYsImV4cCI6MTUyMjY3NTY3Nn0.cXoy-SxSc5YVJ36lSmUoKAYU8KpZsZaFOS-xqcmbKPg
- 连接:保持活动
- 内容长度:179
- DNT: 1
请注意,Cookie具有authorization=[some token]。 这是否意味着没有授权标头,因为它由于某种原因位于Cookie下?如果是这样,我该如何正确设置标题?还是我做错了什么?
SQL 代码
我很确定 SQL 没问题,但这里只是以防万一。
智威汤逊生成
CREATE FUNCTION private.generate_jwt_for_user(username text)
RETURNS json_web_token
LANGUAGE plpgsql
STABLE
AS $$
DECLARE
n_moderator bigint;
BEGIN
SELECT count(*) INTO n_moderator
FROM private.moderator
WHERE account = username;
IF n_moderator = 0
THEN
RETURN ('loggedin', username)::json_web_token; -- x::Y means cast x to type Y
ELSE
RETURN ('moderator', username)::json_web_token;
END IF;
END;
$$;
获取帐户信息
CREATE FUNCTION public.get_account_info()
RETURNS private.account_info
LANGUAGE SQL
SECURITY DEFINER
STABLE
AS $$
SELECT *
FROM private.account_info
WHERE username = current_setting('jwt.claims.username')
$$;
JavaScript 代码
main.js
// Meteor startup script. Runs reactRoutes, and puts the result in the 'content' div in index.html.
import { Meteor } from 'meteor/meteor'
import { render } from 'react-dom'
import Routes from './routes'
import React from 'react'
import ApolloClient from 'apollo-boost'
import { HttpLink } from 'apollo-link-http'
import { ApolloLink, from } from 'apollo-link'
import { ApolloProvider } from 'react-apollo'
// Connect to the database using Apollo
// Add middleware that adds a Json Web Token (JWT) to the request header
const httpLink = new HttpLink({ uri: '/graphql' });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem('token')
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: 'Bearer ' + token || null,
}
}));
return forward(operation);
})
const client = new ApolloClient({
link: from([
authMiddleware,
httpLink
]),
});
// <ApolloProvider> allows React to connect to Apollo
// <Routes> allows client-side routing
// The rendered page inserted into the HTML under 'content'
Meteor.startup(() => {
render(
<ApolloProvider client={client}>
<Routes/>
</ApolloProvider>,
document.getElementById('content'))
})
应用程序.js
对带有偶尔 TODO 注释的长代码表示歉意,这仍在进行中。
import React from 'react'
import jwtDecode from 'jwt-decode'
import { withApollo, graphql } from 'react-apollo'
import gql from 'graphql-tag'
import Nav from './auxiliary/nav'
import Translate from 'react-translate-component'
class UserAppBody extends React.Component {
constructor(props) {
super(props)
this.state = {
activeLanguageId: null
}
}
setLanguage(langId) {
this.setState({
activeLanguageId: langId
})
}
render() {
let native = null
let username = false
// TODO: remove all userId references in app
let tutorial = false
if (this.props.accountInfo) {
console.log("jwt: " + localStorage['token'])
if (this.props.accountInfo.loading) { return <Translate component="div" content="loading.loading" /> }
console.log(this.props.accountInfo)
username = this.props.accountInfo.getAccountInfo.username
tutorial = this.props.accountInfo.getAccountInfo.tutorial
native = this.props.accountInfo.getAccountInfo.native
}
return (
<div id="app-container">
<Nav callbackLogOut={this.props.logOut} username={username} />
{/* Insert the children according to routes.jsx (this.props.children), along with the childrens' props.
username should come from query due to being wrapped by graphql for wrapped case; otherwise username is bool: false. */}
{React.cloneElement(
this.props.children,
{
username: username,
hasSeenTutorial: tutorial,
native: native,
activeLanguageId: this.state.activeLanguageId,
callbackLanguage: this.setLanguage.bind(this),
callbackUser: this.props.setUser,
callbackLogOut: this.props.logOut
}
)}
</div>
)
}
}
// UserAppBody will be wrapped in AppBody if user is logged in, this setup comes before the wrapping
// Calling graphql on this turns it into a function which returns a React element (needed below)
const accountInfoQuery = gql`query{
getAccountInfo {
username
interface
native
customNative
tutorial
email
}
}`
const accountInfoQueryConfig = {
name: 'accountInfo'
}
const SignedInAppBody = graphql(accountInfoQuery, accountInfoQueryConfig)(UserAppBody)
class AppBody extends React.Component {
constructor(props) {
super(props)
const raw_jwt = localStorage.getItem('token')
this.state = {
isLoggedIn: !!raw_jwt // true if there is a jwt in local storage, false otherwise
}
}
setUser(raw_jwt) {
const jwt = jwtDecode(raw_jwt)
// Check if the token has expired
// Note that getTime() is in milliseconds, but jwt.exp is in seconds
const timestamp = (new Date).getTime()
if (!!jwt && timestamp < jwt.exp * 1000) {
// If the token is still valid:
// Store the token in memory, to be added to request headers
localStorage.setItem('token', raw_jwt)
// Set the state, to change the app
this.setState({
isLoggedIn: true
})
// Automatically refresh the token
this.refreshTimer = setInterval(this.refresh, 1000*60*20) // Refresh every 20 minutes
console.log('timer set up')
} else {
// If the token is no longer valid, log out to clear information
this.logOut()
}
}
logOut() {
// Clear everything from setUser (state, memory, refreshing)
localStorage.removeItem('refreshToken')
localStorage.removeItem('token')
clearInterval(this.refreshTimer)
console.log('logging out')
// second argument is a callback that setState will call when it is finished
this.setState( { isLoggedIn: false }, this.props.client.resetStore() )
}
refresh() {
// Get a new token using the refresh code
this.props.refresh({variables: {input: {refreshToken: localStorage.getItem('refreshToken')}}})
.then((response) => {
// Store the new token
const raw_jwt = response.data.refresh.jsonWebToken
localStorage.setItem('token', raw_jwt)
}).catch((error) => {
// If we can't connect to the server, try again
if (error.networkError) {
console.log('network error?') //TODO
//this.refresh()
} else { //TODO
// If we connected to the server and refreshing failed, log out
console.log('error, logging out')
console.log(error)
this.logOut()
}
})
}
componentWillMount() {
const raw_jwt = localStorage.getItem('token')
if (!!raw_jwt) {
console.log('found json web token, running setUser as App compenent mounts')
this.setUser(raw_jwt)
this.refresh()
}
}
render() {
let AppBodyClass
if (this.state.isLoggedIn) {
AppBodyClass = SignedInAppBody
} else {
AppBodyClass = UserAppBody
}
return <AppBodyClass children={this.props.children} setUser={this.setUser.bind(this)} logOut={this.logOut.bind(this)} />
}
}
const refresh = gql`mutation($input:RefreshInput!) {
refresh(input:$input) {
jsonWebToken
}
}`
const refreshConfig = {
name: 'refresh'
}
export default withApollo(graphql(refresh, refreshConfig)(AppBody))