# Oauth2

返回:spring security

OAuth 的核心就是向第三方应用颁发令牌

# 授权映射地址

TIP

/oauth/token

# 授权方式(authorization grant)

# authorizationCode

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

//b.com/oauth/authorize?
https: response_type = code & client_id = CLIENT_ID &
redirect_uri = CALLBACK_URL & scope = read;
1
2
3
  • response_type 参数表示要求返回授权码(code),
  • client_id 参数让 B 知道是谁在请求,
  • redirect_uri 参数是 B 接受或拒绝请求后的跳转网址,
  • scope 参数表示要求的授权范围(这里是只读)。

B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回 redirect_uri 参数指定的网址。跳转时,会传回一个授权码,就像下面这样。

https://a.com/callback?code=AUTHORIZATION_CODE
1

A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。

//b.com/oauth/token?
https: client_id = CLIENT_ID & client_secret = CLIENT_SECRET &
grant_type = authorization_code & code = AUTHORIZATION_CODE &
redirect_uri = CALLBACK_URL;
1
2
3
4
  • client_id 参数和 client_secret 参数用来让 B 确认 A 的身份(client_secret 参数是保密的,因此只能在后端发请求)
  • grant_type 参数的值是 AUTHORIZATION_CODE,表示采用的授权方式是授权码,code 参数是上一步拿到的授权码,
  • redirect_uri 参数是令牌颁发后的回调网址。

B 网站收到请求以后,就会颁发令牌。具体做法是向 redirect_uri 指定的网址,发送一段 JSON 数据。

{
  "access_token":"ACCESS_TOKEN",
  "token_type":"bearer",
  "expires_in":2592000,
  "refresh_token":"REFRESH_TOKEN",
  "scope":"read",
  "uid":100101,
  "info":{...}
}
1
2
3
4
5
6
7
8
9

# implicit

有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。

A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。

//b.com/oauth/authorize?
https: response_type = token & client_id = CLIENT_ID &
redirect_uri = CALLBACK_URL & scope = read;
1
2
3

用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回 redirect_uri 参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。

https://a.com/callback#token=ACCESS_TOKEN
1

WARNING

令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。

这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

# password

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。

//b.com/oauth/token?
https: grant_type = password & username = USERNAME & password = PASSWORD &
client_id = CLIENT_ID;
1
2
3

# credentials

适用于没有前端的命令行应用,即在命令行下请求令牌

//b.com/oauth/token?
https: grant_type = client_credentials & client_id = CLIENT_ID &
client_secret = CLIENT_SECRET;
1
2
3
  • grant_type 参数等于 client_credentials 表示采用凭证式
  • client_id 和 client_secret 用来让 B 确认 A 的身份。

# 更新令牌

B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。

//b.com/oauth/token?
https: grant_type = refresh_token & client_id = CLIENT_ID &
client_secret = CLIENT_SECRET & refresh_token = REFRESH_TOKEN;
1
2
3

# Spring Security Oauth2 认证(获取 token/刷新 token)流程(password 模式)

# 获取 access_token 请求(/oauth/token)

http://localhost/oauth/token?client_id=demoClientId&client_secret=demoClientSecret&grant_type=password&username=demoUser&password=50575tyL86xp29O380t1
1
  1. 用户发起获取 token 的请求。
  2. 过滤器会验证 path 是否是认证的请求/oauth/token,如果为 false,则直接返回没有后续操作。
  3. 过滤器通过 clientId 查询生成一个 Authentication 对象。
  4. 然后会通过 username 和生成的 Authentication 对象生成一个 UserDetails 对象,并检查用户是否存在。
  5. 以上全部通过会进入地址/oauth/token,即 TokenEndpoint 的 postAccessToken 方法中。
  6. postAccessToken 方法中会验证 Scope,然后验证是否是 refreshToken 请求等。
  7. 之后调用 AbstractTokenGranter 中的 grant 方法。
  8. grant 方法中调用 AbstractUserDetailsAuthenticationProvider 的 authenticate 方法,通过 username 和 Authentication 对象来检索用户是否存在。
  9. 然后通过 DefaultTokenServices 类从 tokenStore 中获取 OAuth2AccessToken 对象。 10.然后将 OAuth2AccessToken 对象包装进响应流返回。

一个比较重要的过滤器

spring-security-web:org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;

p

org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter

p

org.springframework.security.authentication.ProviderManager

p

org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider

p

org.springframework.security.authentication.dao.DaoAuthenticationProvider

p p p

进入/oauth/token映射的TokenEndpoint类的postAccessToken方法

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(
        Principal principal, @RequestParam Map<String, String> parameters)
        throws HttpRequestMethodNotSupportedException {

    if (!(principal instanceof Authentication)) {
        throw new InsufficientAuthenticationException(
                "There is no client authentication. Try adding an appropriate authentication filter.");
    }

// 获取ClientDetails
    String clientId = getClientId(principal);
    ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

    TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

    // Only validate client details if a client is authenticated during this request.
    // Double check to make sure that the client ID is the same in the token request and authenticated client.
    if (StringUtils.hasText(clientId) && !clientId.equals(tokenRequest.getClientId())) {
        throw new InvalidClientException("Given client ID does not match authenticated client");
    }
// 验证scope
    if (authenticatedClient != null) {
        oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
    }

    if (!StringUtils.hasText(tokenRequest.getGrantType())) {
        throw new InvalidRequestException("Missing grant type");
    }

    if (tokenRequest.getGrantType().equals("implicit")) {
        throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
    }

    if (isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
        // The scope was requested or determined during the authorization step
        logger.debug("Clearing scope of incoming token request");
        tokenRequest.setScope(Collections.<String>emptySet());
    } else if (isRefreshTokenRequest(parameters)) {
        if (StringUtils.isEmpty(parameters.get("refresh_token"))) {
            throw new InvalidRequestException("refresh_token parameter not provided");
        }
        // A refresh token has its own default scopes, so we should ignore any added by the factory here.
        tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
    }
// 调用AbstractTokenGranter
    OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
    if (token == null) {
        throw new UnsupportedGrantTypeException("Unsupported grant type");
    }
// 包装结果数据
    return getResponse(token);
}
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
// AbstractTokenGranter
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

    if (!this.grantType.equals(grantType)) {
        return null;
    }
    
    String clientId = tokenRequest.getClientId();
    ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
    validateGrantType(grantType, client);

    if (logger.isDebugEnabled()) {
        logger.debug("Getting access token for clientId");
    }

    return getAccessToken(client, tokenRequest);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter

protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

    Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
    String username = parameters.get("username");
    String password = parameters.get("password");
    // Protect from downstream leaks of password
    parameters.remove("password");

    Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
    ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
    try {
        // 使用Provider类遍历所有的provider来验证用户名密码
        userAuth = authenticationManager.authenticate(userAuth);
    }
    catch (AccountStatusException ase) {
        //covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
        throw new InvalidGrantException(ase.getMessage());
    }
    catch (BadCredentialsException e) {
        // If the username/password are wrong the spec says we should send 400/invalid grant
        throw new InvalidGrantException(e.getMessage());
    }
    catch (UsernameNotFoundException e) {
        // If the user is not found, report a generic error message
        throw new InvalidGrantException("username not found");
    }
    if (userAuth == null || !userAuth.isAuthenticated()) {
        throw new InvalidGrantException("Could not authenticate user");
    }
    
    OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
    return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
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

# 检查头肯是否有效请求(/oauth/check_token)

http://localhost/oauth/check_token?token=f57ce129-2d4d-4bd7-1111-f31ccc69d4d1
1

# 刷新 token 请求(/oauth/token)

http://localhost/oauth/token?grant_type=refresh_token&refresh_token=fbde81ee-f419-42b1-1234-9191f1f95be9&client_id=demoClientId&client_secret=demoClientSecret
1

刷新 token(refresh token)的流程与获取 token 的流程只有有所区别:

  • 获取 token 调用的是 AbstractTokenGranter 中的 getAccessToken 方法,然后调用 tokenStore 中的 getAccessToken 方法获取 token。
  • 刷新 token 调用的是 RefreshTokenGranter 中的 getAccessToken 方法,然后使用 tokenStore 中的 refreshAccessToken 方法获取 token。

# tokenStore 的特点

tokenStore 通常情况为自定义实现,一般放置在缓存或者数据库中。此处可以利用自定义 tokenStore 来实现多种需求,如:

  1. 同已用户每次获取 token,获取到的都是同一个 token,只有 token 失效后才会获取新 token。
  2. 同一用户每次获取 token 都生成一个完成周期的 token 并且保证每次生成的 token 都能够使用(多点登录)。
  3. 同一用户每次获取 token 都保证只有最后一个 token 能够使用,之前的 token 都设为无效(单点 token)。