3

问题

不需要授权的查询成功,但需要 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))
4

2 回答 2

2

请注意,Cookie 具有授权=[某个令牌]。这是否意味着没有授权标头,因为它由于某种原因位于 Cookie 下?如果是这样,我该如何正确设置标题?还是我做错了什么?

这很奇怪,但您的客户端代码似乎是正确的;尝试使用不同的开发工具来查看实际发送的内容。“承载者”这个词也被去掉了,很奇怪。

错误显示为访客这一事实意味着该角色尚未正确设置(否则它会显示为已登录)。我相当确定这个错误不是由于 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 不会尝试更改角色。但是,这似乎与您在 PostgreSQL 中生成的 JWT 不一致,所以我怀疑这个 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);
})

(注意:您之前只将标题保留在上下文中,在上面的代码中,我现在也传递了其他属性。)

于 2019-03-28T16:57:34.750 回答
1

@Benjie 是正确的,中间件无法正常工作,因此没有添加标头。问题是apollo-boost不允许link选项。ApolloClient应该从那里导入apollo-client

于 2019-04-01T13:28:13.853 回答