"[Ошибка GraphQL]: сообщение: для функции запрещено доступ" с использованием JWT в заголовках
Эта проблема
Запросы, не требующие авторизации, выполняются успешно, но запрос, требующий авторизации 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 Developer Tools и увидел, что именно это было отправлено в запросе:
Запрос
- URL запроса: http://localhost:3000/graphql
- Метод: ПОСТ
- Версия HTTP: HTTP/1.1
- Заголовки:
- Происхождение: http://localhost:3000/
- Accept-Encoding: gzip, deflate, br
- Хост: localhost: 3000
- Accept-Language: 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, как Gecko) Chrome/73.0.3683.86 Safari/537.36
- Тип контента: приложение / JSON
- принять: /
- Реферер: http://localhost:3000/login
- Cookie: разрешение =eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjaWQiOjgsInN1YiI6InN0YXNAbXJzd29yZHNtaXRoLmNvbSIsImlzcyI6Imh0dHA6Ly9td3MtbWxhLmNvbSIsInBlcm1pc3Npb25zIjoxLCJpYXQiOjE1MjIwNzA4NzYsImV4cCI6MTUyMjY3NTY3Nn0.cXoy-SxSc5YVJ36lSmUoKAYU8KpZsZaFOS-xqcmbKPg
- Подключение: keep-alive
- Длина контента: 179
- DNT: 1
Обратите внимание, что Cookie имеет авторизацию =[некоторый токен].Означает ли это, что заголовок авторизации отсутствует, потому что он по какой-то причине живет под Cookie? Если да, то как правильно установить заголовок? Или есть что-то еще, что я делаю не так?
Код SQL
Я вполне уверен, что SQL в порядке, но здесь это на всякий случай.
Поколение JWT
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;
$$;
get_account_info
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'))
})
app.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))
2 ответа
@Benjie прав, что промежуточное ПО не работает и поэтому заголовок не добавляется. Проблема в том, что apollo-boost
не позволяет link
вариант. ApolloClient
должны быть импортированы из apollo-client
вместо.
Обратите внимание, что Cookie имеет авторизацию =[некоторый токен]. Означает ли это, что заголовок авторизации отсутствует, потому что он по какой-то причине живет под Cookie? Если да, то как правильно установить заголовок? Или есть что-то еще, что я делаю не так?
Это странно, но код вашего клиента кажется правильным; попробуйте использовать другой инструмент разработки, чтобы увидеть, что на самом деле отправляется. Слово "Носитель" тоже было отброшено, очень странно.
Тот факт, что в сообщении об ошибке говорится как гость, означает, что роль не была установлена правильно (в противном случае она будет указана как loggedin). Я вполне уверен, что эта ошибка не из-за чего-то на стороне SQL, а скорее в моем коде JS, но на всякий случай я предоставил код SQL ниже.
Поместив токен JWT в инструмент jwt.io, я вижу, что тело токена:
{
"cid": 8,
"sub": "s[AN EMAIL ADDRESS]m",
"iss": "http://mws-mla.com",
"permissions": 1,
"iat": 1522070876,
"exp": 1522675676
}
Здесь отсутствует утверждение о роли, поэтому PostGraphile не будет пытаться менять роли. Однако это не соответствует JWT, который вы генерируете в PostgreSQL, поэтому я подозреваю, что этот файл cookie вводит в заблуждение. Я считаю, что вы вообще не отправляете заголовок авторизации.
Попробуйте отладить ваше промежуточное ПО аутентификации:
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem('token')
operation.setContext(context => {
const ctx = {
...context,
headers: {
...context.headers,
authorization: 'Bearer ' + token || null,
}
};
console.log(ctx);
return ctx;
});
return forward(operation);
})
(Примечание: ранее вы только держали заголовки в контексте, в приведенном выше коде я теперь также пропускаю и другие свойства.)