文档章节

源码分析 - Spring Security OAuth2 生成 token 的执行流程

nimo10050
 nimo10050
发布于 06/29 22:51
字数 1267
阅读 19
收藏 0

精选30+云产品,助力企业轻松上云!>>>

说明

  1. 本文内容全部基于 Spring Security OAuth2(2.3.5.RELEASE).

  2. OAuth2.0 有四种授权模式, 本文会以 密码模式 来举例讲解源码.

  3. 阅读前, 需要对 OAuth2.0 的相关概念有所了解.

  4. 最好有 Spring Security OAuth 框架的使用经验

下面是前面写的 OAuth2.0 相关文章

结合第三方登录案例理解 OAuth2.0 授权码方式

spring security oauth2 实战(仿微博第三方登录) - 工程搭建及登陆流程

正文

前置知识

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(
Principal principal, 
@RequestParam Map<String, String> parameters) 
	throws HttpRequestMethodNotSupportedException {
	// TODO
}
  1. 获取 token 的默认请求路径是 /oauth/token
  2. 获取 token 的入口类是 TokenEndpoint
  3. 获取 token 的接口需要接收两个参数
    • Principal principal
    • Map<String, String> parameters

执行流程

第一步:

调用 ClientDetailsService 类的 loadClientByClientId 方法, 获取客户端信息装载到 ClientDetails 对象中

  • ClientDetailsService 用来管理客户端信息
    • 实现1: InMemoryClientDetailsService (把客户端信息放在内存中)

    • 实现2: JdbcClientDetialsService (把客户端信息放在数据库中)

第二步:

调用 OAuth2RequestFactory 类生成 TokenRequest 对象

  • DefaultOAuth2RequestFactoryOAuth2RequestFactory 的唯一实现
* `TokenRequest` 是对请求参数 `parameters` 和 `ClientDetails` 属性的封装

第三步:

调用 TokenGranter, 利用 TokenRequest 产生两个对象 OAuth2RequestAuthentication

TokenGranter 是对 4 种授权模式的一个封装。它会根据 grant_type 参数去挑一个具体的实现来生成令牌

部分实现类如下:

* ResourceOwnerPasswordTokenGranter 
* AuthorizationCodeTokenGranter
* ImplicitTokenGranter
* ClientCredentialsTokenGranter

第四步:

OAuth2RequestAuthorization 两个对象组合起来形成一个 OAuth2Authorization 对象,它的里面包含了:

  • 哪个第三方应用在请求 token
  • 哪个用户以哪种授权模式进行授权

第五步: 将第 4 步 的对象会传递到 AuthorizationServerTokenServices 的实现类DefaultTokenServices 中,最终会生成一个OAuth2AccessToken

源码分析

1. TokenEndpoint#postAccessToken()

// 从请求参数中解析出 clientId
String clientId = this.getClientId(principal);

// 第一步: 从 内存 or 数据库(根据 ClientDetailsService 的具体实现)中取出客户端的详细信息
ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
// 第二步: 调用 `OAuth2RequestFactory` 类生成 `TokenRequest` 对象
TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

// 省略一堆判断

// 第3-5步: 根据不同的授权方式, 生成 token
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
    throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
} else {
    return this.getResponse(token);
}

针对上述 第 3-5 步的源码接着分析:

OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

假设现在使用的是授权码模式-密码模式, 那么this.getTokenGranter() 返回的结果就是ResourceOwnerPasswordTokenGranter.

对应的 grant()方法调用的是 CompositeTokenGranter的 grant()方法

2. CompositeTokenGranter#grant()

    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		for (TokenGranter granter : tokenGranters) {
			OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
			if (grant!=null) {
				return grant;
			}
		}
		return null;
	}

CompositeTokenGranter 中有一个集合,这个集合里装的就是五个会产生令牌的操作。

在遍历过程中, 通过 grant_type 在五种情况中挑一种生成 accessToken 对象。

3. AbstractTokenGranter#grant

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		// 判断参数传来的的授权类型和该类所支持的授权类型是否一致
59到第63行是
		if (!this.grantType.equals(grantType)) {
			return null;
		}
		//获取客户端信息跟授权类型再做一个校验
		String clientId = tokenRequest.getClientId();
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		validateGrantType(grantType, client);
		return getAccessToken(client, tokenRequest);

	}

4. AbstractTokenGranter #getAccessToken()


protected OAuth2AccessToken getAccessToken(ClientDetails  client, TokenRequest tokenRequest) {
// 调用 ResourceOwnerPasswordTokenGrante的getOAuth2Authentication方法
        return this.tokenServices.createAccessToken(this.getOAuth2Authentication(client, tokenRequest));
}

5. ResourceOwnerPasswordTokenGranter#getOAuth2Authentication()

在密码模式中的策略:

  1. 根据请求中携带的用户名和密码来获取当前用户的授权信息 Authentication
  2. OAuth2RequestAuthentication 组合一个 OAuth2Authentication 对象
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        // 从 TokenRequest 中获取请求参数
        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        // 用户名和密码
        String username = (String)parameters.get("username");
        String password = (String)parameters.get("password");
        parameters.remove("password");
        
        // 构造一个 Authentication 对象
        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
        ((AbstractAuthenticationToken)userAuth).setDetails(parameters);

        Authentication userAuth;
        try {
        // 把 userAuth 传递给authenticationManager做认证
        // 其实就是调用 自定义的UserDetailService 的 loadUserByUsername 方法去校验用户
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch (AccountStatusException var8) {
            throw new InvalidGrantException(var8.getMessage());
        } catch (BadCredentialsException var9) {
            throw new InvalidGrantException(var9.getMessage());
        }

        if (userAuth != null && userAuth.isAuthenticated()) {
            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
            // 通过 OAuth2Request 构造一个 OAuth2Authentication 对象
            return new OAuth2Authentication(storedOAuth2Request, userAuth);
        } else {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }
    }

6. DefaultTokenServices#createAccessToken

// 从 tokenStore 取出 token
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
	
if (existingAccessToken != null) {
    // 如果 token 过期
    if (existingAccessToken.isExpired()) {
				
	    if (existingAccessToken.getRefreshToken() != null) {
	        // 移除 refresh token
		    refreshToken = existingAccessToken.getRefreshToken();			
			tokenStore.removeRefreshToken(refreshToken);
		}
		// // 移除 token
		tokenStore.removeAccessToken(existingAccessToken);
   } else {
		// 如果token 没过期, 就刷新有效期, 返回 token
		tokenStore.storeAccessToken(existingAccessToken, authentication);
		return existingAccessToken;
	}
}

	
if (refreshToken == null) {
	refreshToken = createRefreshToken(authentication);
} else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
	ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
	if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
		refreshToken = createRefreshToken(authentication);
	}
}
// 创建新的 token, 并返回
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
	tokenStore.storeRefreshToken(refreshToken, authentication);
}

return accessToken;

总结

密码模式获取 token 的流程就是把请求的参数 比如 clientId, secret, grant_type, username, password 等信息, 通过/oauth/token 接口传到后端, 经过下图中的一系列转换得到一个 OAuth2AccessToken 对象

最终获得如下 json

{
     "scope": "[all, read, write]",
     "code": 0,
     "access_token": "71561b3d-73d5-4a91-bf0f-456c9dc84d7d",
     "token_type": "bearer",
     "refresh_token": "b888d6d7-5ec2-47f9-82fe-eca5a0350770",
     "expires_in": 7199
}

在这里插入图片描述

nimo10050
粉丝 16
博文 78
码字总数 85152
作品 0
杭州
程序员
私信 提问
加载中
请先登录后再评论。

暂无文章

SequoiaDB监控与开发实践分析

使用背景 公司近期上线了一个新应用,底层数据库采用了国产的分布式数据库 – SequoiaDB。 因为需要将 SequoiaDB 集群纳入到公司的整个监控体系中,所以需要对 SequoiaDB 的状态、性能指标等...

巨杉数据库
33分钟前
6
0
如何导入其他Python文件? - How to import other Python files?

问题: How do I import other files in Python? 如何在Python中导入其他文件? How exactly can I import a specific python file like import file.py ? 我究竟该如何导入特定的python文件......

fyin1314
41分钟前
22
0
小程序上传图片 返回的地址出现回车空格问题

不知怎么回事 ,今天写小程序上传图片 之前是没问题的,今天突然出现很多回车空格问题 那怎么办呢,处理呗 //去掉空格str = str.replace(/\ +/g,""); console.log(str);//"{'retmsg':'suc......

子枫Eric
51分钟前
6
0
Spring Boot + Spring Security自定义用户认证

自定义认证过程 自定义认证的过程需要实现Spring Security提供的UserDetailService接口 ,源码如下: public interface UserDetailsService { UserDetails loadUserByUsername(String use...

心田已荒
今天
12
0
DateTime2与SQL Server中的DateTime - DateTime2 vs DateTime in SQL Server

问题: Which one: 哪一个: datetime datetime2 is the recommended way to store date and time in SQL Server 2008+? 是在SQL Server 2008+中存储日期和时间的推荐方法吗? I'm aware of......

富含淀粉
今天
13
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部