我的博客地址:blog.sqdyy.cnhtml
Spring Security OAuth2是一个基于OAuth2
封装的一个类库,它提供了构建Authorization Server
、Resource Server
和Client
三种Spring
应用程序角色所须要的功能。Spring Security OAuth
须要与Spring Framework(Spring MVC)
和Spring Security
提供的功能协同工做,在使用Spring Security OAuth
构建Authorization Server
、Resource Server
和Client
的状况下,Spring Security OAuth2
的总体架构图以下:java
UserAgent
访问client
,在受权容许访问受权端点的状况下,OAuth2RestTemplate
会建立OAuth2
认证的REST
请求,指示UserAgent
重定向到Authorization Server
的受权端点AuthorizationEndpoint
。UserAgent
访问Authorization Server
的受权端点的authorize
方法,当未注册受权时,受权端点将须要受权的界面/oauth/confirm_access
显示给资源拥有者,资源拥有者受权后会经过 AuthorizationServerTokenServices
生成受权码或访问令牌,生成的令牌最终会经过userAgent
重定向传递给客户端。OAuth2RestTemplate
拿到受权码后建立请求访问受权服务器TokenEndpoint
令牌端点,令牌端点经过调用AuthorizationServerTokenServices
来验证客户端提供的受权码进行受权,并颁发访问令牌响应给客户端。OAuth2RestTemplate
在请求头中加入从受权服务器获取的访问令牌来访问资源服务器,资源服务器经过OAuth2AuthenticationManager
调用ResourceServerTokenServices
验证访问令牌和与访问令牌关联的验证信息。访问令牌验证成功后,返回客户端请求对应的资源。上面大体讲解了
Spring Security OAuth2
三种应用角色的执行流程,下面咱们将逐个剖析这三种角色的架构和源码来加深理解。git
受权服务器主要提供了资源拥有者的认证服务,客户端经过受权服务器向资源拥有者获取受权,而后获取受权服务器颁发的令牌。在这个认证流程中,涉及到两个重要端点,一个是受权端点AuthorizationEndpoint
,另外一个是令牌端点TokenEndpoint
。下面将经过源码分析这两个端点的内部运行流程。github
首先让咱们来看下访问受权端点AuthorizationEndpoint
的执行流程:spring
UserAgent
会访问受权服务器的AuthorizationEndpoint
(受权端点)的URI:/oauth/authorize
,调用的是authorize
方法,主要用于判断用户是否已经受权,若是受权颁发新的authorization_code,不然跳转到用户受权页面。authorize
它会先调用ClientDetailsService
获取客户端详情信息,并验证请求参数。authorize
方法再将请求参数传递给UserApprovalHandler
用来检测客户端是否已经注册了scope
受权。approved
为false
,将会向资源拥有者显示请求受权的界面/oauth/confirm_access
。/oauth/authorize
,这次请求参数会增长一个user_oauth_approval
,所以会调用另外一个映射方法approveOrDeny
。approveOrDeny
会调用userApprovalHandler.updateAfterApproval
根据用户是否受权,来决定是否更新authorizationRequest
对象中的approved
属性。userApprovalHandler
的默认实现类是ApprovalStoreUserApprovalHandler
,其内部是经过ApprovalStore
的addApprovals
来注册受权信息的。当没有携带请求参数user_oauth_approval
时,会访问authorize
方法,执行流程和上面1-5步对应,若是用户已经受权则颁发新的authorization_code
,不然跳转到用户受权页面:安全
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) {
// 根据请求参数封装 认证请求对象 ----> AuthorizationRequest
// 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);
// 获取请求参数中的response_type类型,并进行条件检验:response_type只支持token和code,即令牌和受权码
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
// 请求参数必须携带客户端ID
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
// 在使用Spring Security OAuth2受权完成以前,必须先完成Spring Security对用户进行的身份验证
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
// 获取客户端详情
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// 得到重定向URL,它能够来自请求参数,也能够来自客户端详情,总之你须要将它存储在受权请求中
// 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);
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);
// 根据客户端详情来校验请求参数中的scope
// 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);
// 此处检测请求的用户是否已经被受权,或者有配置默认受权的权限;若已经有accessToke存在或者被配置默认受权的权限则返回含有受权的对象
// 用到userApprovalHandler ----> ApprovalStoreUserApprovalHandler
// 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?
// 若是authorizationRequest.approved为true,则将跳过Approval页面。
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// 已受权 直接返回对应的视图,返回的视图中包含新生成的authorization_code(固定长度的随机字符串)值
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the sessionfor approveOrDeny to use.
// That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
// 未受权 跳转到受权界面,让用户选择是否受权
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
复制代码
用户经过受权页面确认是否受权,并携带请求参数user_oauth_approval
访问受权端点,会执行approveOrDeny
方法,执行流程对应上面6-7步:服务器
@RequestMapping(value = "/oauth/authorize", method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model, SessionStatus sessionStatus, Principal principal) {
// 在使用Spring Security OAuth2受权完成以前,必须先完成Spring Security对用户进行的身份验证
if (!(principal instanceof Authentication)) {
sessionStatus.setComplete();
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorizing an access token.");
}
// 获取请求参数
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
if (authorizationRequest == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
}
try {
// 获取请求参数中的response_type类型
Set<String> responseTypes = authorizationRequest.getResponseTypes();
// 设置Approval的参数
authorizationRequest.setApprovalParameters(approvalParameters);
// 根据用户是否受权,来决定是否更新authorizationRequest对象中的approved属性。
authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest,
(Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// 须要携带重定向URI
if (authorizationRequest.getRedirectUri() == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
}
// 用户拒绝受权,响应错误信息到客户端的重定向URL上
if (!authorizationRequest.isApproved()) {
return new RedirectView(getUnsuccessfulRedirect(authorizationRequest,
new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")),
false, true, false);
}
// 简化模式,直接颁发访问令牌
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest).getView();
}
// 受权码模式,生成受权码存储并返回给客户端
return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
}
finally {
sessionStatus.setComplete();
}
}
复制代码
接下来咱们看下令牌端点TokenEndpoint
的执行流程:session
userAgent
经过访问受权服务器令牌端点TokenEndpoint的URI:/oauth/token
,调用的是postAccessToken
方法,主要用于为客户端生成Token
。postAccessToken
首先会调用ClientDetailsService
获取客户端详情信息并验证请求参数。Token
。AbstractTokenGranter
抽象类,它的成员AuthorizationServerTokenServices
能够用来建立、刷新、获取token
。AuthorizationServerTokenServices
默认实现类只有DefaultTokenServices
,经过它的createAccessToken
方法能够看到token
是如何建立的。token
的类是TokenStore
,程序根据TokenStore
接口的不一样实现来生产和存储token
。下面列出TokenEndpoint
的URI:/oauth/token
的源码分析:架构
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
// 在使用Spring Security OAuth2受权完成以前,必须先完成Spring Security对用户进行的身份验证
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
// 经过客户端Id获取客户端详情
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
// 根据请求参数封装 认证请求对象 ----> AuthorizationRequest
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
// 根据客户端详情来校验请求参数中的scope,防止客户端越权获取更多权限
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");
}
// 若是grant_type=authoraztion_code,则清空scope
if (isAuthCodeRequest(parameters)) {
// 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());
}
}
// 若是grant_type=refresh_token,设置刷新令牌的scope
if (isRefreshTokenRequest(parameters)) {
// 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)));
}
// 为客户端生成token
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
复制代码
令牌端点最关键的就是如何生产token
,不一样的受权模式都会基于AbstractTokenGranter
接口作不一样实现,AbstractTokenGranter
会委托AuthorizationServerTokenServices
来建立、刷新、获取token
。AuthorizationServerTokenServices
的默认实现只有DefaultTokenServices
,简单抽取它的createAccessToken
方法源码便可看到:app
// 生成accessToken和RefreshToken
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 首先尝试获取当前存在的Token
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 若是现有的访问令牌accessToken不为空且没有失效,则保存现有访问令牌, 若是失效则从新存储新的访问令牌
if (existingAccessToken != null) {
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);
}
// 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);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
复制代码
资源服务器主要用于处理客户端对受保护资源的访问请求并返回相应。资源服务器会验证客户端的访问令牌是否有效,并获取与访问令牌关联的认证信息。获取认证信息后,验证访问令牌是否在容许的scope
内,验证完成后的处理行为能够相似于普通应用程序来实现。下面是资源服务器的运行流程:
OAuth2AuthenticationProcessingFilter
,这个拦截器的做用是从请求中提取访问令牌,而后从令牌中提取认证信息Authentication
并将其存放到上下文中。OAuth2AuthenticationProcessingFilter
拦截器中会调用AuthenticationManager的authenticate
方法提取认证信息。OAuth2AuthenticationProcessingFilter
拦截器若是发生认证错误时,将委托AuthenticationEntryPoint
作出错误响应,默认实现类是OAuth2AuthenticationEntryPoint
。OAuth2AuthenticationProcessingFilter
执行完成后进入下一个安全过滤器ExceptionTranslationFilter
。ExceptionTranslationFilter
过滤器用来处理在系统认证受权过程当中抛出的异常,拦截器若是发生异常,将委托AccessDeniedHandler
作出错误响应,默认实现类是OAuth2AccessDeniedHandler
。资源服务器咱们要关心的是它如何验证客户端的访问令牌是否有效,因此咱们从一开始的OAuth2AuthenticationProcessingFilter
源码入手,这个拦截器的做用是从请求中提取访问令牌,而后从令牌中提取认证信息Authentication
并将其存放到上下文中:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
boolean debug = logger.isDebugEnabled();
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
// 从请求中提取token,而后再提取token中的认证信息Authorization
Authentication authentication = this.tokenExtractor.extract(request);
if (authentication == null) {
if (this.stateless && this.isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//获取token携带的认证信息Authentication并进行验证,而后存到spring security的上下文,以供后续使用
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
} catch (OAuth2Exception var9) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + var9);
}
this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
return;
}
chain.doFilter(request, response);
}
复制代码
上面代码提到Oauth2AuthenticationManager
会获取token
携带的认证信息进行认证,经过源码能够了解到它主要作了3步工做:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
// 1.经过token获取OAuuth2Authentication对象
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
// 2.验证资源服务的ID是否正确
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
// 3.验证客户端的访问范围(scope)
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
复制代码
验证经过后,通过ExceptionTranslationFilter
过滤器,便可访问资源。
Spring security OAuth2
客户端控制着OAuth 2.0
保护的其它服务器的资源的访问权限。配置包括创建相关受保护资源与有权限访问资源的用户之间的链接。客户端也须要实现存储用户的受权代码和访问令牌的功能。
客户端代码结构不是特别复杂,这里接触架构图的描述,有兴趣能够本身按着下面介绍的流程研究源码:
UserAgent
调用客户端的Controller
,在这以前会通过OAuth2ClientContextFilter
过滤器,它主要用来捕获第5步可能发生的UserRedirectRequiredException
,以便重定向到受权服务器从新受权。RestOperations->OAuth2RestOperations
接口的实现类OAuth2RestTemplate
。它主要提供访问受权服务器或资源服务器的RestAPI
。OAuth2RestTemplate
的成员OAuth2ClientContext
接口实现类为DefaultOAuth2ClientContext
。它会校验访问令牌是否有效,有效则执行第6步访问资源服务器。AccessTokenProvider
来获取访问令牌。AccessTokenProvider
根据定义的资源详情信息和受权类型获取访问令牌,若是获取不到,抛出UserRedirectRequiredException
。文中架构图和部份内容参考自TERASOLUNA服务器框架(5.x)开发指南,转载请注明来源。