OAuth2流程分析笔记

oauth2笔记

引言

2020年在公司做devops,属于内部系统,工作中使用到spring-security和oauth2流程,之前分析过spring-security,后面深入了oauth2,发现oauth2流程有些复杂,因此学习和记录下。

OAuth2 简介

OAuth 2.0是用于授权的行业标准协议。OAuth 2.0为简化客户端开发提供了特定的授权流,包括Web应用、桌面应用、移动端应用等。

OAuth2 相关名词解释

Resource owner(资源拥有者):拥有该资源的最终用户,他有访问资源的账号密码;
Resource server(资源服务器):拥有受保护资源的服务器,如果请求包含正确的访问令牌,可以访问资源;
Client(客户端):访问资源的客户端,会使用访问令牌去获取资源服务器的资源,可以是浏览器、移动设备或者服务器;
Authorization server(认证服务器):用于认证用户的服务器,如果客户端认证通过,发放访问资源服务器的令牌。

举例如下:

公司有sso系统,使用的是oauth2规范,这个就是认证服务器

xxx-server系统给公司内部业务人员使用,用户通过sso登录xxx-server,然后内部人员可以xxx-server的数据,xxx-server这个系统就是资源服务器。(sso的作用是公司有许多系统,只需要登录任何系统,在其它系统就不需要再登录了)

业务人员每个都配有用户密码,业务人员就是资源拥有者

客户端就是用户通过什么工具访问服务器资源,自然是通过浏览器了。 浏览器就是客户端

四种授权模式

Authorization Code(授权码模式):正宗的OAuth2的授权模式,客户端先将用户导向认证服务器,登录后获取授权码,然后进行授权,最后根据授权码获取访问令牌;

Resource Owner Password Credentials(密码模式):客户端直接向用户获取用户名和密码,之后向认证服务器获取访问令牌;

Implicit(简化模式):和授权码模式相比,取消了获取授权码的过程,直接获取访问令牌;

Client Credentials(客户端模式):客户端直接通过客户端认证(比如client_id和client_secret)从认证服务器获取访问令牌。

常用的两种授权模式流程

比如用户要使用的系统是Nebula,认证服务器是sso

密码模式

1.浏览器访问应用系统

2.应用系统spring-security判断用户未登录,重定向到sso登录页面返回前端

3.用户输入用户密码,请求sso验证和下发token

4.sso验证用户密码正确,下发token给前端

5.用户请求携带token正常访问应用系统资源

OAuth2流程分析笔记

对于原生的spring-security-oauth2来说,用户未登录,被Nebula重定向到登录页面,用户输入用户密码,请求直接转发给spring-security-oauth2内部TokenEndpoint的/oauth/token接口处理,该接口内会验证用户密码正确和下发token。

授权码模式

OAuth2流程分析笔记

1.浏览器访问应用系统https://nebula.com/order

2.应用系统spring-security判断用户未登录,组装重定向url重定向到sso登录页面rediect https://connect.sso.com/oauth/authorize?client_id=nebula&redirect_uri=http://nebula.com/gettoken&response_type=code&scope=all&state=normal

3.用户输入用户密码,请求sso验证和下发授权code

4.如果非autoapprove,则需要forward:/oauth/confirm_access,用户点击授权(如果配置了autoapprove为true,则没有这个步骤)

5.SSO重定向到url中携带的重定向地址,url带code,http://nebula.com/gettoken?code=eTsADY&state=normal

6.应用系统根据sso重定向回来携带的code,到sso申请token,https://connect.sso.com/oauth/token?code=eTsADY&client_id=nebula&grant_type=authorization_code&scope=all

7.sso下发token给应用系统

8.应用系统返回token并重定向前端首页地址

10.用户请求携带token正常访问应用系统资源

对比授权码和密码模式,发现授权码模式多了个获取code的过程,授权码模式主要是通过redirect来实现的,而且不好调试,我也是专门学习和使用了findder抓包才清楚。

spring-secutiry-oauth2默认对两种方式生成token的源码分析

无论是密码模式还是授权码模式,都最终会去申请token,先分析spring-security-oauth2默认的获取token的接口/oauth/token,该接口在org.springframework.security.oauth2.provider.endpoint.TokenEndpoint

@FrameworkEndpoint	//当做Controller理解即可
public class TokenEndpoint extends AbstractEndpoint {

	// /oauth/token接口支持get和post,get请求也是调用post请求处理的,因此只分析post请求的方法即可
	
	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {//代码@1

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

		String clientId = getClientId(principal);//代码@2
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);//代码@3

		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);//代码@4
		
        //省略校验代码

		if (isAuthCodeRequest(parameters)) {//代码@5
			// The scope was requested or determined during the authorization step
			if (!tokenRequest.getScope().isEmpty()) {
				logger.debug("Clearing scope of incoming token request");
				tokenRequest.setScope(Collections.<String> emptySet());
			}
		}

		if (isRefreshTokenRequest(parameters)) {//代码@6
			// 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)));
		}

		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);//代码@7
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}

		return getResponse(token);//代码@8

	}
    //省略其它代码
}

代码分析如下:

代码@1:入参principal是UsernamePasswordAuthenticationToken认证对象(认证的是client而非用户密码),入参parameters是请求参数。比如对于授权码模式请求参数是:client_id、client_secret、code、redirect_uri、grant_type。对于密码模式请求参数是用户、密码、授权类型等信息,比如username=admin,password=123456,grant_type=password。

代码@2:获取client_id,即比如是Nebula。

代码@3:根据client_id从数据源(通常默认是查询数据库的oauth_client_details表)获取client_id信息对象ClientDetails。oauth_client_details表是spring-security-oauth2的默认客户端表。

代码@4:创建TokenRequest对象,该对象封装了请求参数,client_id,scope,grant_type。

代码@5:授权码模式,清除scope。授权码模式在获取token的时候,不需要设置scope。

代码@6:grant_type是refresh_token,只设置scope为请求上送的scope,忽略掉框架自动添加的scope。

代码@7:生成token对象OAuth2AccessToken,核心逻辑,重点分析

代码@8:返回生成的token令牌,通常令牌都是使用的jwt。

下面分析代码@7生成token的核心逻辑,使用TokenGranter进行授权,该接口设计使用了模板模式,抽象类是AbstractTokenGranter,默认实现有

授权码模式授权 AuthorizationCodeTokenGranter

密码模式授权 ResourceOwnerPasswordTokenGranter

客户端模式授权 ClientCredentialsTokenGranter

简化模式授权 ImplicitTokenGranter

刷新token授权 RefreshTokenGranter

对应了四种不同的授权方式

其中CompositeTokenGranter是组合模式,组合了四种不同的授权码模式,代码@7执行授权就是调用的CompositeTokenGranter#grant(String, TokenRequest),代码如下

//org.springframework.security.oauth2.provider.CompositeTokenGranter.grant(String, TokenRequest)
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
    for (TokenGranter granter : tokenGranters) {//tokenGranters就是四种授权码模式的具体实现
        OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);//选择对应的grant_type实现来进行授权
        if (grant!=null) {
            return grant;
        }
    }
    return null;
}

上述代码使用对应的grant_type实现来进行授权,密码模式的实现是ResourceOwnerPasswordTokenGranter,接着看授权代码

//模板模式,执行抽象类的公共方法
//AbstractTokenGranter#grant(String, TokenRequest)
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
    if (!this.grantType.equals(grantType)) {//grant_type不同,则不执行
        return null;
    }

    String clientId = tokenRequest.getClientId();//获取client_id
    ClientDetails client = clientDetailsService.loadClientByClientId(clientId);//从数据库根据client_id查询oauth_client_details获取client_id对象。
    validateGrantType(grantType, client);//oauth_client_details表内当前client_id没配置authorized_grant_types,抛出异常

    return getAccessToken(client, tokenRequest);//获取token核心方法

}
//AbstractTokenGranter#getAccessToken(ClientDetails, TokenRequest)
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
    return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));//代码@8
}

代码@8:先看getOAuth2Authentication,再看createAccessToken创建token。

授权类型是密码模式

ResourceOwnerPasswordTokenGranter重写了getOAuth2Authentication方法(protected方法,本来就是默认让用户重写的),代码如下

//ResourceOwnerPasswordTokenGranter#getOAuth2Authentication(ClientDetails, TokenRequest)
@Override
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);//代码@9,创建认证对象
    ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
    try {
        userAuth = authenticationManager.authenticate(userAuth);//代码@10 认证,即验证用户密码,返回认证通过的对象
    }
    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());
    }
    if (userAuth == null || !userAuth.isAuthenticated()) {
        throw new InvalidGrantException("Could not authenticate user: " + username);
    }

    OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);	//代码@11	创建OAuth2Request
    return new OAuth2Authentication(storedOAuth2Request, userAuth);//代码@12
}

代码@9:封装用户名和密码,创建认证对象UsernamePasswordAuthenticationToken[false],此时认证对象还未认证通过。

代码@10:这里就是验证用户密码,根据用户名查询数据库,看密码是否匹配。具体执行方法是ProviderManager.authenticate(Authentication) -> AbstractUserDetailsAuthenticationProvider.authenticate(Authentication) -> DaoAuthenticationProvider.retrieveUser(String, UsernamePasswordAuthenticationToken) -> UserDetailsService.loadUserByUsername(String),其中UserDetailsService是接口,我们需要实现该接口,从数据库查询用户信息并匹配密码是否正确。验证通过,返回认证通过的对象UsernamePasswordAuthenticationToken[true]。

代码@11:创建oauth2请求对象,该对象封装了client_id信息对象ClientDetails和token请求对象TokenRequest

代码@12:创建认证对象OAuth2Authentication,该对象封装了认证对象和oauth2请求。

授权类型是授权码模式

AuthorizationCodeTokenGranter重写了getOAuth2Authentication方法,代码如下:

//AuthorizationCodeTokenGranter.getOAuth2Authentication(ClientDetails, TokenRequest)

@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {//

    //对于授权码模式获取token的请求url通常如下:http://xxx/oauth/token?client_id=xxx&client_secret=xxx&code=xxx&redirect_uri=xxx&grant_type=xxx,因此参数就是?后面的
    Map<String, String> parameters = tokenRequest.getRequestParameters();//获取请求参数
    String authorizationCode = parameters.get("code");//获取请求code
    String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);//redirect_uri是登录前访问的地址,获取token后要重定向到该地址

    if (authorizationCode == null) {
        throw new InvalidRequestException("An authorization code must be supplied.");
    }

    OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);//核心方法。根据code从表oauth_code中获取用户登录认证信息
    if (storedAuth == null) {
        throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
    }

    OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
    // https://jira.springsource.org/browse/SECOAUTH-333
    // This might be null, if the authorization was done without the redirect_uri parameter
    String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
        OAuth2Utils.REDIRECT_URI);

    if ((redirectUri != null || redirectUriApprovalParameter != null)
        && !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {//比较redirect_uri,认证和访问token的redirect_uri必须相同,防止篡改
        throw new RedirectMismatchException("Redirect URI mismatch.");
    }

    String pendingClientId = pendingOAuth2Request.getClientId();
    String clientId = tokenRequest.getClientId();
    if (clientId != null && !clientId.equals(pendingClientId)) {//比较client_id,认证和访问token的client_id必须相同,防止篡改
        // just a sanity check.
        throw new InvalidClientException("Client ID mismatch");
    }

    // Secret is not required in the authorization request, so it won't be available
    // in the pendingAuthorizationRequest. We do want to check that a secret is provided
    // in the token request, but that happens elsewhere.

    Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request
                                                                         .getRequestParameters());
    // Combine the parameters adding the new ones last so they override if there are any *es
    combinedParameters.putAll(parameters);//合并请求参数和认证时候的请求参数

    // Make a new stored request with the combined parameters
    OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);//重写创建OAuth2Request对象,该对象除了参数,其余属性和认证时候生成的OAuth2Request完全相同。

    Authentication userAuth = storedAuth.getUserAuthentication();//用户认证信息UsernamePasswordAuthenticationToken

    return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);//最后创建认证对象OAuth2Authentication返回

}

核心方法是authorizationCodeServices.consumeAuthorizationCode(authorizationCode),根据code从数据库表oauth_code查询获取Authentication,该对象包装了用户的认证信息。

对比密码模式和授权码模式getOAuth2Authentication区别

密码模式是是直接获取token步骤进行验证用户密码。

授权码模式在获取token步骤之前的/oauth/authorize已经验证了用户密码,只是用code的值表示用户认证结果。

相同之处都是认证后,先创建tokenrequest,再创建OAuth2Request,然后再创建OAuth2Authentication。流程还是比较相同的。

密码和授权码模式创建access_token共性处

接着看创建token逻辑createAccessToken,源码如下

//DefaultTokenServices#createAccessToken(OAuth2Authentication)
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

    OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);//代码@13 tokenStore表示token的存储方式,有内存、jdbc、jwt、redis等方式
    OAuth2RefreshToken refreshToken = null;
    if (existingAccessToken != null) {//代码@14
        if (existingAccessToken.isExpired()) {
            if (existingAccessToken.getRefreshToken() != null) {
                refreshToken = existingAccessToken.getRefreshToken();
                // The token store could remove the refresh token when the
                // access token is removed, but we want to
                // be sure...
                tokenStore.removeRefreshToken(refreshToken);
            }
            tokenStore.removeAccessToken(existingAccessToken);
        }
        else {
            // Re-store the access token in case the authentication has changed
            tokenStore.storeAccessToken(existingAccessToken, authentication);
            return existingAccessToken;
        }
    }

    // Only create a new refresh token if there wasn't an existing one
    // associated with an expired access token.
    // Clients might be holding existing refresh tokens, so we re-use it in
    // the case that the old access token
    // expired.
    if (refreshToken == null) {
        refreshToken = createRefreshToken(authentication);//代码@15
    }
    // But the refresh token itself might need to be re-issued if it has
    // expired.
    else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
        ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
        if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
            refreshToken = createRefreshToken(authentication);
        }
    }

    OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);//代码@16
    tokenStore.storeAccessToken(accessToken, authentication);
    // In case it was modified
    refreshToken = accessToken.getRefreshToken();
    if (refreshToken != null) {
        tokenStore.storeRefreshToken(refreshToken, authentication);
    }
    return accessToken;//代码@17

}

代码@13:根据OAuth2Authentication从token存储源获取token对象。 token存储源有内存、jdbc、jwt、redis等。使用jwt的话,基本都是使用redis存储token。

代码@14:这部分整体逻辑是存储的access_token存在且未过期的话,直接返回存储的access_token;如果token过期了,则继续向下执行,生成access_token。

代码@15:创建refresh_token对象,即DefaultExpiringOAuth2RefreshToken,该对象本质就是包装了一个uuid和一个预计过期时间。

代码@16:创建access_token对象DefaultOAuth2AccessToken,本质就是包装了一个uuid和过期时间,同时也会对access_token进行增强。核心逻辑。

代码@17:存储access_token并返回。

创建access_token对象代码如下:

//DefaultTokenServices.createAccessToken(OAuth2Authentication, OAuth2RefreshToken)
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
    DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());//创建access_token对象DefaultOAuth2AccessToken,本质就是包装了一个uuid字符串。
    int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
    if (validitySeconds > 0) {
        token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));//设置access_token过期时间
    }
    token.setRefreshToken(refreshToken);//把refresh_token保存到access_token
    token.setScope(authentication.getOAuth2Request().getScope());//设置access_token的scope

    return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;//代码@18 对access_token增强
}

代码@18:是对access_token进行增强,通常使用jwt的话,都会对access_token进行增强,比如在jwt内加入一些额外的用户信息等。

access_token增强逻辑如下

//TokenEnhancerChain.enhance(OAuth2AccessToken, OAuth2Authentication)
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
    OAuth2AccessToken result = accessToken;
    for (TokenEnhancer enhancer : delegates) {//代码@19
        result = enhancer.enhance(result, authentication);//执行token增强
    }
    return result;
}

代码@19:spring-oauth2默认的jwt token增强是JwtAccessTokenConverter,用于把token的value进行编码转换为jwt形式。

总结下access_token生成的流程:

OAuth2流程分析笔记

从上图总结可以看出,创建access_token需要的 几个必要接口/类:ClientDetailsService 、ClientDetails 、 UserDetailsService、UserDetails、ClientDetailsUserDetailsService、TokenRequest 、OAuth2Request、 Authentication、AuthorizationServerTokenServices、OAuth2Authentication,不去搞明白这些类之间关系,感觉很乱。

UserDetailsService接口只有默认一个loadUserByUsername方法,根据用户名查询数据库用户表,获取用户信息UserDetails。

ClientDetailsService 接口只有默认一个loadClientByClientId方法,根据client_id查询数据库oauth_client_details表,获取客户端信息ClientDetails 。

ClientDetailsUserDetailsService类,虽然是实现的UserDetailsService,但是持有ClientDetailsService,因此获取结果却是ClientDetails 。这个类很容易迷惑我们。

这几个类图关系

OAuth2流程分析笔记

对于ClientDetailsService 、 UserDetailsService、AuthorizationServerTokenServices (默认实现类DefaultTokenServices token生成器)我们可以直接从Spring 容器中获取(启动阶段就赋值)。

根据client_id获取ClientDetails,有了 ClientDetails 就有了 TokenRequest,有了 TokenRequest 和 Authentication(认证后肯定有的) 就有了 OAuth2Authentication ,有了OAuth2Authentication 就能够生成 OAuth2AccessToken。

OAuth2流程分析笔记

几个容易混淆的对象区别:

token的创建涉及到TokenRequest、OAuth2Request,UsernamePasswordAuthenticationToken、OAuth2Authentication,这几个也容易混淆。TokenRequest封装了client_id和grant_type

OAuth2Request除了封装了client_id和grant_type,还持有client的authorities,和TokenRequest相比,只是不同阶段的对象而已,封装的属性不同,两者都extends BaseRequest

OAuth2流程分析笔记

UsernamePasswordAuthenticationToken表示用户通过认证后的对象(理解为封装了用户和密码信息)

OAuth2Authentication封装了用户认证对象UsernamePasswordAuthenticationToken和OAuth2Request,表示用户和client都通过认证后的对象。

OAuth2流程分析笔记

疑问:/oauth/token接口生成token,参数Principal是怎么来的呢?

OAuth2流程分析笔记

从前面的代码分析说明入参Principal是client的认证对象Authentication即UsernamePasswordAuthenticationToken,该接口是client请求认证,那么client请求获取token的时候上送client_id和client_secret在哪里进行认证的呢?答案是在FilterChainProxy内的ClientCredentialsTokenEndpointFilter进行对client认证(判断client_id和client_secret是否匹配),生成认证对象UsernamePasswordAuthenticationToken,该对象就是/oauth/token接口的参数principal。因为UsernamePasswordAuthenticationToken implements Authentication extends Principal。

ClientCredentialsTokenEndpointFilter用于拦截/oauth/token请求,对client进行认证。

那么还有问题,ClientCredentialsTokenEndpointFilter是如何引入的呢?答案在@EnableAuthorizationServer引入配置类AuthorizationServerSecurityConfiguration,该配置类是个WebSecurityConfigurerAdapter类型,在执行创建FilterChainProxy的时候,执行AuthorizationServerSecurityConfigurer#configure(HttpSecurity http),最终是在AuthorizationServerSecurityConfigurer#clientCredentialsTokenEndpointFilter(HttpSecurity)内创建ClientCredentialsTokenEndpointFilter并加入到FilterChainProxy。

看完源码后总结

密码模式流程

密码模式用户认证和token生成都在一个请求/oauth/token,流程如下

OAuth2流程分析笔记

授权码模式生成token流程

授权码模式token生成和密码模式生成token不同之处在于getOAuth2Authentication方法,密码模式在该方法内进行了用户认证,但是授权码模式由于已经在/oauth/authorize已经对用户进行认证过了,getOAuth2Authentication方法只需要根据code判断用户认证信息在oauth_code是否存在即可。其它生成token步骤相同,流程图如下

OAuth2流程分析笔记

授权码模式认证分析

授权码模式认证默认是/oauth/authorize请求,支持get和post,代码分析如下

//AuthorizationEndpoint.authorize(Map<String, Object>, Map<String, String>, SessionStatus, Principal)
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
                              SessionStatus sessionStatus, Principal principal) {//代码@@1

    // Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
    // query off of the authorization request instead of referring back to the parameters map. The contents of the
    // parameters map will be stored without change in the AuthorizationRequest object once it is created.
    AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);//代码@@2

    Set<String> responseTypes = authorizationRequest.getResponseTypes();

    if (!responseTypes.contains("token") && !responseTypes.contains("code")) {//授权码模式请求参数response_type必须有code或者token
        throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
    }

    if (authorizationRequest.getClientId() == null) {
        throw new InvalidClientException("A client id must be provided");
    }

    try {

        if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {//如果用户未登录,抛出异常,并redirect到登录页面
            throw new InsufficientAuthenticationException(
                "User must be authenticated with Spring Security before authorization can be completed.");
        }

        ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());//根据client_id获取client对象ClientDetails,注意,这个阶段并未对client进行验证,在生成token时候才验证client_secret

        // The resolved redirect URI is either the redirect_uri from the parameters or the one from
        // clientDetails. Either way we need to store it on the AuthorizationRequest.
        String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);//获取redirect_uri
        String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
        if (!StringUtils.hasText(resolvedRedirect)) {
            throw new RedirectMismatchException(
                "A redirectUri must be either supplied or preconfigured in the ClientDetails");
        }
        authorizationRequest.setRedirectUri(resolvedRedirect);

        // We intentionally only validate the parameters requested by the client (ignoring any data that may have
        // been added to the request by the manager).
        oauth2RequestValidator.validateScope(authorizationRequest, client);

        // Some systems may allow for approval decisions to be remembered or approved by default. Check for
        // such logic here, and set the approved flag on the authorization request accordingly.
        authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
                                                                       (Authentication) principal);
        // TODO: is this call necessary?
        boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
        authorizationRequest.setApproved(approved);

        // Validation is all done, so we can check for auto approval...
        if (authorizationRequest.isApproved()) {//代码@@3
            if (responseTypes.contains("token")) {
                return getImplicitGrantResponse(authorizationRequest);
            }
            if (responseTypes.contains("code")) {//代码@@4
                return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                                                                     (Authentication) principal));
            }
        }

        // Store authorizationRequest AND an immutable Map of authorizationRequest in session
        // which will be used to validate against in approveOrDeny()
        model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);//代码@@5
        model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));

        return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);//代码@@6

    }
    catch (RuntimeException e) {
        sessionStatus.setComplete();
        throw e;
    }

}

请求url例子如下:http://localhost:9401/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all&state=normal

代码@@1:参数model是map,用于像map存放数据,而非取数据。参数parameters是请求参数(response_type、redirect_uri等查询参数)。sessionStatus是抛出异常时候才用。principal是认证对象,即UsernamePasswordAuthenticationToken,该对象是用户密码认证后的对象。具体是在FilterChainProxy的UsernamePasswordAuthenticationFilter进行对用户密码进行认证。

代码@@2:创建AuthorizationRequest对象,该对象封装了请求参数、client信息

代码@@3:这段逻辑是如果approve为true,即oauth_client_details表autoapprove为true,可以直接进行跳转到redirect_uri指定的的uri,如果为false,则需要先跳转到/oauth/confirm_access,在页面点击approve后,再跳转到redirect_uri指定的的uri。

OAuth2流程分析笔记

代码@@4:如果是自动approve,则直接跳转到redirect_uri指定的的uri

private View getAuthorizationCodeResponse(AuthorizationRequest authorizationRequest, Authentication authUser) {
    try {
        return new RedirectView(getSuccessfulRedirect(authorizationRequest,
                                                      generateCode(authorizationRequest, authUser)), false, true, false);
    }
    catch (OAuth2Exception e) {
        return new RedirectView(getUnsuccessfulRedirect(authorizationRequest, e, false), false, true, false);
    }
}

其中generateCode是生成code,所谓code就是一个随机数,表示用户已经认证通过了,同时会把code值保存到oauth_code表。

getSuccessfulRedirect是组装url,组装后的url是https://www.baidu.com/?code=qaNPlR&state=normal,要带上code和值,在生成token的时候要进行校验code值的。

代码@@5 和 代码@@6:如果approve=false,则跳转到/oauth/confirm_access

/oauth/confirm_access请求在WhitelabelApprovalEndpoint端点内,该请求在用户点击了approve后,会redirct到/oauth/authorize接口,由于此时请求参数包含user_oauth_approval,因此执行AuthorizationEndpoint.approveOrDeny(Map<String, String>, Map<String, ?>, SessionStatus, Principal)

AuthorizationEndpoint内/oauth/authorize请求有两个Controller方法,其中approveOrDeny方法是/oauth/authorize请求参数有user_oauth_approval的时候才由此方法接收处理。

OAuth2流程分析笔记

approveOrDeny方法也是验证用户密码,生成code,redirect到指定url,和autoapprove=true逻辑一样。

总结下spring-oauth2默认的端点有如下:

AuthorizationEndpoint 负责验证用户密码生成code并redirect到用户指定url,请求url:/oauth/authorize

TokenEndpoint 负责生成token,请求url:/oauth/token

WhitelabelApprovalEndpoint:授权码模式下,autoapprove=false时候,跳转这里,请求url:/oauth/confirm_access

WhitelabelErrorEndpoint:失败页面,请求url:/oauth/error

CheckTokenEndpoint:校验token,请求url:/oauth/check_token

oauth2启动流程

认证服务启动

通过@EnableAuthorizationServer开启oauth2认证,引入了配置类AuthorizationServerEndpointsConfiguration、AuthorizationServerSecurityConfiguration。

先看配置类AuthorizationServerEndpointsConfiguration源码

@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {

	private AuthorizationServerEndpointsConfigurer endpoints = new AuthorizationServerEndpointsConfigurer();
    
	@Autowired
	private ClientDetailsService clientDetailsService;//这个bean是IOC容器什么时候创建的呢?

	@Autowired
	private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();//把IOC内类型是AuthorizationServerConfigurer的bean注入到此集合,通常是我们开发的AuthorizationServerConfigurer认证服务配置对象。

	@PostConstruct //在构造器和@Autowired注入完成后执行
	public void init() {
		for (AuthorizationServerConfigurer configurer : configurers) {
			try {
                //执行AuthorizationServerConfigurerAdapter.configure(AuthorizationServerEndpointsConfigurer)
				configurer.configure(endpoints);//配置授权服务器端点的非安全功能,例如令牌存储,令牌自定义,用户批准和授予类型
			} catch (Exception e) {
				throw new IllegalStateException("Cannot configure enpdoints", e);
			}
		}
		endpoints.setClientDetailsService(clientDetailsService);
	}
//重点关注两个端点bean
    //创建认证端点,默认的认证处理接口/oauth/authorize
	@Bean
	public AuthorizationEndpoint authorizationEndpoint() throws Exception {
		AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
		FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
		authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
		authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
		authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
		authorizationEndpoint.setTokenGranter(tokenGranter());
		authorizationEndpoint.setClientDetailsService(clientDetailsService);
		authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
		authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
		authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
		authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
		authorizationEndpoint.setRedirectResolver(redirectResolver());
		return authorizationEndpoint;
	}

    //创建申请token端点,处理/oauth/token
	@Bean
	public TokenEndpoint tokenEndpoint() throws Exception {
		TokenEndpoint tokenEndpoint = new TokenEndpoint();
		tokenEndpoint.setClientDetailsService(clientDetailsService);
		tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
		tokenEndpoint.setTokenGranter(tokenGranter());
		tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
		tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
		tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
		return tokenEndpoint;
	}

	//其它省略
}    

在这个配置类内,不好理解的是@Autowired private ClientDetailsService clientDetailsService;,为什么呢,因为表面来看,并没这个类型的bean被实例化到IOC内。这个对象注入的是ClientDetailsService类型的aop代理类,实际上是AuthorizationServerSecurityConfiguration配置类内@Import的ClientDetailsServiceConfiguration配置类内的

@Bean
@Lazy
@Scope(proxyMode=ScopedProxyMode.INTERFACES)
public ClientDetailsService clientDetailsService() throws Exception {
    return configurer.and().build();
}

这里有些特殊的是这个clientDetailsService bean是@Scope(proxyMode=ScopedProxyMode.INTERFACES),因此生成ClientDetailsService接口的aop代理类,又因为是lazy,因此实际在使用过程中,才会真正的创建JdbcClientDetailsService/InMemoryClientDetailsService,那么为什么要使用@Lazy和@Scope呢?因为此时AuthorizationServerSecurityConfiguration需要注入ClientDetailsService,但是此时并没有实际的ClientDetailsService bean,因此需要进行代理。使用@Lazy的原因也是必须要延迟加载,解决注入的冲突。等到运行过程中,真正使用到的时候,会build个JdbcClientDetailsService/InMemoryClientDetailsService bean。

接着配置类AuthorizationServerSecurityConfiguration 认证服务器安全配置,用于配置认证服务器的安全,继承了spring-security的WebSecurityConfigurerAdapter,那么因此执行的就是configure方法了

@Configuration
@Order(0)
@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })
public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Autowired
	private List<AuthorizationServerConfigurer> configurers = Collections.emptyList();//把IOC内类型是AuthorizationServerConfigurer的bean注入到此集合。AuthorizationServerConfigurer的常用实现是AuthorizationServerConfigurerAdapter,开发基本要重写AuthorizationServerConfigurerAdapter。 把认证服务配置对象注入到此集合

	@Autowired
	private ClientDetailsService clientDetailsService;//这个是@Import内的ClientDetailsServiceConfiguration的clientDetailsService(),这个方法使用了@Scope(proxyMode=ScopedProxyMode.INTERFACES)会创建aop动态代理类。 

	@Autowired
	private AuthorizationServerEndpointsConfiguration endpoints;//注入另外一个配置类 认证服务端点配置

    //入参clientDetails需要注入,这个ClientDetailsServiceConfigurer类型bean是在@Import ClientDetailsServiceConfiguration这个配置类的方法org.springframework.security.oauth2.config.annotation.configuration.ClientDetailsServiceConfiguration创建
	@Autowired
	public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception {
		for (AuthorizationServerConfigurer configurer : configurers) {
			configurer.configure(clientDetails);//执行AuthorizationServerConfigurer.configure(ClientDetailsServiceConfigurer),配置ClientDetailsService
		}
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		// Over-riding to make sure this.disableLocalConfigureAuthenticationBldr = false
		// This will ensure that when this configurer builds the AuthenticationManager it will not attempt
		// to find another 'Global' AuthenticationManager in the ApplicationContext (if available),
		// and set that as the parent of this 'Local' AuthenticationManager.
		// This AuthenticationManager should only be wired up with an AuthenticationProvider
		// composed of the ClientDetailsService (wired in this configuration) for authenticating 'clients' only.
	}

    //spring-security,保护端点资源
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer();
		FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping();
		http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping);
		configure(configurer);
		http.apply(configurer);//配置 ClientCredentialsTokenEndpointFilter,用于申请token时候访问/oauth/token时候验证client_secret
		String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token");
		String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key");
		String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token");
		if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) {
			UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class);
			endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService);
		}
		// @formatter:off
		http
        	.authorizeRequests()
            	.antMatchers(tokenEndpointPath).fullyAuthenticated()
            	.antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess())
            	.antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess())
        .and()
        	.requestMatchers()
            	.antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath)
        .and()
        	.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
		// @formatter:on
		http.setSharedObject(ClientDetailsService.class, clientDetailsService);
	}

	protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
		for (AuthorizationServerConfigurer configurer : configurers) {
			configurer.configure(oauthServer);//执行AuthorizationServerConfigurer.configure(AuthorizationServerSecurityConfigurer),配置/oauth,/token端点的安全性
		}
	}

}

此外还要个重要接口AuthorizationServerConfigurer,我们需要重写该接口的configure,用于配置认证服务、端点。在两个配置类内都会进行调用和配置。

总结oauth2认证服务

通过@EnableAuthorizationServer开启oauth2认证,实现接口AuthorizationServerConfigurer,使用jdbc配置clentId。做法也spring-security很类似。重要的就是两个端点AuthorizationEndpoint和TokenEndpoint,分别用来认证和下发令牌,还有个重要的filter是ClientCredentialsTokenEndpointFilter,用于在第三方应用系统向认证服务申请令牌时候验证client_id和client_secret是否匹配。

公司内部系统无法分享,后面会总结下公司的sso流程分析。

想学习oauth2的大佬可以参考下github mall-swarm/mall-auth即可 https://github.com/macrozheng/mall-swarm.git。

上一篇:Spring Security OAuth2之认证服务、资源服务、web安全配置服务加载优先级详解


下一篇:wcf服务与web发布时无法访问 几种解决办法