Angular と Node を使用したトークンベースの認証

王林
リリース: 2023-09-01 14:01:06
オリジナル
1284 人が閲覧しました

Angular と Node を使用したトークンベースの認証

認証は、Web アプリケーションの最も重要な部分の 1 つです。このチュートリアルでは、トークンベースの認証システムと、それが従来のログイン システムとどのように異なるかについて説明します。このチュートリアルを終えると、Angular と Node.js で書かれた完全に動作するデモが表示されます。

従来の認証システム

トークンベースの認証システムに進む前に、従来の認証システムを見てみましょう。

  1. ユーザーはログイン フォームに ユーザー名 パスワード を入力し、ログイン をクリックします。
  2. リクエストを行った後、データベースにクエリを実行してバックエンドでユーザーを認証します。リクエストが有効な場合、データベースから取得したユーザー情報を使用してセッションが作成され、セッション情報が応答ヘッダーで返され、セッション ID がブラウザに保存されます。
  3. アプリケーション内の制限されたエンドポイントにアクセスするためのセッション情報を提供します。
  4. セッション情報が有効な場合は、ユーザーに指定されたエンドポイントにアクセスして、レンダリングされた HTML コンテンツで応答してもらいます。

Angular と Node を使用したトークンベースの認証

###ここまでは順調ですね。 Web アプリケーションは正常に動作し、ユーザーを認証できるため、ユーザーは制限されたエンドポイントにアクセスできます。しかし、アプリケーション用に別のクライアント (Android クライアントなど) を開発したい場合はどうなるでしょうか?現在のアプリケーションを使用してモバイル クライアントを認証し、制限されたコンテンツを提供できますか?現状では、いいえ。これには主に 2 つの理由があります:

    セッションと Cookie はモバイル アプリケーションでは意味がありません。サーバー側で作成されたセッションや Cookie をモバイル クライアントと共有することはできません。
  1. 現在のアプリケーションで、レンダリングされた HTML を返します。モバイル クライアントでは、JSON や XML などを応答として含める必要があります。
この場合、クライアントに依存しないアプリケーションが必要です。

トークンベースの認証

トークンベースの認証では、Cookie とセッションは使用されません。トークンは、サーバーに対して行われるすべてのリクエストに対してユーザーを認証するために使用されます。トークンベースの認証を使用して最初のシナリオを再設計してみましょう。

次の制御フローを使用します:

    ユーザーはログイン フォームに
  1. ユーザー名 パスワード を入力し、ログイン をクリックします。
  2. リクエストを行った後、データベースにクエリを実行してバックエンドでユーザーを認証します。リクエストが有効な場合、データベースから取得したユーザー情報を使用してトークンが作成され、応答ヘッダーで返されるため、トークン ブラウザをローカル ストレージに保存できます。
  3. アプリケーション内の制限付きエンドポイントにアクセスするには、各リクエスト ヘッダーにトークン情報を指定します。
  4. リクエストヘッダー情報から取得したトークンが有効な場合、ユーザーは指定されたエンドポイントにアクセスし、JSON または XML で応答します。
この場合、セッションや Cookie は返されず、HTML コンテンツも返されません。これは、このアーキテクチャをあらゆるクライアント固有のアプリケーションに使用できることを意味します。以下のスキーマを確認できます:

Angular と Node を使用したトークンベースの認証

それでは、この JWT とは何でしょうか?

JWT

JWT は

JSON Web Token の略で、認可ヘッダーで使用されるトークン形式です。このトークンは、2 つのシステム間の通信を安全な方法で設計するのに役立ちます。このチュートリアルでは、JWT を「ベアラー トークン」として再定式化します。ベアラー トークンは、ヘッダー、ペイロード、署名の 3 つの部分で構成されます。

  • Header は、トークンのタイプと暗号化方法を保持するトークンの一部であり、Base-64 を使用して暗号化されます。
  • ペイロード 情報が含まれます。ユーザー情報、製品情報など、あらゆる種類のデータを入力できます。これらのデータはすべて Base-64 暗号化を使用して保存されます。
  • 署名は、ヘッダー、ペイロード、キーの組み合わせで構成されます。キーはサーバー側で安全に保管する必要があります。
JWT スキーマとサンプル トークンは以下で確認できます:

Angular と Node を使用したトークンベースの認証

多くの言語用に確立されたパッケージが存在するため、ベアラー トークン ジェネレーターを実装する必要はありません。以下でその一部をご覧いただけます:

###。ネット### Python

実際的な例

トークンベースの認証に関する基本的な情報をいくつか説明したので、実際の例に移ります。以下のアーキテクチャを見て、さらに詳しく分析していきます:

Angular と Node を使用したトークンベースの認証

  1. 複数のクライアント (Web アプリケーションやモバイル クライアントなど) が、特定の目的で API にリクエストを送信します。
  2. リクエストは、https://api.yourexampleapp.com などのサービスに対して行われます。多くの人がアプリケーションを使用する場合、要求された操作を実行するために複数のサーバーが必要になる場合があります。
  3. ここでは、ロード バランサーを使用して、バックエンド アプリケーション サーバーに最適になるようにリクエストのバランスをとります。 https://api.yourexampleapp.com にリクエストを行うと、ロード バランサーが最初にリクエストを処理し、次にクライアントを特定のサーバーにリダイレクトします。
  4. アプリケーションがあり、そのアプリケーションは複数のサーバー (server-1、server-2、...、server-n) にデプロイされています。 https://api.yourexampleapp.com に対してリクエストが行われるたびに、バックエンド アプリケーションはリクエスト ヘッダーをインターセプトし、Authorization ヘッダーからトークン情報を抽出します。このトークンはデータベース クエリに使用されます。このトークンが有効で、要求されたエンドポイントにアクセスするために必要なアクセス許可がある場合、トークンは続行されます。そうでない場合は、403 応答コード (禁止ステータスを示す) が返されます。
###アドバンテージ###

トークンベースの認証には、深刻な問題を解決するいくつかの利点があります。以下にその一部を示します:

クライアントに依存しないサービス

トークンベースの認証では、トークンはセッションまたは Cookie に認証情報を保持するのではなく、要求ヘッダーを通じて送信されます。これは状態が存在しないことを意味します。 HTTP リクエストを実行できる任意のタイプのクライアントからサーバーにリクエストを送信できます。

コンテンツ配信ネットワーク (CDN)

現在のほとんどの Web アプリケーションでは、ビューはバックエンドでレンダリングされ、HTML コンテンツがブラウザーに返されます。フロントエンド ロジックはバックエンド コードに依存します。

このような依存関係を確立する必要はありません。これにはいくつかの疑問が生じます。たとえば、フロントエンド HTML、CSS、および JavaScript を実装するデザイン会社と協力している場合は、レンダリングまたは塗りつぶし操作を実行できるように、そのフロントエンド コードをバックエンド コードに移行する必要があります。しばらくすると、レンダリングする HTML コンテンツは、コード作成会社が実装したものとは大きく異なります。

トークンベースの認証では、バックエンド コードとは別にフロントエンド プロジェクトを開発できます。バックエンド コードは、レンダリングされた HTML の代わりに JSON 応答を返し、フロントエンド コードの縮小された gzip バージョンを CDN に置くことができます。 Web ページにアクセスすると、HTML コンテンツが CDN から提供され、ページのコンテンツは API サービスによって Authorization ヘッダーのトークンを使用して設定されます。

Cookie なしのセッション (または CSRF なし)

永続的なトークンストレージ

アプリケーションでセッションの読み取り、書き込み、または削除操作が発生すると、少なくとも初回はオペレーティング システムの temp フォルダー内でファイル操作が実行されます。複数のサーバーがあり、最初のサーバーでセッションを作成するとします。別のリクエストを作成し、そのリクエストが別のサーバーに転送されると、セッション情報はそこに存在せず、「Unauthorized」応答が返されます。この問題はスティッキー セッションで解決できます。しかし、トークンベースの認証では、この状況は自然に解決されます。リクエスト トークンはサーバー上のすべてのリクエストでインターセプトされるため、スティッキー セッションの問題はありません。

これらは、トークンベースの認証と通信の最も一般的な利点です。これで、トークンベースの認証の理論的およびアーキテクチャの説明は終わりです。今度は実際の例を見てみましょう。

サンプルアプリケーション

トークンベースの認証を示す 2 つのアプリケーションが表示されます:

  1. トークンベースの認証バックエンド
  2. トークンベースの認証フロントエンド

バックエンド プロジェクトではサービスの実装が行われ、サービスの結果は JSON 形式になります。サービスから返されるビューはありません。フロントエンド プロジェクトには、フロントエンド HTML 用の Angular プロジェクトがあり、バックエンド サービスにリクエストを行うために Angular サービスによってフロントエンド アプリケーションが設定されます。

トークンベースの認証バックエンド

バックエンド プロジェクトには、3 つの主要なファイルがあります:

  • package.json は依存関係の管理に使用されます。
  • models/User.js ユーザーに対するデータベース操作のためのユーザー モデルが含まれています。
  • server.js プロジェクトのガイダンスとリクエストの処理に使用されます。
###それでおしまい!このプロジェクトは非常にシンプルなので、深く掘り下げなくても主要な概念を簡単に理解できます。

リーリー ​

package.json プロジェクトの依存関係が含まれます: MVC の場合は express、ポスト ノードをモックする場合は body-parser js では、リクエストのログ記録には morgan 、MongoDB に接続するための ORM フレームワークには mongoose 、および jsonwebtokenユーザーモデルを使用して JWT トークンを作成するために使用されます。 engines というプロパティもあります。これは、プロジェクトが Node.js バージョン >= 0.10.0 を使用して作成されたことを示します。これは Heroku などの PaaS サービスに役立ちます。このトピックについては、別のセクションでも説明します。 リーリー ​

ユーザー モデルのペイロードを使用してトークンを生成すると言いました。このモデルは、MongoDB 上でユーザー操作を実行するのに役立ちます。

User.js では、ユーザー パターンが定義され、マングース モデルを使用してユーザー モデルが作成されます。モデルはデータベース操作の準備ができています。

依存関係が定義され、ユーザー モデルが定義されたので、これらをすべてまとめて、特定のリクエストを処理するサービスを構築しましょう。

リーリー ​

Node.js では、

require を使用してプロジェクトにモジュールを含めることができます。まず、必要なモジュールをプロジェクトにインポートする必要があります: リーリー ​

当社のサービスは特定のポートを通じて提供されます。システム環境変数でポート変数が定義されている場合、またはポート

3001 が定義されている場合は、これを使用できます。その後、User モデルが組み込まれ、一部のユーザー操作のためにデータベース接続が確立されます。データベース接続 URL の環境変数 MONGO_URL を定義することを忘れないでください。 リーリー ​

上のセクションでは、Express を使用して、Node での HTTP リクエスト処理をシミュレートするためのいくつかの構成を作成しました。クライアントに依存しないシステムを開発するために、さまざまなドメインからのリクエストを許可します。これを許可しない場合、Web ブラウザで CORS (Cross-Origin Request Sharing) エラーが発生します。

  • Access-Control-Allow-Origin 允许所有域。
  • 您可以向此服务发送 POSTGET 请求。
  • X-Requested-Withcontent-type 标头是允许的。
app.post('/authenticate', async function(req, res) {
    try {
      const user = await User.findOne({ email: req.body.email, password: req.body.password }).exec();
      if (user) {
        res.json({
          type: true,
          data: user,
          token: user.token
        });
      } else {
        res.json({
          type: false,
          data: "Incorrect email/password"
        });
      }
    } catch (err) {
      res.json({
        type: false,
        data: "Error occurred: " + err
      });
    }
  });
ログイン後にコピー
 

我们已经导入了所有必需的模块并定义了我们的配置,所以现在是时候定义请求处理程序了。在上面的代码中,每当你使用用户名和密码向 /authenticate 发出 POST 请求时,你都会得到一个 JWT 令牌。首先,使用用户名和密码处理数据库查询。如果用户存在,则用户数据将与其令牌一起返回。但是如果没有与用户名和/或密码匹配的用户怎么办?

 app.post('/signin', async function(req, res) {
    try {
      const existingUser = await User.findOne({ email: req.body.email }).exec();
      if (existingUser) {
        res.json({
          type: false,
          data: "User already exists!"
        });
      } else {
        const userModel = new User();
        userModel.email = req.body.email;
        userModel.password = req.body.password;
        const savedUser = await userModel.save();
        savedUser.token = jwt.sign(savedUser.toObject(), process.env.JWT_SECRET);
        const updatedUser = await savedUser.save();
        res.json({
          type: true,
          data: updatedUser,
          token: updatedUser.token
        });
      }
    } catch (err) {
      res.json({
        type: false,
        data: "Error occurred: " + err
      });
    }
  });
ログイン後にコピー
 

当您使用用户名和密码向 /signin 发出 POST 请求时,将使用发布的用户信息创建一个新用户。在 14th 行,您可以看到使用 jsonwebtoken 模块生成了一个新的 JSON 令牌,该令牌已分配给 jwt 变量。认证部分没问题。如果我们尝试访问受限端点怎么办?我们如何设法访问该端点?

app.get('/me', ensureAuthorized, async function(req, res) {
    try {
      const user = await User.findOne({ token: req.token }).exec();
      res.json({
        type: true,
        data: user
      });
    } catch (err) {
      res.json({
        type: false,
        data: "Error occurred: " + err
      });
    }
  });
ログイン後にコピー
 

当您向 /me 发出 GET 请求时,您将获得当前用户信息,但为了继续请求的端点,确保Authorized函数将被执行。

function ensureAuthorized(req, res, next) {
    var bearerToken;
    var bearerHeader = req.headers["authorization"];
    if (typeof bearerHeader !== 'undefined') {
        var bearer = bearerHeader.split(" ");
        bearerToken = bearer[1];
        req.token = bearerToken;
        next();
    } else {
        res.send(403);
    }
}
ログイン後にコピー
 

在该函数中,拦截请求头,并提取authorization头。如果此标头中存在承载令牌,则该令牌将分配给 req.token 以便在整个请求中使用,并且可以使用 next( )。如果令牌不存在,您将收到 403(禁止)响应。让我们回到处理程序 /me,并使用 req.token 使用此令牌获取用户数据。每当您创建新用户时,都会生成一个令牌并将其保存在数据库的用户模型中。这些令牌是独一无二的。

对于这个简单的项目,我们只有三个处理程序。之后,您将看到:

process.on('uncaughtException', function(err) {
    console.log(err);
});
ログイン後にコピー
 

如果发生错误,Node.js 应用程序可能会崩溃。使用上面的代码,可以防止崩溃,并在控制台中打印错误日志。最后,我们可以使用以下代码片段启动服务器。

// Start Server
app.listen(port, function () {
    console.log( "Express server listening on port " + port);
});
ログイン後にコピー
 

总结一下:

  • 模块已导入。
  • 配置已完成。
  • 已定义请求处理程序。
  • 定义中间件是为了拦截受限端点。
  • 服务器已启动。

我们已经完成了后端服务。为了让多个客户端可以使用它,您可以将这个简单的服务器应用程序部署到您的服务器上,或者也可以部署在 Heroku 中。项目根文件夹中有一个名为 Procfile 的文件。让我们在 Heroku 中部署我们的服务。

Heroku 部署

您可以从此 GitHub 存储库克隆后端项目。

我不会讨论如何在 Heroku 中创建应用程序;如果您之前没有创建过 Heroku 应用程序,可以参考这篇文章来创建 Heroku 应用程序。创建 Heroku 应用程序后,您可以使用以下命令将目标添加到当前项目:

git remote add heroku <your_heroku_git_url>
ログイン後にコピー

现在您已经克隆了一个项目并添加了一个目标。在 git addgit commit 之后,您可以通过执行 git push heroku master 将代码推送到 Heroku。当您成功推送项目时,Heroku 将执行 npm install 命令将依赖项下载到 Heroku 上的 temp 文件夹中。之后,它将启动您的应用程序,您可以使用 HTTP 协议访问您的服务。

基于令牌的-auth-frontend

在前端项目中,您将看到一个 Angular 项目。在这里,我只提及前端项目中的主要部分,因为 Angular 不是一个教程可以涵盖的内容。

您可以从此 GitHub 存储库克隆该项目。在此项目中,您将看到以下文件夹结构:

Angular と Node を使用したトークンベースの認証

我们拥有三个组件——注册、配置文件和登录——以及一个身份验证服务。

您的app.component.html 如下所示:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
  </head>
  <body>
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
        <div class="container-fluid">
          <a class="navbar-brand" href="#">Home</a>
          <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
          </button>
          <div class="collapse navbar-collapse" id="navbarNav">
            <ul class="navbar-nav">
              
              <li class="nav-item"><a class="nav-link" routerLink="/profile">Me</a></li>
              <li class="nav-item"><a class="nav-link" routerLink="/login">Signin</a></li>
              <li class="nav-item"><a class="nav-link" routerLink="/signup">Signup</a></li>
              <li class="nav-item"><a class="nav-link" (click)="logout()">Logout</a></li>
            </ul>
          </div>
        </div>
      </nav>

    <div class="container">
        <router-outlet></router-outlet>
    </div> 

  </body>
</html>
ログイン後にコピー
 

在主组件文件中,<router-outlet></router-outlet> 定义各个组件的路由。

auth.service.ts 文件中,我们定义 AuthService 类,该类通过 API 调用来处理身份验证,以登录、验证 Node.js 应用程序的 API 端点。

import { Injectable } from '@angular/core';
import { HttpClient,HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private apiUrl = 'your_node_app_url';
  public token: string ='';


  constructor(private http: HttpClient) {
    
  }


  signin(username: string, password: string): Observable<any> {
    const data = { username, password };
    return this.http.post(`${this.apiUrl}/signin`, data);
  }

 

  authenticate(email: string, password: string): Observable<any> {
    const data = { email, password };
    console.log(data)

    return this.http.post(`${this.apiUrl}/authenticate`, data)
      .pipe(
        tap((response:any) => {
          this.token = response.data.token; // Store the received token
          localStorage.setItem('token',this.token)
          console.log(this.token)
        })
      );
  }

  profile(): Observable<any> {
    const headers = this.createHeaders();
    return this.http.get(`${this.apiUrl}/me`,{ headers });
  }


  private createHeaders(): HttpHeaders {
    let headers = new HttpHeaders({
      'Content-Type': 'application/json',
    });

    if (this.token) {
      headers = headers.append('Authorization', `Bearer ${this.token}`);
    }

    return headers;
  }

  logout(): void {
    
    localStorage.removeItem('token');
  }
 
  
}


ログイン後にコピー

authenticate() 方法中,我们向 API 发送 POST 请求并对用户进行身份验证。从响应中,我们提取令牌并将其存储在服务的 this.token 属性和浏览器的 localStorage 中,然后将响应作为 Observable 返回。

profile() 方法中,我们通过在 Authorization 标头中包含令牌来发出 GET 请求以获取用户详细信息。

createHeaders() 方法在发出经过身份验证的 API 请求时创建包含身份验证令牌的 HTTP 标头。当用户拥有有效令牌时,它会添加一个授权标头。该令牌允许后端 API 对用户进行身份验证。

如果身份验证成功,用户令牌将存储在本地存储中以供后续请求使用。该令牌也可供所有组件使用。如果身份验证失败,我们会显示一条错误消息。

不要忘记将服务 URL 放入上面代码中的 baseUrl 中。当您将服务部署到 Heroku 时,您将获得类似 appname.herokuapp.com 的服务 URL。在上面的代码中,您将设置 var baseUrl = "appname.herokuapp.com"

注销功能从本地存储中删除令牌。

signup.component.ts 文件中,我们实现了 signup () 方法,该方法获取用户提交的电子邮件和密码并创建一个新用户。

import { Component } from '@angular/core';
import { AuthService } from '../auth.service';



@Component({
  selector: 'app-signup',
  templateUrl: './signup.component.html',
  styleUrls: ['./signup.component.css']
})
export class SignupComponent {
  password: string = '';
  email: string = '';
  

  constructor(private authService:AuthService){}

  signup(): void {
    this.authService.signin(this.email, this.password).subscribe(
      (response) => {
        // success response
        console.log('Authentication successful', response);
       
      },
      (error) => {
        // error response
        console.error('Authentication error', error);
      }
    );
  }
}
ログイン後にコピー
  login.component.ts 文件看起来与注册组件类似。  
import { Component } from '@angular/core';
import { AuthService } from '../auth.service';



@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent {
    
  email: string = '';
  password: string = '';

  constructor(private authService: AuthService) {}

  login(): void {
    this.authService.authenticate(this.email, this.password).subscribe(
      (response) => {
        // success response
        console.log('Signin successful', response);
       
      },
      (error) => {
        // error response
        console.error('Signin error', error);
      }
    );
  }
}
ログイン後にコピー

配置文件组件使用用户令牌来获取用户的详细信息。每当您向后端的服务发出请求时,都需要将此令牌放入标头中。 profile.component.ts 如下所示:

import { Component } from '@angular/core';
import { AuthService } from '../auth.service';
@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.css']
})

export class ProfileComponent {
  myDetails: any;

  constructor(private authService: AuthService) { }

  ngOnInit(): void {
    this.getProfileData();
  }
  getProfileData(): void {
    this.authService.me().subscribe(
      (response: any) => {
        this.myDetails = response;
        console.log('User Data:', this.myDetails);
      },
      (error: any) => {
        console.error('Error retrieving profile data');
      }
    );
  }
ログイン後にコピー
 

在上面的代码中,每个请求都会被拦截,并在标头中放入授权标头和值。然后,我们将用户详细信息传递到 profile.component.html 模板。

<h2>User profile </h2>

<div class="row">
    <div class="col-lg-12">
        <p>{{myDetails.data.id}}</p>
        <p>{{myDetails.data.email}}</p>
    </div>
</div>
ログイン後にコピー

最后,我们在 app.routing.module.ts 中定义路由。

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { ProfileComponent } from './profile/profile.component';
import { SignupComponent } from './signup/signup.component';

const routes: Routes = [
  {path:'signup' , component:SignupComponent},
  {path:'login' , component:LoginComponent},
  { path: 'profile', component: ProfileComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
ログイン後にコピー
 

从上面的代码中您可以很容易地理解,当您转到/时,将呈现app.component.html页面。另一个例子:如果您转到/signup,则会呈现signup.component.html。这个渲染操作将在浏览器中完成,而不是在服务器端。

结论

基于令牌的身份验证系统可帮助您在开发独立于客户端的服务时构建身份验证/授权系统。通过使用这项技术,您将只需专注于您的服务(或 API)。

身份验证/授权部分将由基于令牌的身份验证系统作为服务前面的一层进行处理。您可以从任何客户端(例如网络浏览器、Android、iOS 或桌面客户端)访问和使用服务。

Node.js https://github.com/auth0/node-jsonwebtoken
PHP http://github.com/firebase/php-jwt
Java http://github.com/auth0/java-jwt
红宝石 https://github.com/jwt/ruby-jwt
https://github.com/auth0/java-jwt
http://github.com/progrium/pyjwt/

以上がAngular と Node を使用したトークンベースの認証の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!