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正常访问应用系统资源
对于原生的spring-security-oauth2来说,用户未登录,被Nebula重定向到登录页面,用户输入用户密码,请求直接转发给spring-security-oauth2内部TokenEndpoint的/oauth/token接口处理,该接口内会验证用户密码正确和下发token。
授权码模式
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生成的流程:
从上图总结可以看出,创建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 。这个类很容易迷惑我们。
这几个类图关系
对于ClientDetailsService 、 UserDetailsService、AuthorizationServerTokenServices (默认实现类DefaultTokenServices token生成器)我们可以直接从Spring 容器中获取(启动阶段就赋值)。
根据client_id获取ClientDetails,有了 ClientDetails 就有了 TokenRequest,有了 TokenRequest 和 Authentication(认证后肯定有的) 就有了 OAuth2Authentication ,有了OAuth2Authentication 就能够生成 OAuth2AccessToken。
几个容易混淆的对象区别:
token的创建涉及到TokenRequest、OAuth2Request,UsernamePasswordAuthenticationToken、OAuth2Authentication,这几个也容易混淆。TokenRequest封装了client_id和grant_type
OAuth2Request除了封装了client_id和grant_type,还持有client的authorities,和TokenRequest相比,只是不同阶段的对象而已,封装的属性不同,两者都extends BaseRequest
UsernamePasswordAuthenticationToken表示用户通过认证后的对象(理解为封装了用户和密码信息)
OAuth2Authentication封装了用户认证对象UsernamePasswordAuthenticationToken和OAuth2Request,表示用户和client都通过认证后的对象。
疑问:/oauth/token接口生成token,参数Principal是怎么来的呢?
从前面的代码分析说明入参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,流程如下
授权码模式生成token流程
授权码模式token生成和密码模式生成token不同之处在于getOAuth2Authentication方法,密码模式在该方法内进行了用户认证,但是授权码模式由于已经在/oauth/authorize已经对用户进行认证过了,getOAuth2Authentication方法只需要根据code判断用户认证信息在oauth_code是否存在即可。其它生成token步骤相同,流程图如下
授权码模式认证分析
授权码模式认证默认是/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。
代码@@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的时候才由此方法接收处理。
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。