1、《入门示例和流程分析》
2、《未认证的请求是如何重定向到登录地址的》
3、《应用A是如何重定向到授权服务器的授权地址呢?》
4、《授权服务器是如何实现授权的呢?》
5、《登录访问应用A后再访问应用B会发生什么呢?》
1、前言
在前面的几篇博文中,我们分析了实现单点登录过程中是如何实现从访问http://localhost:8082/index重定向到http://localhost:8082/login,然后又重定向到授权服务器http://localhost:8080/oauth/authorize地址上的。到目前为止,我们的请求已经从应用A到授权服务器了,那授权服务器是如何实现登录认证的呢?我们下面开始跟着代码进行分析。
2、重定向到授权登录页
当从应用A重定向到授权服务器http://localhost:8080/oauth/authorize地址时,这时候就需要从授权服务器的角度来进行分析了。
首先,当我们访问http://localhost:8080/oauth/authorize地址时,这个时候,也会经过一系列的过滤器,不过最终还是进入到了/oauth/authorize对应的方法中,代码如下:
//AuthorizationEndpoint.java
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
//省略 ……
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
//省略 ……
}
这个时候,请求中的principal参数为空,即没有认证信息,这个时候就会抛出InsufficientAuthenticationException异常,然后被SpringSecurity过滤器链中的异常过滤器拦截,并重定向到授权服务器的登录地址(和重定向到应用A的登录地址类似),因为这里是授权服务器,所以最终会向用户呈现出统一登录的界面。
3、授权服务器 如何进行登录认证?
在统一登录页,用户输入用户名密码后,点击“登录”按钮后,会向授权服务器发送一个http://localhost:8080/login请求(POST类型),这个时候会携带用户名密码到授权服务器,而授权服务器端的SpringSecurity过滤器链中有如下的过滤器,需要注意,和应用A过滤器链有一些区别,如下所示:
在登录验证这一步,授权服务器其实和普通的SpringSecurity应用是没有区别的,就是通过UsernamePasswordAuthenticationFilter过滤器验证用户名密码的有效性,如果登录成功就执行后续successfulAuthentication()方法,具体实现如下:
//AbstractAuthenticationProcessingFilter.java
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
// 省略 ……
Authentication authResult;
try {
//实际调用了实现类UsernamePasswordAuthenticationFilter中的实现
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
如果输入正确的用户名密码,attemptAuthentication()方法会正常返回,最后开始执行successfulAuthentication()方法,用于处理登录成功的后续逻辑。
在successfulAuthentication()方法中,又调用了SavedRequestAwareAuthenticationSuccessHandler的onAuthenticationSuccess()方法,而onAuthenticationSuccess()方法中实现了地址(localhost:8080/oauth/authorize)的重定向,具体如下:
//SavedRequestAwareAuthenticationSuccessHandler.java
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
// 省略 ……
String targetUrl = savedRequest.getRedirectUrl();
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
//targetUrl 为 localhost:8080/oauth/authorize
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
经过上述方法onAuthenticationSuccess()内实现的重定向,又重新回到了前面提到的授权地址(AuthorizationEndpoint的authorize()方法),在前面提到因为没有认证,所以直接抛出异常,最后跳转到了登录页,而这个时候我们已经进行了认证,所以就可以执行后续的代码了,这里主要实现了重定向到应用A,具体如下:
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
//省略 ……
try {
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());
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);
oauth2RequestValidator.validateScope(authorizationRequest, client);
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
//重定向到 应用A的登录地址,不过这个时候,就会携带code参数值
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
4、应用A请求获取accessToken
在授权服务器完成了登录认证后,就会重定向到了应用A上了,不过这次携带了code参数,这个参数也是前面经历这么多过程为了实现的目的。后续,我们可以使用这个code来换取accessToken了。
回到应用A后,重定向的地址是http://localhost:8082/login?code=A2LWAK&state=25p7td,即应用A的登录地址,不过这次会携带code参数。
访问http://localhost:8082/login时,又重新开始经过SpringSecurity的过滤器,当经过OAuth2ClientAuthenticationProcessingFilter过滤器时,会调用doFilter()方法,进而调用attemptAuthentication()方法进行登录验证(抽象类中定义的方法,然后在OAuth2ClientAuthenticationProcessingFilter类中实现)。
在attemptAuthentication()方法中,又调用了OAuth2RestTemplate的getAccessToken()方法获取accessToken,而在getAccessToken()方法中,又调用了acquireAccessToken()方法获取accessToken,前面在《应用A是如何重定向到授权服务器的授权地址呢?》中,已经分析了该过程,这里不再重复贴出代码了。
在acquireAccessToken()方法中,又调用了AuthorizationCodeAccessTokenProvider的obtainAccessToken()方法获取accessToken,具体实现如下:
//AuthorizationCodeAccessTokenProvider.java
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
OAuth2AccessDeniedException {
AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;
//不携带code参数时,通过obtainAuthorizationCode()方法去获取授权码
if (request.getAuthorizationCode() == null) {
if (request.getStateKey() == null) {
throw getRedirectForAuthorization(resource, request);
}
obtainAuthorizationCode(resource, request);
}
//携带code参数时,通过retrieveToken()方法获取accessToken
return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
getHeadersForTokenRequest(request));
}
在上一篇博文中,因为没有携带code参数值,所以调用了obtainAuthorizationCode()方法,而这时候,因为携带了code参数值,所以会直接调用retrieveToken()方法获取accessToken。而retrieveToken()方法的实现如下:
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException {
try {
// 省略 ……
return getRestTemplate().execute(getAccessTokenUri(resource, form), getHttpMethod(),
getRequestCallback(resource, form, headers), extractor , form.toSingleValueMap());
}
// 省略 catch代码块……
}
在retrieveToken()方法中,又会通过getRestTemplate().execute()方法去授权服务器获取accessToken。这个时候,向授权服务器发送的请求地址是http://localhost:8080/oauth/token,即在配置文件中配置的access-token-uri参数值。
5、授权服务器生成 accessToken
在应用A执行retrieveToken()方法后,就回到了授权服务器进行验证,并生成应用A需要的accessToken。
首先进入了/oauth/token对应的接口方法,即TokenEndpoint类的postAccessToken()方法,实现如下:
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
//省略 ……
//生成 accessToken
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
在postAccessToken()方法中,通过grant()方法生成accessToken,并返回到了应用A。
6、应用A 获取 accessToken
经过前面的流程,把code换成了accessToken,当应用A获取到accessToken后,然后会通过UserInfoTokenServices.loadAuthentication()方法,使用accessToken换取具体的用户信息,其中loadAuthentication()方法会根据配置中的security.oauth2.resource.user-info-uri请求资源服务器中的用户信息,实现如下:
//OAuth2ClientAuthenticationProcessingFilter.java
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
OAuth2AccessToken accessToken;
try {
//获取accessToken
accessToken = restTemplate.getAccessToken();
} catch (OAuth2Exception e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
try {
//请求资源服务器,获取用户信息,即根据配置中的security.oauth2.resource.user-info-uri值获取用户信息
OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue());
if (authenticationDetailsSource!=null) {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
result.setDetails(authenticationDetailsSource.buildDetails(request));
}
publish(new AuthenticationSuccessEvent(result));
return result;
}
catch (InvalidTokenException e) {
BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
publish(new OAuth2AuthenticationFailureEvent(bad));
throw bad;
}
}
经过上述attemptAuthentication()方法,首先完成了通过code换取accessToken,然后又根据accessToken获取用户信息。这个时候,实现了应用A的认证登录,然后就会继续执行OAuth2ClientAuthenticationProcessingFilter的doFilter()方法(实际上在父类AbstractAuthenticationProcessingFilter中定义)中后续的代码,最终通过successfulAuthentication()方法实现了最终访问地址的重定向,即重定向到了最初访问的http://localhost:8082/index地址上。
7、写在最后
至此,我们就完成了应用A,从访问、授权服务器认证,然后跳转到访问地址的全部流程。后续,我们将继续分析,如果这个时候访问应用B又会发生什么呢?敬请期待!!!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68737.html