OAuth 2.0 容许第三方应用程序访问受限的HTTP资源的受权协议,像日常你们使用Github、Google帐号来登录其余系统时使用的就是 OAuth 2.0 受权框架,下图就是使用Github帐号登录Coding系统的受权页面图:html
相似使用 OAuth 2.0 受权的还有不少,本文将介绍 OAuth 2.0 相关的概念如:角色、受权类型等知识,如下是我整理一张 OAuth 2.0 受权的脑头,但愿对你们了解 OAuth 2.0 受权协议有帮助。git
文章将以脑图中的内容展开 OAuth 2.0 协议同时除了 OAuth 2.0 外,还会配合 Spring Security OAuth2 来搭建OAuth2客户端,这也是学习 OAuth 2.0 的目的,直接应用到实际项目中,加深对 OAuth 2.0 和 Spring Security 的理解。github
OAuth 2.0 中有四种类型的角色分别为:资源Owner、受权服务、客户端、资源服务,这四个角色负责不一样的工做,为了方便理解先给出一张大概的流程图,细节部分后面再分别展开:web
OAuth 2.0 大概受权流程面试
资源 Owner能够理解为一个用户,如以前提到使用Github登录Coding中的例子中,用户使用GitHub帐号登录Coding,Coding就须要知道用户在GitHub系统中的的头像、用户名、email等信息,这些帐户信息都是属于用户的这样就不难理解资源 Owner了。在Coding请求从GitHub中获取想要的用户信息时也是没那容易的,GitHub为了安全起见,至少要经过用户(资源 Owner)的赞成才行。spring
明白资源 Owner后,相信你已经知道什么是资源服务器,在这个例子中用户帐号的信息都存放在GitHub的服务器中,因此这里的资源服务器就是GitHub服务器。GitHub服务器负责保存、保护用户的资源,任何其余第三方系统想到使用这些信息的系统都须要通过资源 Owner受权,同时依照 OAuth 2.0 受权流程进行交互。数据库
知道资源 Owner和资源服务器后,OAuth中的客户端角色也相对容易理解了,简单的说客户端就是想要获取资源的系统,如例子中的使用GitHub登录Coding时,Coding就是OAuth中的客户端。客户端主要负责发起受权请求、获取AccessToken、获取用户资源。api
有了资源 Owner、资源服务器、客户端还不能完成OAuth受权的,还须要有受权服务器。在OAuth中受权服务器除了负责与用户(资源 Owner)、客房端(Coding)交互外,还要生成AccessToken、验证AccessToken等功能,它是OAuth受权中的很是重要的一环,在例子中受权服务器就是GitHub的服务器。浏览器
OAuth中:资源Owner、受权服务、客户端、资源服务有四个角色在使用GitHub登录Coding的例子中分别表示:缓存
OAuth2有三个重要的Endpoint其中受权 Endpoint、Token Endpoint结点在受权服务器中,还有一个可选的重定向 Endpoint在客户端中。
经过四个OAuth角色,应该对OAuth协议有一个大概的认识,不过可能仍是一头雾水不知道OAuth中的角色是如何交互的,不要紧继续往下看一下受权类型就知道OAuth中的角色是如何完成本身的职责,进一步对OAuth的理解。在OAuth中定义了四种受权类型,分别为:
这种形式就是咱们常见的受权形式(如使用GitHub帐号登录Coding),在整个受权流程中会有资源Owner、受权服务器、客户端三个OAuth角色参与,之因此叫作受权码受权是由于在交互流程中受权服务器会给客房端发放一个code,随后客房端拿着受权服务器发放的code继续进行受权如:请求受权服务器发放AccessToken。
为方便理解再将上图的内容带进真实的场景中,用文字表述一下整个流程:
D、Coding拿到code后,调用Github受权服务器API获取AccessToken,因为这一步是在Coding服务器后台作的浏览器中捕获不到,基本就是使用code访问github的access_token节点获取AccessToken;
以上是大体的受权码受权流程,大部分是客户端与受权服务器的交互,整个过程当中有几个参数说明以下:
在使用受权码受权的模式中,做为客户端请求受权的的时候都须要按规范请求,如下是使用受权码受权发起受权时所须要的参数 :
在这里插入图片描述
如使用Github登录Coding例子中的https://github.com/login/oauth/authorize?client_id=a5ce5a6c7e8c39567ca0&scope=user:email&redirect_uri=https://coding.net/api/oauth/github/callback&response_type=code受权请求URL,就有client_id、redirect_uri参数,至于为啥没有response_type在下猜测是由于Github给省了吧。
若是用户赞成受权,那受权服务器也会返回标准的OAuth受权响应:
在这里插入图片描述
如Coding登录中的https://coding.net/api/oauth/github/callback&response_type=code,用户赞成受权后Github受权服务器回调Coding的回调地址,同时返回code、state参数。
客房端凭证受权受权的过程当中只会涉及客户端与受权服务器交互,相比较其余三种受权类型是比较简单的。通常这种受权模式是用于服务之间受权,如在AWS中两台服务器分别为应用服务器(A)和数据服务器(B),A 服务器须要访问 B 服务器就须要经过受权服务器受权,而后才能去访问 B 服务器获取数据。
简单二步就能够完成客房端凭证受权啦,不过在使用客房端凭证受权时客户端是直接访问的受权服务器中获取AccessToken接口。
客房端凭证受权中客户端会直接发起获取AccessToken请求受权服务器的AccessTokenEndpoint,请求参数以下:
在这里插入图片描述
注意: 在OAuth中AccessTokenEndpoint是使用HTTP Basic认证,在请求时还须要携带Authorization请求头,如使用postman测试请求时:
其中的username和password参数对于OAuth协议中的client_id和client_secret,client_id和client_secret都是由受权服务器生成的。
客户端凭证受权响应
受权服务器验证完client_id和client_secret后返回token:
{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "example_parameter":"example_value" }
用户凭证受权与客户端凭证受权相似,不一样的地方是进行受权时要提供用户名和用户的密码。
基本流程以下:
用户凭证受权请求参数要比客户端凭证受权多username和pwssword参数:
注意: 获取Token时使用HTTP Basic认证,与客户端凭证受权同样。
用户凭证受权响应与客户端凭证受权差很少:
{ "access_token":"2YotnFZFEjr1zCsicMWpAA", "token_type":"example", "expires_in":3600, "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA", "example_parameter":"example_value" }
隐式受权用于获取AccessToken,可是获取的方式与用户凭证受权和客户端受权不一样的是,它是在访问受权Endpoint的时候就会获取AccessToken而不是访问Token Endpoing,并且AccessToken的会做为redirect_uri的Segment返回。
再使用隐式受权时,所须要请求参数以下:
在这里插入图片描述
隐式受权响应参数是经过redirect_uri回调返回的,如http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA &state=xyz&token_type=example&expires_in=3600就是隐式受权响应参数,其中须要注意的是响应的参数是使用Segment的形式的,而不是普通的URL参数。
在这里插入图片描述
前面提到过OAuth协议中有四个角色,这一节使用Spring Boot实现一个登录GitHub的OAuthClient,要使用OAuth2协议登录GitHub首先要云GitHub里面申请:
申请 OAuth App
OAuth Apps
填写必需的信息
在这里插入图片描述
上图中的Authorization callback URL就是redirect_uri用户赞成受权后GitHub会将浏览器重定向到该地址,所以先要在本地的OAuth客户端服务中添加一个接口响应GitHub的重定向请求。
熟悉OAuth2协议后,咱们在使用 Spring Security OAuth2 配置一个GitHub受权客户端,使用认证码受权流程(能够先去看一遍认证码受权流程图),示例工程依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
Spring Security OAuth2 默认集成了Github、Goolge等经常使用的受权服务器,由于这些经常使用的受权服务的配置信息都是公开的,Spring Security OAuth2 已经帮咱们配置了,开发都只须要指定必需的信息就行如:clientId、clientSecret。
Spring Security OAuth2使用Registration做为客户端的的配置实体:
public static class Registration { //受权服务器提供者名称 private String provider; //客户端id private String clientId; //客户端凭证 private String clientSecret; ....
下面是以前注册好的 GitHub OAuth App 的信息:
spring.security.oauth2.client.registration.github.clientId=5fefca2daccf85bede32 spring.security.oauth2.client.registration.github.clientSecret=01dde7a7239bd18bd8a83de67f99dde864fb6524``
Spring Security OAuth2内置了一个redirect_uri模板:{baseUrl}/login/oauth2/code/{registrationId},其中的registrationId
是在从配置中提取出来的:
spring.security.oauth2.client.registration.[registrationId].clientId=xxxxx
如在上面的GitHub客户端的配置中,由于指定的registrationId是github,因此重定向uri地址就是:
{baseUrl}/login/oauth2/code/github
OAuth2客户端和重定向Uri配置好后,将服务器启动,而后打开浏览器进入:http://localhost:8080/。第一次打开由于没有认证会将浏览器重客向到GitHub的受权Endpoint:
在这里插入图片描述
Spring Security OAuth2内置了一些经常使用的受权服务器的配置,这些配置都在CommonOAuth2Provider中:
public enum CommonOAuth2Provider { GOOGLE { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("openid", "profile", "email"); builder.authorizationUri("https://accounts.google.com/o/oauth2/v2/auth"); builder.tokenUri("https://www.googleapis.com/oauth2/v4/token"); builder.jwkSetUri("https://www.googleapis.com/oauth2/v3/certs"); builder.userInfoUri("https://www.googleapis.com/oauth2/v3/userinfo"); builder.userNameAttributeName(IdTokenClaimNames.SUB); builder.clientName("Google"); return builder; } }, GITHUB { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("read:user"); builder.authorizationUri("https://github.com/login/oauth/authorize"); builder.tokenUri("https://github.com/login/oauth/access_token"); builder.userInfoUri("https://api.github.com/user"); builder.userNameAttributeName("id"); builder.clientName("GitHub"); return builder; } }, FACEBOOK { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.POST, DEFAULT_REDIRECT_URL); builder.scope("public_profile", "email"); builder.authorizationUri("https://www.facebook.com/v2.8/dialog/oauth"); builder.tokenUri("https://graph.facebook.com/v2.8/oauth/access_token"); builder.userInfoUri("https://graph.facebook.com/me?fields=id,name,email"); builder.userNameAttributeName("id"); builder.clientName("Facebook"); return builder; } }, OKTA { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("openid", "profile", "email"); builder.userNameAttributeName(IdTokenClaimNames.SUB); builder.clientName("Okta"); return builder; } }; private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/code/{registrationId}"; }
CommonOAuth2Provider中有四个受权服务器配置:OKTA、FACEBOOK 、GITHUB 、GOOGLE。在OAuth2协议中的配置项redirect_uri、Token Endpoint、受权 Endpoint、scope都会在这里配置:
GITHUB { @Override public Builder getBuilder(String registrationId) { ClientRegistration.Builder builder = getBuilder(registrationId, ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL); builder.scope("read:user"); builder.authorizationUri("https://github.com/login/oauth/authorize"); builder.tokenUri("https://github.com/login/oauth/access_token"); builder.userInfoUri("https://api.github.com/user"); builder.userNameAttributeName("id"); builder.clientName("GitHub"); return builder; } }
脑瓜子有点蒙了,感受本身就配置了clientid和clientSecret一个OAuth2客户端就完成了,其中的一些起因还没搞明白啊。。。,最好奇的是重定向Uri是怎么被处理的。
Spring Security OAuth2 是基于 Spring Security 的,以前看过Spring Security文章,知道它的处理原理是基于过滤器的,若是你不知道的话推荐看这篇文章:《Spring Security 架构》。在源码中找了一下,发现一个可疑的Security 过滤器:
public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";
是一个匹配器,以前提到过Spring Security OAuth2中有一个默认的redirect_uri模板:{baseUrl}/{action}/oauth2/code/{registrationId},/login/oauth2/code/*正好能与redirect_uri模板匹配成功,因此OAuth2LoginAuthenticationFilter会在用户赞成受权后执行,它的构造方法以下:
public OAuth2LoginAuthenticationFilter(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) { this(clientRegistrationRepository, authorizedClientService, DEFAULT_FILTER_PROCESSES_URI); }
OAuth2LoginAuthenticationFilter 主要将受权服务器返回的code拿出来,而后经过AuthenticationManager 来认证(获取AccessToken),下来是移除部分代码后的源代码:
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap()); //检查没code与state if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) { OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } //获取 OAuth2AuthorizationRequest OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response); if (authorizationRequest == null) { OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } //取出 ClientRegistration String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID); ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); if (clientRegistration == null) { OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE, "Client Registration not found with Id: " + registrationId, null); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) .replaceQuery(null) .build() .toUriString(); //认证、获取AccessToken OAuth2AuthorizationResponse authorizationResponse = OAuth2AuthorizationResponseUtils.convert(params, redirectUri); Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request); OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken( clientRegistration, new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse)); authenticationRequest.setDetails(authenticationDetails); OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest); ... return oauth2Authentication; }
前面提到OAuth2LoginAuthenticationFilter是使用 AuthenticationManager 来进行OAuth2认证的,通常状况下在 Spring Security 中的 AuthenticationManager 都是使用的 ProviderManager 来进行认证的,因此对应在 Spring Security OAuth2 中有一个 OAuth2LoginAuthenticationProvider 用于获取AccessToken:
public class OAuth2LoginAuthenticationProvider implements AuthenticationProvider { private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient; private final OAuth2UserService<OAuth2UserRequest, OAuth2User> userService; private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities); .... @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication; // Section 3.1.2.1 Authentication Request - https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest // scope // REQUIRED. OpenID Connect requests MUST contain the "openid" scope value. if (authorizationCodeAuthentication.getAuthorizationExchange() .getAuthorizationRequest().getScopes().contains("openid")) { // This is an OpenID Connect Authentication Request so return null // and let OidcAuthorizationCodeAuthenticationProvider handle it instead return null; } OAuth2AccessTokenResponse accessTokenResponse; try { OAuth2AuthorizationExchangeValidator.validate( authorizationCodeAuthentication.getAuthorizationExchange()); //访问GitHub TokenEndpoint获取Token accessTokenResponse = this.accessTokenResponseClient.getTokenResponse( new OAuth2AuthorizationCodeGrantRequest( authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange())); } catch (OAuth2AuthorizationException ex) { OAuth2Error oauth2Error = ex.getError(); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } ... return authenticationResult; } @Override public boolean supports(Class<?> authentication) { return OAuth2LoginAuthenticationToken.class.isAssignableFrom(authentication); } }
-END-
架构文摘
ArchDigest
架构知识丨大型网站丨大数据丨机器学习若有收获,点个在看,诚挚感谢图片