无论你是构建一个传统的登录表单,还是一个api token验证系统或者是你需要集成一些专有的单点登录系统,Guard组件能够很容易的做这些事情….并且很有趣!
在这个例子中,你需要构建一个api token验证系统并且学习如何使用Guard来完成工作。
创建一个User和一个User Provider
不管你如何验证,你都需要创建一个User类并实现UserInterface并配置一个 user provider.在本例中,用户使用doctrine存储在数据库中,并且每个用户都有一个apikey属性,使用这个api来访问他们的账户:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// src/AppBundle/Entity/User.php namespace AppBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="user") */ class User implements UserInterface { /** * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", unique=true) */ private $username; /** * @ORM\Colum(type"string", unique=true) */ private $apiKey; public function getUsername() { return $this->username; } public function getRoles() { return ['ROLE_USER']; } public function getPassword() { } public function getSalt() { } public function eraseCredentials() { } // more getters/setters } |
该用户没有密码,如果你想允许这个用户登录时使用密码,你可以添加一个password属性(在表单登陆中会用到)
你的User类不一定要存储在Doctrine:看你的需要。
下一步,确保你配置了用户的”user provider”:
1 2 3 4 5 6 7 8 9 10 |
# app/config/security.yml security: # ... providers: your_db_provider: entity: class: AppBundle:User # ... |
然而!你想了解关于这些的更多信息,请查阅:
- How to Load Security Users from the Database (the Entity Provider)
- How to Create a custom User Provider
步骤1)创建Authenticator类
假设你有一个api,你的客户端将针对每个请求发送一个X-AUTH-TOKEN头并跟随API token。你的任务就是读取他们并找到相关用户(如果他们有的话)。
创建一个自定义验证系统,仅仅需要创建一个类并让他实现 GuardAuthenticatorInterface
.或者继承这个更简单的AbstractGuardAuthenticator
.他需要你去实现六个方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
// src/AppBundle/Security/TokenAuthenticator.php namespace AppBundle\Security; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserProviderInterface; use Doctrine\ORM\EntityManager; class TokenAuthenticator extends AbstractGuardAuthenticator { private $em; public function __construct(EntityManager $em) { $this->em = $em; } /** * Called on every request. Return whatever credentials you want, * or null to stop authentication. */ public function getCredentials(Request $request) { if (!$token = $request->headers->get('X-AUTH-TOKEN')) { // no token? Return null and no other methods will be called return; } // What you return here will be passed to getUser() as $credentials return array( 'token' => $token, ); } public function getUser($credentials, UserProviderInterface $userProvider) { $apiToken = $credentials['token']; // if null, authentication will fail // if a User object, checkCredentials() is called return $this->em->getRepository('AppBundle:User') ->findOneBy(array('apiToken' => $apiToken)); } public function checkCredentials($credentials, UserInterface $user) { // check credentials - e.g. make sure the password is valid // no credential check is needed in this case // return true to cause authentication success return true; } public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { // on success, let the request continue return null; } public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { $data = array( 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) // or to translate this message // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) ); return new JsonResponse($data, 403); } /** * Called when authentication is needed, but it's not sent */ public function start(Request $request, AuthenticationException $authException = null) { $data = array( // you might translate this message 'message' => 'Authentication Required' ); return new JsonResponse($data, 401); } public function supportsRememberMe() { return false; } } |
干的好!下面有每种方法的解释(Guard 验证方法)
步骤2)配置这个验证
要完成他,需要注册这个类为一个服务:
1 2 3 4 5 |
# app/config/services.yml services: app.token_authenticator: class: AppBundle\Security\TokenAuthenticator arguments: ['@doctrine.orm.entity_manager'] |
最后,在security.yml中配置你的firewalls键来开启并使用这个验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# app/config/security.yml security: # ... firewalls: # ... main: anonymous: ~ logout: ~ guard: authenticators: - app.token_authenticator # if you want, disable storing the user in the session # stateless: true # maybe other things, like form_login, remember_me, etc # ... |
你做到了!你现在有一个有效的api token 验证系统了。如果你的首页需要ROLE_USER,那么你可以在不同条件下进行测试:
1 2 3 4 5 6 7 8 9 10 11 |
# test with no token curl http://localhost:8000/ # {"message":"Authentication Required"} # test with a bad token curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/ # {"message":"Username could not be found."} # test with a working token curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/ # the homepage controller is executed: the page loads normally |
现在,开始了解每一个方法。
Guard 验证方法
每个验证都需要一下方法:
getCredentials(Request $request)
他会获得每一个请求,你要从中获得token(或者任何你身份验证的信息)并将其返回。如果返回null,其余的验证部分就会被跳过。否则getUser()将会被调用并且返回值会作为第一个参数。
getUser($credentials, UserProviderInterface $userProvider)
如果getCredentials()返回一个非空值,则此方法会被调用,并传入这个返回值$credentials作为参数。你的任务是实现UserInterface并返回一个对象。如果你这么做那么checkCredentials()就会被调用。如果返回null(或者抛出AuthenticationException)验证宣告失败。
checkCredentials($credentials, UserInterface $user)
如果getUser成功返回一个User对象,该方法就会被调用。你的任务是验证credentials是否正确。对于登陆表单,在这里会检查用户的密码是否正确。如果通过验证,则返回true。如果你返回其他的东西(或者抛出AuthenticationException)验证失败。
onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
这个会在成功授权之后调用,你的任务是返回一个Response对象并发送到客户端,或者返回null继续这个请求(例如,通常会允许请求的路由器/控制器继续被调用)。由于这个是一个api,每个请求都会自我验证,你可能回去返回null。
onAuthenticationFailure(Request $request, AuthenticationException $exception)
如果验证失败会返回这个方法。你的工作是返回一个Response对象并发送到客户端。这个 $exception将告诉你什么验证过程出的错。
start
如果客户端访问一个需要验证的URI/资源他就会被调用,但没有发送验证细节信息(例如,你从getCredentials()返回null)。你的任务是返回一个Response对象帮助用户验证(例如 一个401相应,上面写着“令牌丢了”)。
supportsRememberMe
如果你希望支持“remember me”功能,这个方法就要返回true。你仍需要在防火墙下启用rememebe_me才能工作。由于这是一个无状态的api,你在这个例子中就不需要支持remember me功能了。
自定义错误信息
当onAuthenticationFailure()被调用,他就会传入一个AuthenticationException,可以使用$e->getMessageKey()(或$e->getMessageData())方法来描述你怎样验证失败的。这些信息将基于不同的验证失败产生(例如getUser与checkCredentials())。
但是,你也可以很容易的返回一个自定义的消息使用CustomUserMessageAuthenticationException。你能够抛出由getCredentials()
, getUser()
和 checkCredentials()导致的失败:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// src/AppBundle/Security/TokenAuthenticator.php // ... use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; class TokenAuthenticator extends AbstractGuardAuthenticator { // ... public function getCredentials(Request $request) { // ... if ($token == 'ILuvAPIs') { throw new CustomUserMessageAuthenticationException( 'ILuvAPIs is not a real API key: it\'s just a silly phrase' ); } // ... } // ... } |
这种情况下,由于”ILuvAPIs”是一个荒唐的api键,如果有人试图这样你可以返回一个“复活节的彩蛋”消息
1 2 |
curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/ # {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"} |
常见问题
我们可以有多个验证吗?
可以!但是当你这样做时,你需要选择一个验证的“entry_point”.这意味着当一个匿名用户试图访问受保护的资源时,你需要选择验证应该调用的start方法。例如,假设你有一个app.form_login_authenticator来处理传统的登陆。当匿名用户访问受保护的页面时,你就要使用form authenticator的start()方法,并将其重定向到登陆页面(而不是返回JOSN响应):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# app/config/security.yml security: # ... firewalls: # ... main: anonymous: ~ logout: ~ guard: authenticators: - app.token_authenticator # if you want, disable storing the user in the session # stateless: true # maybe other things, like form_login, remember_me, etc # ... |
我可以使用“form_login”吗?
可以!form_login是一种验证方式,所以你可以使用它,并还可以添加更多的验证。使用一个guard authenticator并不与其他的验证方法相冲突。
你可以使用FOSUserBundle?
可以!其实,FOSUserBundle不处理安全:他只是给你一个用户对象和一些路由和控制器,以帮助你完成登陆,注册,忘记密码等,当你使用FOSUserBundle,您通常使用form_login来验证身份。你可以继续这样做(参考前面的问题),或使用FOSUserBundle的User对象并创建我们自己的验证(就像本章一样)。