«[Ошибка 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://локальный:3000/graphql
  • Метод: ПОСТ
  • HTTP-версия: HTTP/1.1
  • Заголовки:
    • Происхождение: http://локальный: 3000
    • Accept-Encoding: gzip, deflate, br
    • Хост: локальный: 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
    • User-Agent: 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://локальный:3000/логин
    • Cookie: авторизация = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjaWQiOjgsInN1YiI6InN0YXNAbXJzd29yZHNtaXRoLmNvbSIsImlzcyI6Imh0dHA6Ly9td3MtbWxhLmNvbSIsInBlcm1pc3Npb25zIjoxLCJpYXQiOjE1MjIwNzA4NzYsImV4cCI6MTUyMjY3NTY3Nn0.cXoy-SxSc5YVJ36lSmUoKAYU8KpZsZaFOS-xqcmbKPg
    • Соединение: Keep-alive
    • Длина контента: 179
    • ДНТ: 1

Обратите внимание, что Куки имеет авторизация = [какой-то токен]. Означает ли это, что нет заголовка авторизации, потому что по какой-то причине он живет под Куки? Если да, то как правильно настроить заголовок? Или я еще что-то делаю не так?

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.info("jwt: " + localStorage['token'])
            if (this.props.accountInfo.loading) { return <Translate component = "div" content = "loading.loading" /> }
            console.info(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.info('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.info('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.info('network error?') //TODO
                //this.refresh()
            } else { //TODO
                // If we connected to the server and refreshing failed, log out
                console.info('error, logging out')
                console.info(error)
                this.logOut()
            }
        })
    }

    componentWillMount() {
        const raw_jwt = localStorage.getItem('token')
        if (!!raw_jwt) {
            console.info('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))
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
0
1 934
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Note that Cookie has authorization=[some token]. Does this mean that there is no authorization header because it is living under Cookie for some reason? If so, how do I set the header correctly? Or is there anything else that I'm doing wrong?

Это странно, но ваш клиентский код кажется правильным; попробуйте использовать другой инструмент разработки, чтобы увидеть, что на самом деле отправляется. Слово «Носитель» тоже было опущено, что очень странно.

The fact that the error says as guest means that the role hasn't been set correctly (otherwise it would say as loggedin). I'm fairly sure this error isn't due to something on the SQL side, but rather in my JS code, but I've provided some SQL code below just in case.

Поместив токен 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.info(ctx);
    return ctx;
  });

  return forward(operation);
})

(Примечание: раньше вы сохраняли только заголовки в контексте, теперь в приведенном выше коде я просматриваю и другие свойства.)

Спасибо за указание на то, что в контексте сохранялись только заголовки (даже если это не было источником ошибки) — этот код был фактически взят из документации Apollo! Похоже, это упущение все еще существует: apollographql.com/docs/react/advanced/…

eaglebrain 01.04.2019 15:44
Ответ принят как подходящий

@Benjie прав в том, что промежуточное ПО не работает, поэтому заголовок не добавляется. Проблема в том, что apollo-boost не допускает вариант link. Вместо этого ApolloClient следует импортировать из apollo-client.

Другие вопросы по теме