-1

我正在开发一个 Spotify 克隆,它能够播放歌曲预览并显示用户的不同热门曲目和艺术家。在使用帮助 spotify-web-api-node 包进行授权后,我已经为网站制作了独立页面,但是我在连接路由器时遇到了问题,在我使用 spotify 登录后,我到达了我的个人资料页面,其中我有指向其他的链接页面,但是当我尝试转到另一个页面时,我在服务器上收到一个错误,它是一个无效的授权代码,在 Web 控制台上,程序包会抛出一个错误,即没有提供访问令牌。我已经尝试了所有可能的方法来纠正这个问题,但我无能为力。请帮帮我。相关代码以及整个 GitHub 存储库链接如下: 该项目的 Github 存储库是https://github.com/amoghkapoor/Spotify-Clone

应用程序.js

const code = new URLSearchParams(window.location.search).get("code")

const App = () => {

    return (
        <>
            {code ?
                <Router>
                    <Link to="/tracks">
                        <div style={{ marginBottom: "3rem" }}>
                            <p>Tracks</p>
                        </div>
                    </Link>
                    <Link to="/">
                        <div style={{ marginBottom: "3rem" }}>
                            <p>Home</p>
                        </div>
                    </Link>
                    <Switch>
                        <Route exact path="/">
                            <Profile code={code} />
                        </Route>
                        <Route path="/tracks">

                            <TopTracks code={code} />
                        </Route>
                    </Switch>

                </Router> : <Login />}
        </>
    )
}

TopTracks.js

const spotifyApi = new SpotifyWebApi({
    client_id: "some client id"
})

const TopTracks = ({ code }) => {
    const accessToken = useAuth(code)

    console.log(accessToken) // undefined in console

    console.log(code) // the correct code as provided by spotify 

    useEffect(() => {
        if (accessToken) {
            spotifyApi.setAccessToken(accessToken)
            return
        }

    }, [accessToken])

'useAuth' 自定义钩子

export default function useAuth(code) {
    const [accessToken, setAccessToken] = useState()
    const [refreshToken, setRefreshToken] = useState()
    const [expiresIn, setExpiresIn] = useState()

    useEffect(() => {
        axios
            .post("http://localhost:3001/login", {
                code
            })
            .then(res => {
                setAccessToken(res.data.accessToken)
                setRefreshToken(res.data.refreshToken)
                setExpiresIn(res.data.expiresIn)
                window.history.pushState({}, null, "/")
            })
            .catch((err) => {
                // window.location = "/"
                console.log("login error", err)
            })

    }, [code])
4

1 回答 1

0

您似乎没有在任何地方保留您的访问/刷新令牌。一旦组件被卸载,数据就会被丢弃。此外,登录代码只能使用一次。如果您多次使用它,任何符合 OAuth 的服务都会使与该代码相关的所有令牌失效。

localStorage您可以使用或IndexedDB其他数据库机制来持久化这些令牌。

出于示例的目的(即使用比这更安全和永久的东西),我将使用localStorage.

为了帮助管理多个视图和组件的状态,您应该使用 React Context。这使您可以将组件树中的通用逻辑提升到更高的位置,以便可以重用它。

此外,不要使用setInterval定期刷新令牌,您应该只按需执行刷新操作 - 即在它过期时刷新它。

// SpotifyAuthContext.js

import SpotifyWebApi from 'spotify-web-api-node';

const spotifyApi = new SpotifyWebApi({
  clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});

export const SpotifyAuthContext = React.createContext({
    exchangeCode: () => throw new Error("context not loaded"),
    refreshAccessToken: () => throw new Error("context not loaded"),
    get hasToken: spotifyApi.getAccessToken() !== undefined,
    api: spotifyApi
});

export const useSpotify = () => useContext(SpotifyAuthContext);

function setStoredJSON(id, obj) {
    localStorage.setItem(id, JSON.stringify(obj));
}

function getStoredJSON(id, fallbackValue = null) {
    const storedValue = localStorage.getItem(id);
    return storedValue === null
        ? fallbackValue
        : JSON.parse(storedValue);
}

export function SpotifyAuthContextProvider({children}) {
    const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))

    const hasToken = tokenInfo !== null

    useEffect(() => {
        if (tokenInfo === null) return; // do nothing, no tokens available

        // attach tokens to `SpotifyWebApi` instance
        spotifyApi.setCredentials({
            accessToken: tokenInfo.accessToken,
            refreshToken: tokenInfo.refreshToken,
        })

        // persist tokens
        setStoredJSON('myApp:spotify', tokenInfo)
    }, [tokenInfo])

    function exchangeCode(code) {
        return axios
            .post("http://localhost:3001/login", {
                code
            })
            .then(res => {
                // TODO: Confirm whether response contains `accessToken` or `access_token`
                const { accessToken, refreshToken, expiresIn } = res.data;
                // store expiry time instead of expires in
                setTokenInfo({
                    accessToken,
                    refreshToken,
                    expiresAt: Date.now() + (expiresIn * 1000)
                });
            })
    }

    function refreshAccessToken() {
        return axios
            .post("http://localhost:3001/refresh", {
                refreshToken
            })
            .then(res => {
                const refreshedTokenInfo = {
                     accessToken: res.data.accessToken,
                     // some refreshes may include a new refresh token!
                     refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
                     // store expiry time instead of expires in
                     expiresAt: Date.now() + (res.data.expiresIn * 1000)
                }

                setTokenInfo(refreshedTokenInfo)

                // attach tokens to `SpotifyWebApi` instance
                spotifyApi.setCredentials({
                    accessToken: refreshedTokenInfo.accessToken,
                    refreshToken: refreshedTokenInfo.refreshToken,
                })

                return refreshedTokenInfo
            })
    }

    async function refreshableCall(callApiFunc) {
         if (Date.now() > tokenInfo.expiresAt)
             await refreshAccessToken();

         try {
             return await callApiFunc()
         } catch (err) {
             if (err.name !== "WebapiAuthenticationError")
                 throw err; // rethrow irrelevant errors
         }

         // if here, has an authentication error, try refreshing now
         return refreshAccessToken()
             .then(callApiFunc)
    }

    return (
        <SpotifyAuthContext.Provider value={{
            api: spotifyApi,
            exchangeCode,
            hasToken,
            refreshableCall,
            refreshAccessToken
        }}>
            {children}
        </SpotifyAuthContext.Provider>
    )
}

用法:

// TopTracks.js
import useSpotify from '...'

const TopTracks = () => {
    const { api, refreshableCall } = useSpotify()
    const [ tracks, setTracks ] = useState([])
    const [ error, setError ] = useState(null)

    useEffect(() => {
        let disposed = false
        refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
            .then((res) => {
                if (disposed) return
                setTracks(res.body.items)
                setError(null)
            })
            .catch((err) => {
                if (disposed) return
                setTracks([])
                setError(err)
            });

        return () => disposed = true
    });

    if (error != null) {
       return <span class="error">{error.message}</span>
    }

    if (tracks.length === 0) {
       return <span class="warning">No tracks found.</span>
    }

    return (<ul>
       {tracks.map((track) => {
           const artists = track.artists
               .map(artist => artist.name)
               .join(', ')

           return (
               <li key={track.id}>
                   <a href={track.preview_url}>
                       {track.name} - {artists}
                   </a>
               </li>
           )
       }
    </ul>)
}
// Login.js
import useSpotify from '...'

const Login = () => {
    const { exchangeCode } = useSpotify()
    const [ error, setError ] = useState(null)

    const code = new URLSearchParams(window.location.search).get("code")

    useEffect(() => {
       if (!code) return // no code. do nothing.

       // if here, code available for login
       
       let disposed = false
       exchangeCode(code)
           .then(() => {
               if (disposed) return
               setError(null)
               window.history.pushState({}, null, "/")
           })
           .catch(error => {
               if (disposed) return
               console.error(error)
               setError(error)
           })

        return () => disposed = true
    }, [code])

    if (error !== null) {
        return <span class="error">{error.message}</span>
    }

    if (code) {
        // TODO: Render progress bar/spinner/throbber for "Signing in..."
        return /* ... */
    }

    // if here, no code & no error. Show login button
    // TODO: Render login button
    return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'

const MyRouter = () => {
    const { hasToken } = useSpotify()

    if (!hasToken) {
        // No access token available, show login screen
        return <Login />
    }
   
    // Access token available, show main content
    return (
        <Router>
            // ...
        </Router>
    )
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'

const App = () => {
    return (
        <SpotifyAuthContextProvider>
             <MyRouter />
        </SpotifyAuthContextProvider>
    );
}
于 2021-08-21T12:45:57.553 回答