You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React上传图片至Firebase Storage权限问题解决方案咨询

问题:Firebase Storage上传头像权限错误

上传头像时持续收到错误:

FirebaseError: Firebase Storage: User does not have permission to access 'files/hhh.png'. (storage/unauthorized)

不想放宽Storage安全规则允许所有用户访问,希望通过修改代码解决。


当前Firebase Storage安全规则

// Grants a user access to a node matching their user ID
service firebase.storage {
  match /b/{bucket}/o {
    // Files look like: "user/<UID>/path/to/file.txt"
    match /user/{userId}/{allPaths=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Firebase配置文件

import firebase from 'firebase/compat/app';
import 'firebase/compat/auth';
import 'firebase/compat/firestore';
import { getStorage } from "firebase/storage";

const firebaseConfig = {
   
  };

const firebaseApp = firebase.initializeApp(firebaseConfig);
const db = firebaseApp.firestore();
const auth = firebase.auth();
export const storage = getStorage(firebaseApp);

export {db, auth};

Login组件代码

import React, { useState } from "react";
import { auth } from "./firebase";
import { useDispatch } from "react-redux";
import { login } from "./features/userSlice";
import hpic from "./images/screenshot.png";

import { storage } from './firebase';
import { ref, getDownloadURL, uploadBytesResumable } from "firebase/storage";

function Login() {
  
  const [progresspercent, setProgresspercent] = useState(0);
  const [name, setName] = useState("");
  const [profpic, setProfpic] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const dispatch = useDispatch();

  

  const loginToApp = (e) => {
    e.preventDefault();
    const file = e.target[1]?.files[0]

    if (!file) return;

    const storageRef = ref(storage, `files/${file.name}`);
    const uploadTask = uploadBytesResumable(storageRef, file);

    uploadTask.on("state_changed",
      (snapshot) => {
        const progress =
          Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
        setProgresspercent(progress);
      },
      (error) => {
        alert(error);
      },
      () => {
        getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
          setProfpic(downloadURL)
        });
      }
    );

    auth
      .signInWithEmailAndPassword(email, password)
      .then((userAuth) => {
        dispatch(
          login({
            email: userAuth.user.email,
            uid: userAuth.user.uid,
            displayName: userAuth.user.displayName,
            photoUrl: userAuth.user.photoURL,
          })
        );
      })
      .catch((error) => alert(error));
  };

  const register = () => {
    if (!name) {
      return alert("Please enter your name!");
    }
  
    const storageRef = ref(storage, `files/${profpic.name}`);
    const uploadTask = uploadBytesResumable(storageRef, profpic);
  
    uploadTask.on("state_changed",
      (snapshot) => {
        const progress =
          Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
        setProgresspercent(progress);
      },
      (error) => {
        alert(error);
      },
      () => {
        getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
          auth
            .createUserWithEmailAndPassword(email, password)
            .then((userAuth) => {
              userAuth.user
                .updateProfile({
                  displayName: name,
                  photoUrl: downloadURL,
                })
                .then(() => {
                  dispatch(
                    login({
                      email: userAuth.user.email,
                      uid: userAuth.user.uid,
                      displayName: name,
                      photoUrl: downloadURL,
                    })
                  );
                });
            })
            .catch((error) => alert(error));
        });
      }
    );
  };
  

  return (
    <div className="grid grid-cols-1  bg-cover bg-center bg-fixed bg-opacity-80 bg-gradient-to-r from-blue-100 to-green-100 md:grid-cols-2 gap-8 mx-auto py-20 px-6 md:px-32 items-center">
      <form className="flex flex-col items-center w-full">
        

        <input
          className="w-[60%]  h-12 px-3 rounded mb-3"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Full name (required if registering)"
        />
        <input
          className="w-[60%] h-12 px-3 rounded mb-3 "
          type="file"
          onChange={(event) => {
            setProfpic(event.target.files[0]);
          }}
          placeholder="Optional Profile Pic (Paste URL Here)"
        />
        
        <input
          className="w-[60%]  h-12 px-3 rounded mb-3  "
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
        />
        <input
          className="w-[60%]  h-12 px-3 rounded mb-3  "
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
        />
        <button
          className="w-[60%]  h-12 bg-green-500 rounded text-white font-bold mb-3 hover:bg-green-500 transition-all duration-200 hover:shadow-xl"
          type="submit"
          onClick={loginToApp}
        >
          Sign In
        </button>
        <p className="bg-white rounded p-2 text-center transition-all duration-200">
          Not a member? Fill out the information above and Click:
          <span
            className="text-blue-500 hover:text-blue-900 pl-2 font-bold cursor-pointer"
            onClick={register}
          >
            Register Now
          </span>
        </p>
      </form>
      <div className="hidden md:block">
        <img
          className="w-full h-auto rounded-lg  border-2 border-gray-400 object-cover"
          src={hpic}
          alt="login pic"
        />
      </div>
    </div>
  );
}

export default Login

解决方案

问题根源

你的安全规则只允许用户访问/user/{userId}/路径下的文件,但代码中上传路径是files/${file.name},完全不匹配规则要求的路径结构,同时:

  • 登录流程:你在用户登录前就发起了文件上传,此时用户还未完成身份验证,request.auth为null,不符合规则中request.auth != null的要求。
  • 注册流程:同样在创建用户前就上传文件,且路径未包含用户UID。

代码修改步骤

1. 调整文件上传路径

按照规则要求,将上传路径改为user/{userId}/{file.name},其中userId是已登录用户的UID。

2. 调整登录流程:先完成登录,再上传文件

修改loginToApp函数,确保用户登录成功后再执行文件上传:

const loginToApp = (e) => {
  e.preventDefault();
  const file = e.target[1]?.files[0];

  auth
    .signInWithEmailAndPassword(email, password)
    .then((userAuth) => {
      // 用户登录成功后再处理文件上传
      if (file) {
        const storageRef = ref(storage, `user/${userAuth.user.uid}/${file.name}`);
        const uploadTask = uploadBytesResumable(storageRef, file);

        uploadTask.on("state_changed",
          (snapshot) => {
            const progress = Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
            setProgresspercent(progress);
          },
          (error) => {
            alert(error);
          },
          () => {
            getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
              // 可选:更新用户头像到Auth
              userAuth.user.updateProfile({ photoURL: downloadURL })
                .then(() => {
                  dispatch(login({
                    email: userAuth.user.email,
                    uid: userAuth.user.uid,
                    displayName: userAuth.user.displayName,
                    photoUrl: downloadURL,
                  }));
                });
            });
          }
        );
      } else {
        // 无文件时直接分发登录状态
        dispatch(login({
          email: userAuth.user.email,
          uid: userAuth.user.uid,
          displayName: userAuth.user.displayName,
          photoUrl: userAuth.user.photoURL,
        }));
      }
    })
    .catch((error) => alert(error));
};

3. 调整注册流程:先创建用户,再上传文件并更新头像

修改register函数,先创建用户,再用用户UID作为路径上传文件:

const register = () => {
  if (!name) {
    return alert("Please enter your name!");
  }

  auth
    .createUserWithEmailAndPassword(email, password)
    .then((userAuth) => {
      if (profpic) {
        const storageRef = ref(storage, `user/${userAuth.user.uid}/${profpic.name}`);
        const uploadTask = uploadBytesResumable(storageRef, profpic);

        uploadTask.on("state_changed",
          (snapshot) => {
            const progress = Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
            setProgresspercent(progress);
          },
          (error) => {
            alert(error);
          },
          () => {
            getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
              userAuth.user.updateProfile({
                displayName: name,
                photoURL: downloadURL,
              }).then(() => {
                dispatch(login({
                  email: userAuth.user.email,
                  uid: userAuth.user.uid,
                  displayName: name,
                  photoUrl: downloadURL,
                }));
              });
            });
          }
        );
      } else {
        // 无头像时直接更新用户名并分发状态
        userAuth.user.updateProfile({ displayName: name })
          .then(() => {
            dispatch(login({
              email: userAuth.user.email,
              uid: userAuth.user.uid,
              displayName: name,
              photoUrl: userAuth.user.photoURL,
            }));
          });
      }
    })
    .catch((error) => alert(error));
};

修改后验证

调整后,文件上传路径符合安全规则的/user/{userId}/结构,且只有用户完成身份验证后才会发起上传,此时request.auth不为null且request.auth.uid与路径中的userId一致,完全满足规则要求,无需放宽权限。

内容的提问来源于stack exchange,提问作者Jerm

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 19:54:58