将图像上传到 FirebaseStorage - 权限被拒绝

Uploading Image to FirebaseStorage - Permission Denied

我正在 authentication/signing 开发 FirebaseStorage,用户可以在注册期间上传图片,但我一直收到关于 用户没有任何权限的错误。 当用户点击注册时,使用 Firebase 的注册过程会起作用,因为用户是在身份验证中创建的,但信息不会保存在 Firestore 数据库中。有谁知道我做错了什么?

这是我的 Firebase 存储规则:

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /user-images {
      allow read, create: if request.auth != null;
    }
    match /avatar.png {
        allow read: if true;
    }
  }
}

这是注册屏幕。

import 'dart:io';
import 'dart:ui';

import 'package:firebase_storage/firebase_storage.dart';
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/services.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

import '/screens/auth_screen.dart';
import '/widgets/build_row.dart';
import '/screens/chat_screen.dart';
import '/widgets/user_image_picker.dart';

class SignUpScreen extends StatefulWidget {
  static const routeName = '/sign-up-screen';

  @override
  _SignUpScreenState createState() => _SignUpScreenState();
}

class _SignUpScreenState extends State<SignUpScreen> {
  final _auth = FirebaseAuth.instance;

  final GlobalKey<FormState> _key = GlobalKey();
  String _username = '';
  String _userPW = '';
  String _userEmail = '';

  var _isLoading = false;

  File? _userImageFile;
  void _pickedImage(File image) {
    _userImageFile = image;
  }

  void _submit() {
    final isValid = _key.currentState!.validate();
    if (isValid) {
      _key.currentState!.save();
    }
    this._submitAuthForm(_userEmail.trim(), _userPW.trim(), _username.trim());
  }

  void _submitAuthForm(
    String userEmail,
    String password,
    String username,
  ) async {
    try {
      setState(() {
        _isLoading = true;
      });

      UserCredential userCredential = await _auth
          .createUserWithEmailAndPassword(email: userEmail, password: password);

      final ref = FirebaseStorage.instance
          .ref()
          .child('user-images/${userCredential.user!.uid}.jpg');

      final avatarRef = FirebaseStorage.instance.ref().child('avatar.png');

      var url;
      if (_userImageFile == null) {
        url = await avatarRef.getDownloadURL();
      }
      if (_userImageFile != null) {
        await ref.putFile(_userImageFile!);
        url = await ref.getDownloadURL();
      }

      await FirebaseFirestore.instance
          .collection('users')
          .doc(userCredential.user!.uid)
          .set(
        {
          'username': username,
          'email': userEmail,
          'image_url': url,
        },
      );
      Navigator.of(context).pushReplacement(
        MaterialPageRoute(builder: (ctx) => ChatScreen()),
      );
    } on PlatformException catch (error) {
      var message = 'An Error has occured!';
      if (error.message != null) {
        message = error.message!;
      }
      setState(() {
        _isLoading = false;
      });
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(message),
          backgroundColor: Colors.deepPurple.shade600,
        ),
      );
    } catch (error) {
      if (error.toString().contains(
          'The email address is already in use by another account.')) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content:
                Text('The email address is already in use by another account.'),
            backgroundColor: Colors.deepPurple.shade600,
          ),
        );
      }
      print(error);

      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final deviceSize = MediaQuery.of(context).size;

    return GestureDetector(
      onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
      child: Scaffold(
        backgroundColor: Theme.of(context).primaryColor,
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Container(
                width: deviceSize.width * 0.8,
                constraints: BoxConstraints(
                    maxHeight: (deviceSize.height -
                            MediaQuery.of(context).viewInsets.bottom) *
                        0.7),
                decoration: BoxDecoration(
                  border: Border.all(
                    color: Colors.deepPurple.shade600,
                    width: 2.0,
                  ),
                  color: Colors.white,
                ),
                child: SingleChildScrollView(
                  padding: EdgeInsets.only(
                    left: 10,
                    bottom: 20,
                  ),
                  child: Form(
                    key: _key,
                    child: Column(
                      mainAxisSize: MainAxisSize.min,

                      children: [
                        UserImagePicker(_pickedImage),
                        BuildRowWithIcon(Icons.person, 'Username:', false,
                            (value) {
                          if (value.isEmpty) {
                            return 'Please enter a username';
                          }
                        
                          return null;
                        }, (value) {
                          _username = value;
                        }, 'Username must be at least 6 characters long. Special characters: $, ^, &, !, ?'),
                        BuildRow(
                          Icons.email_outlined,
                          'E-mail:',
                          false,
                          (value) {
                            if (value.isEmpty) {
                              return 'Please provide an e-mail';
                            }
                            return null;
                          },
                          (value) {
                            _userEmail = value;
                          },
                        ),
                        BuildRowWithIcon(
                            Icons.password_outlined, 'Password:', true,
                            (value) {
                          if (value.isEmpty) {
                            return 'Please enter a password';
                          }
                          if (value.length < 7) {
                            return 'Password must be at least 7 characters long';
                          }
                          return null;
                        }, (value) {
                          _userPW = value;
                        }, 'Password must be at least 7 characters long. You must use a combination of two of the following: letters, numbers, & special characters. Special characters: $, ^, &, !, ?'),
                        BuildRowWithIcon(
                          Icons.password_outlined,
                          'Confirm PW:',
                          true,
                          (value) {
                            if (value.isEmpty) {
                              return 'Please enter the same password';
                            }
                            return null;
                          },
                          null,
                          'Confirm password',
                        ),
                      ],
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 30),
              if (_isLoading) CircularProgressIndicator(),
              if (!_isLoading)
                ElevatedButton(
                  onPressed: _submit,
                  child: const Text(
                    'Sign up!',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  style: ButtonStyle(
                    elevation: MaterialStateProperty.all<double>(20),
                    backgroundColor: MaterialStateProperty.all<Color>(
                        Colors.deepPurple.shade600),
                    foregroundColor: MaterialStateProperty.all<Color>(
                        Colors.tealAccent.shade200),
                    padding: MaterialStateProperty.all<EdgeInsets>(
                        EdgeInsets.all(15)),
                    shadowColor: MaterialStateProperty.all<Color>(
                        Colors.deepPurple.shade800),
                    shape: MaterialStateProperty.all<OutlinedBorder>(
                        RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(15),
                    )),
                  ),
                ),
              SizedBox(height: 25),
              TextButton(
                onPressed: () => Navigator.of(context)
                    .pushReplacementNamed(AuthScreen.routeName),
                child: Text(
                  'I already have an account.',
                  style: TextStyle(
                    color: Colors.deepPurple.shade400,
                    fontSize: 15,
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

这是 ImagePicker() 小部件

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'package:path/path.dart' as path;

import 'package:path_provider/path_provider.dart' as syspath;

class UserImagePicker extends StatefulWidget {
  final void Function(File pickedImage) imagePickFn;

  UserImagePicker(this.imagePickFn);

  @override
  _UserImagePickerState createState() => _UserImagePickerState();
}

class _UserImagePickerState extends State<UserImagePicker> {
  File? _pickedImage;

  Future<void> _pickImage() async {
    final _pickedImageXFile =
        await ImagePicker().pickImage(
            source: ImageSource.gallery,
            imageQuality: 50,
            maxHeight: 150,
            maxWidth: 150);
    if (_pickedImageXFile == null) {
      return;
    }
    final _pickedImageFile = File(_pickedImageXFile.path);
    setState(() {
      _pickedImage = _pickedImageFile;
    });
    final appDir = await syspath.getApplicationDocumentsDirectory();
    final fileName = path.basename(_pickedImageFile.path);
    final userImageFile =
        await _pickedImageFile.copy('${appDir.path}/$fileName');
    widget.imagePickFn(userImageFile);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Container(
          margin: EdgeInsets.only(top: 10),
          child: CircleAvatar(
            radius: 35,
            backgroundColor: Colors.tealAccent.shade200,
            backgroundImage:
                _pickedImage == null ? null : FileImage(_pickedImage!),
          ),
        ),
        TextButton.icon(
          onPressed: _pickImage,
          icon: Icon(
            Icons.image_outlined,
            color: Colors.deepPurple.shade600,
          ),
          label: Text(
            'Add User Image',
            style: TextStyle(color: Colors.deepPurple.shade600),
          ),
        ),
      ],
    );
  }
}

附带问题: 有没有办法使用 Future 进行验证?我希望能够在 he/she 输入 password/confirm 密码时向用户显示它不符合要求或确认密码值不是密码值。我需要为此使用单独的 TextController 吗?

您的规则中的 match /user-images 匹配根名称 user-images 中的文件。它不会递归地将权限应用到过滤器中的文件。

要允许任何人在 /user-images 下写入文件,您可以应用 wildcard match:

match /user-images/{file} {
  allow read, create: if request.auth != null;
}

或者如果您想允许在 /user-images 下的任何文件夹中写入:

match /user-images/{file=**} {
  allow read, create: if request.auth != null;
}

然后让大家阅读/user-images/avatar.png:

match /user-images/avatar.png {
    allow read: if true;
}

您需要检查规则部分的权限并允许读取、写入:如果为真;

并检查 AndroidManifest.xml 文件,是否已授予所需权限。