不管進行何種身份驗證,本質上都是經過用戶憑證 (credentials) 尋找用戶 (user class) 的過程,不管是傳統的表單 (username/password) 登陸仍是 API 令牌 (token),憑證一般存儲於請求對象 (header, body, query 等) 中,所以從請求對象中提取用戶憑證並尋找用戶的過程,即稱之爲認證 (Authentication) 。php
Symfony Security 安全組件由 4 個子組件組合而成,它們之間相互獨立,你能夠選擇性的安裝。html
組件 | 描述 |
---|---|
security-core | 提供用戶密碼加密到認證、受權等基本安全功能。 |
security-guard | 一個抽象的身份驗證層,用於建立複雜的身份驗證系統。 |
security-http | 將安全組件與 HTTP 協議相互集成,以處理安全方面的請求和響應。 |
security-csrf | 爲跨域請求僞造 (CSRF) 提供驗證和保護。 |
使用 Symfony Security 須要預先安裝,因爲 security-bundle
已經集成了全部的安全組件,所以只須要安裝它便可:git
$ composer require symfony/security-bundle
要實現 Github
登陸,首先須要在 Settings -> Developer settings -> OAuth Apps -> New OAuth App
建立一個應用,並設置認證回調地址(Authorization callback URL
),建立成功以後會分配你一個 Client ID
和 Client Secret
,將此參數複製到項目 環境變量文件:github
# .env.local GITHUB_CLIENT_ID=YOUR_CLIENT_ID GITHUB_CLIENT_SECRET=YOUR_CLIENT_SECRET
建立一個類用於集中管理 Github API 訪問服務,咱們使用 http-client 組件做爲 HTTP 客戶端:數據庫
// ./src/OAuth/Github.php <?php namespace App\OAuth; use Symfony\Contracts\HttpClient\HttpClientInterface; class Github { const AUTHORIZE_URL = 'https://github.com/login/oauth/authorize'; const ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token'; const USER_URL = 'https://api.github.com/user'; private $client; private $clientId; private $clientSecret; public function __construct(HttpClientInterface $client, string $clientId, string $clientSecret) { $this->client = $client; $this->clientId = $clientId; $this->clientSecret = $clientSecret; } public function getAuthorizeUrl(string $redirectUri, string $state = null): string { $query = [ 'client_id' => $this->clientId, 'redirect_uri' => $redirectUri, 'state' => $state, ]; return self::AUTHORIZE_URL.'?'.http_build_query($query); } public function getAccessToken(string $code): array { $options = [ 'body' => [ 'client_id' => $this->clientId, 'client_secret' => $this->clientSecret, 'code' => $code, ], 'headers' => [ 'Accept' => 'application/json', ], ]; $response = $this->client->request('POST', self::ACCESS_TOKEN_URL, $options); $data = $response->toArray(); if (isset($data['error'])) { throw new \RuntimeException(sprintf('%s (%s)', $data['error_description'], $data['error'])); } return $data; } public function getUser(string $accessToken): array { $options = [ 'headers' => [ 'Authorization' => sprintf('token %s', $accessToken), ], ]; $response = $this->client->request('GET', self::USER_URL, $options); $data = $response->toArray(); if (isset($data['error'])) { throw new \RuntimeException(sprintf('%s (%s)', $data['error_description'], $data['error'])); } return $data; } }
配置環境變量到服務:json
# ./config/services.yaml services: # ... App\OAuth\Github: $clientId: '%env(GITHUB_CLIENT_ID)%' $clientSecret: '%env(GITHUB_CLIENT_SECRET)%'
建立用戶對象 (user class),用戶對象必需繼承 UserInterface。api
// ./src/Entity/User.php class User implements UserInterface { // ... /** * @ORM\Column(type="string", length=255, unique=true) */ private $username; /** * @ORM\Column(type="string", length=255) */ private $nickname; /** * @ORM\Column(type="string", length=255) */ private $avatar; /** * @ORM\Column(type="datetime", nullable=true) */ private $updatedAt; /** * @ORM\Column(type="datetime_immutable") */ private $createdAt; // ... }
建立控制器,跳轉到 Github 進行認證:跨域
// ./src/Controller/SecurityController.php class SecurityController extends AbstractController { /** * @Route("/login/oauth/github", name="login_oauth_github") * * @param Request $request * @param Github $client * * @return Response */ public function loginWithGithub(Request $request, Github $client) { // 隨機 state 字符串,用於防止 CSRF 攻擊 $state = bin2hex(random_bytes(8)); $session = $request->getSession(); $session->set(GithubAuthenticator::STATE, $state); // 生成回調地址,該地址即爲 Authorization callback URL,需在 Github 上填寫 $callback = $this->generateUrl('login_oauth_github_callback', [], 0); $redirect = $client->getAuthorizeUrl($callback, $state); return $this->redirect($redirect); } /** * @Route("/login/oauth/github/callback", name="login_oauth_github_callback") * * 只須要定義路由,這個路由什麼都不用幹,由於它將會被 Guard 攔截 */ public function loginWithGithubCallback() { // nothing todo... } }
建立 Guard 攔截器實現認證過程:安全
// ./src/Security/GithubAuthenticator.php class GithubAuthenticator extends AbstractGuardAuthenticator { use TargetPathTrait; const STATE = '_github_oauth_state'; private $entityManager; private $httpUtils; private $github; public function __construct(EntityManagerInterface $entityManager, HttpUtils $httpUtils, Github $github) { $this->entityManager = $entityManager; $this->httpUtils = $httpUtils; $this->github = $github; } /** * 每個請求都會進入該方法,須要在此過濾那些不相干的請求,返回 false 便可跳過 */ public function supports(Request $request) { // 過濾請求,只攔截回調地址便可,回調地址中 Github 迴帶上 code return $this->httpUtils->checkRequestPath($request, 'login_oauth_github_callback') && $request->query->has('code'); } /** * 若是匹配到 supports 則調用該方法,用於從請求中獲取憑證,用於 getUser */ public function getCredentials(Request $request) { // 驗證 state,防止 CSRF 攻擊 $state = $request->query->get('state'); if ($state !== $request->getSession()->get(self::STATE)) { throw new CustomUserMessageAuthenticationException('Bad authentication state.'); } return $request->query->get('code'); } /** * 從 getCredentials 獲取到的憑證查找並返回用戶,若是返回 NULL 或拋出異常則認證失敗 * 若是返回了用戶 (UserInterface),則進入到 checkCredentials */ public function getUser($credentials, UserProviderInterface $userProvider) { // $credentials 便是 getCredentials 返回的數據 $token = $this->github->getAccessToken($credentials); try { $user = $this->github->getUser($token['access_token']); } catch (\Throwable $th) { // ... } try { // 若是找到用戶直接返回,進入下一步 $entity = $userProvider->loadUserByUsername($user['login']); } catch (UsernameNotFoundException $e) { // 若是第一次登陸,則須要存進數據庫 $entity = new User(); $entity->setUsername($user['login']); $entity->setNickname($user['name']); $entity->setAvatar($user['avatar_url']); $entity->setCreatedAt(new \DateTimeImmutable()); $this->entityManager->persist($entity); $this->entityManager->flush(); } return $entity; } /** * OAuth 認證不須要檢查憑證正確與否 */ public function checkCredentials($credentials, UserInterface $user) { return true; } /** * 任何一步認證失敗將調用該方法 */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { throw new \RuntimeException($exception->getMessage()); } /** * 認證成功後將調用該方法,用於跳轉至前一頁面 */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { // getTargetPath 返回用戶在認證前停留的頁面地址,由 TargetPathTrait 提供 $targetPath = $this->getTargetPath($request->getSession(), $providerKey); if (!$targetPath) { $targetPath = $this->httpUtils->generateUri($request, 'app_index'); } return $this->httpUtils->createRedirectResponse($request, $targetPath); } /** * 若是在 secruity.yaml 中配置了access_control 段,當用戶權限不足時進入到該方法,不然不執行 */ public function start(Request $request, AuthenticationException $authException = null) { return $this->httpUtils->createRedirectResponse($request, 'app_login'); } /** * 是否啓用 「自動登陸」 功能,OAuth 認證不支持該功能 */ public function supportsRememberMe() { return false; } }
配置 Guard 攔截器:session
# ./config/packages/security.yarml security: # ... # 定義用戶加載器 providers: entity_provider: entity: { class: App\Entity\User, property: username } # 定義防火牆規則 firewalls: # ... # 防火牆區域,可隨意定義名稱 my_area: # 加載器使用 entity_provider provider: entity_provider # 攔截器使用 GithubAuthenticator guard: authenticators: - App\Security\GithubAuthenticator
如今,只須要在登陸的地方放上連接至路由 login_oauth_github 便可使用 Github 登陸:
<a href="{{ path('login_oauth_github') }}">Github 登陸</a>
如下是常見的安全配置參數說明。
# config/packages/security.yaml security: # 用戶密碼加密方式,該參數決定了用戶密碼將由何種方式加密 encoders: # 由系統決定合適的加密方式 App\Entity\FooUser: auto # 由 sodium 方式加密,可取範圍:plaintext, pbkdf2, bcrypt, argon2i, native, sodium App\Entity\BarUser: sodium # 用戶加載器,該參數決定用戶從何處加載 providers: # 硬編碼用戶加載器 my_memory_provider: memory: users: # 一個稱爲 foo 的用戶,並擁有 ROLE_READER 角色 foo: { password: foo_password, roles: ROLE_READER } # 又一個稱爲 bar 的用戶,並擁有 ROLE_EDITOR 角色 bar: { password: bar_password, roles: ROLE_EDITOR } # 實體用戶加載器(從數據庫中加載) my_entity_provider: entity: { class: App\Entity\AcmeUser, property: username } # 自定義用戶加載器,必需實現 UserProviderInterface 接口 my_custom_provider: id: App\Security\MyCustomProvider # 訪問區域,可配置多個訪問區域,多個區域有前後之分好比 /api 和 /api/user 將先匹配到 /api firewalls: # 由 /api 開始的請求將匹配到 zone_a 區域,該區域由 my_entity_provider 提供用戶 zone_a: pattern: ^/api provider: my_entity_provider # 由 /admin 開始,而且 host 爲 admin.com 的請求將匹配到 area_b 區域,該域由用 my_custom_provider 提供用戶 zone_b: pattern: ^/admin host: admin.com provider: my_custom_provider # 訪問控制,可配置多個訪問區域,多個區域有前後之分 access_control: # 由 /api 開始的請求必需包含 ROLE_API 或 ROLE_USER 角色 - { path: ^/api, roles: [ROLE_API, ROLE_USER] } # 由 /admin 開始的請求必需包含 ROLE_ADMIN 角色 - { path: ^/admin, roles: ROLE_ADMIN } # 用戶角色等級 role_hierarchy: # 擁有 ROLE_API 角色的用戶將同時具有 ROLE_READER 和 ROLE_EDITOR 權限 ROLE_API: [ROLE_READER, ROLE_EDITOR] # 擁有 ROLE_ADMIN 角色的用戶將同時具有 ROLE_ADMIN_ARTICLE, ROLE_ADMIN_COMMENT 權限 ROLE_ADMIN: [ROLE_ADMIN_ARTICLE, ROLE_ADMIN_COMMENT]