spring-ouauth2-原理
一.Oauth2授权流程
1.oauth2
2.OIDC
2.源码解析(springboot-3.1.1,spring-security-oauth2-authorization-server 1.1.1)
1.访问客户端首页 http://localhost:7999/index
OAuth2AuthorizationRequestRedirectFilter
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
} catch (Exception var11) {
this.unsuccessfulRedirectForAuthorization(request, response, var11);
return;
}
try {
filterChain.doFilter(request, response);
//调用解析器解析请求
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
String registrationId = this.resolveRegistrationId(request);
if (registrationId == null) {
return null;
} else {
String redirectUriAction = this.getAction(request, "login");
return this.resolve(request, registrationId, redirectUriAction);
}
}
//由于registrationId ==null 返回空 ,继续执行后面的filter AuthorizationFilter
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;
if (this.observeOncePerRequest && this.isApplied(request)) {
chain.doFilter(request, response);
} else if (this.skipDispatch(request)) {
chain.doFilter(request, response);
} else {
String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
AuthorizationDecision decision = this.authorizationManager.check(this::getAuthentication, request);
this.eventPublisher.publishAuthorizationEvent(this::getAuthentication, request, decision);
if (decision != null && !decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
chain.doFilter(request, response);
} finally {
request.removeAttribute(alreadyFilteredAttributeName);
}
}
由于上面还没有授权访问 会抛出 throw new AccessDeniedException("Access Denied");,此异常被ExceptionTranslationFilter捕获
2.重定向到http://localhost:7999/oauth2/authorization/messaging-client-oidc
}
//检测是否认证,如果没有认证则抛出异常,被上一个过滤器ExceptionTranslationFilter捕获
private void handleSpringSecurityException(HttpServletRequest request, HttpServletResponse response, FilterChain chain, RuntimeException exception) throws IOException, ServletException {
if (exception instanceof AuthenticationException) {
this.handleAuthenticationException(request, response, chain, (AuthenticationException)exception);
} else if (exception instanceof AccessDeniedException) {
this.handleAccessDeniedException(request, response, chain, (AccessDeniedException)exception);
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, AuthenticationException reason) throws ServletException, IOException {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
this.securityContextHolderStrategy.setContext(context);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
//然后冲认证进入点获取到配置的登录切入点重定向 即在客户端的securityFilterChain中配置的loginpage
.oauth2Login(oauth2Login ->
oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc"))
3. 重定向到授权服务器
http://localhost:8500/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile%20message.read%20message.write&state=zB8C9OngKhZd9bLgq_ndSfY3B7t-HvBsLoorxPrZIJQ%3D&redirect_uri=http://127.0.0.1:7999/login/oauth2/code/messaging-client-oidc&nonce=Df3hSGqMNPMeJ2opt4LMfNqgssZNpU4qxhVLWtiRGJE
//DefaultOAuth2AuthorizationRequestResolver,首先解析请求是不是OAuth2AuthorizationRequest ,解析过程中会生成state和nonce参数,然后构建重定向参数
private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId, String redirectUriAction) {
if (registrationId == null) {
return null;
} else {
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
throw new InvalidClientRegistrationIdException("Invalid Client Registration with Id: " + registrationId);
} else {
OAuth2AuthorizationRequest.Builder builder = this.getBuilder(clientRegistration);
String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
builder.clientId(clientRegistration.getClientId()).authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri()).redirectUri(redirectUriStr).scopes(clientRegistration.getScopes()).state(DEFAULT_STATE_GENERATOR.generateKey());
this.authorizationRequestCustomizer.accept(builder);
return builder.build();
}
}
}
4.认证服务武器8500 ,由OAuth2AuthorizationEndpointFilter处理,由于此时还是匿名认证,则放行到后面的filter ,然后和上面重定向逻辑一样 会重定向到登录页面http://localhost:8500/login
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (!this.authorizationEndpointMatcher.matches(request)) {
filterChain.doFilter(request, response);
} else {
try {
Authentication authentication = this.authenticationConverter.convert(request);
if (authentication instanceof AbstractAuthenticationToken) {
((AbstractAuthenticationToken)authentication).setDetails(this.authenticationDetailsSource.buildDetails(request));
}
Authentication authenticationResult = this.authenticationManager.authenticate(authentication);
if (!authenticationResult.isAuthenticated()) {//没有认证,放行
filterChain.doFilter(request, response);
return;
}
if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {
if (this.logger.isTraceEnabled()) {
this.logger.trace("Authorization consent is required");
}
this.sendAuthorizationConsent(request, response, (OAuth2AuthorizationCodeRequestAuthenticationToken)authentication, (OAuth2AuthorizationConsentAuthenticationToken)authenticationResult);
return;
}
this.sessionAuthenticationStrategy.onAuthentication(authenticationResult, request, response);
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
} catch (OAuth2AuthenticationException var6) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Authorization request failed: %s", var6.getError()), var6);
}
this.authenticationFailureHandler.onAuthenticationFailure(request, response, var6);
}
}
}
5.输入用户名和密码 点击登录 ,此时AbstractAuthenticationProcessingFilter 判断是否需要认证,如果需要认证则调用子类的attemptAuthentication方法
//UsernamePasswordAuthenticationFilter 拿到用户名和密码
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username.trim() : "";
String password = this.obtainPassword(request);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
然后寻找合适的认证提供者进行认证,这里会找到DaoAuthenticationProvider,然后执行父类AbstractUserDetailsAuthenticationProvider的authenticate方法
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = this.determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw var6;
}
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
先从换从从查找,如果没有,则通过配置的userDetailsService通过loadUserByUsername加载,认证成功后执行认证成功策略AbstractAuthenticationProcessingFilter
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
context.setAuthentication(authResult);
this.securityContextHolderStrategy.setContext(context);
this.securityContextRepository.saveContext(context, request, response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
SavedRequestAwareAuthenticationSuccessHandler ,冲requestCache中获取之前开始认证时候存入的请求 [nio-8500-exec-1] o.s.s.w.s.HttpSessionRequestCache : Saved request http://localhost:8500/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile%20message.read%20message.write&state=D--ICQCGOIuMY3zdsnlzN7330ETAQflC4ad2-wsMBcA%3D&redirect_uri=http://127.0.0.1:7999/login/oauth2/code/messaging-client-oidc&nonce=oh900P9yudr1v4UBUDw2BV8GtWV-XTq1AWKrdYmRfog&continue to session
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
SavedRequest savedRequest = this.requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
} else {
String targetUrlParameter = this.getTargetUrlParameter();
if (!this.isAlwaysUseDefaultTargetUrl() && (targetUrlParameter == null || !StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
this.clearAuthenticationAttributes(request);
String targetUrl = savedRequest.getRedirectUrl();
this.getRedirectStrategy().sendRedirect(request, response, targetUrl);
} else {
this.requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
之后开始成功重定向 AbstractAuthenticationTargetUrlRequestHandler
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String targetUrl = this.determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
this.logger.debug(LogMessage.format("Did not redirect to %s since response already committed.", targetUrl));
} else {
this.redirectStrategy.sendRedirect(request, response, targetUrl);
}
}
6. 再次执行/oauth2/authorize请求, 然后被OAuth2AuthorizationEndpointFilter处理->> OAuth2AuthorizationCodeRequestAuthenticationConverter 转换请求 将请去转为OAuth2AuthorizationCodeRequestAuthenticationToken ,之后被OAuth2AuthorizationCodeRequestAuthenticationProvider认证 ,在该类的方法中生成授权码,认证成功再次执行重定向 并且携带授权码 http://127.0.0.1:7999/login/oauth2/code/messaging-client-oidc?code=2V2TwP37MefzAkw_U5W1FuZ0Hn79uS9Vfu23wpdYmBITVtxELEmcpnRdCWSbU9ODgymRbURmLElDhb6sgSqgs3zFPl4vlyXuHU8NUTQliYQwPwWJnJLNN2yYITDuQjMJ&state=JgWcgJZwNPaODcBXDFtnF8EYHEn0tX2TwgPrXGm9F6g%3D
被OAuth2LoginAuthenticationFilter处理 ,会抛出异常
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
MultiValueMap<String, String> params = OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_request");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error("authorization_request_not_found");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
====为什么会再走一遍
重定向到授权服务
http://localhost:8500/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile%20message.read%20message.write&state=x2wgRAaH1iUghjaKc1w5s9LIsjUhUyqexMRUFTrSJP0%3D&redirect_uri=http://127.0.0.1:7999/login/oauth2/code/messaging-client-oidc&nonce=mUMNu7MQWDGsAQtT-XD4nJJvgNm3Edk4FA-dwtQbD0s
重定向??
http://127.0.0.1:7999/login/oauth2/code/messaging-client-oidc?code=3X5rgRXvtviBc4T0AUuNjQfG7dMbPMH0fnNVmBt-BybRM2cO1EqeIvmNXXUwpaHB1SscyaNOUsjmufzTC34SziXgdme6oAxL9FtmIEGSHicN8SL_ePi2CJGQ_MVTNIPk&state=x2wgRAaH1iUghjaKc1w5s9LIsjUhUyqexMRUFTrSJP0%3D
有了授权码,就可以换取访问token, OidcAuthorizationCodeAuthenticationProvider ,请求认证服务/oauth2/token 节点,被
OAuth2TokenEndpointFilter处理,token 由OAuth2AuthorizationCodeAuthenticationProvider 生成 ,同时包含access_token ,refresh_token ,id_token .同时会加载OidcUser,默认节点为/userinfo
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken)authentication;
if (!authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getScopes().contains("openid")) {
return null;
} else {
OAuth2AuthorizationRequest authorizationRequest = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest();
OAuth2AuthorizationResponse authorizationResponse = authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationResponse();
if (authorizationResponse.statusError()) {
throw new OAuth2AuthenticationException(authorizationResponse.getError(), authorizationResponse.getError().toString());
} else if (!authorizationResponse.getState().equals(authorizationRequest.getState())) {
OAuth2Error oauth2Error = new OAuth2Error("invalid_state_parameter");
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
} else {
OAuth2AccessTokenResponse accessTokenResponse = this.getResponse(authorizationCodeAuthentication);
ClientRegistration clientRegistration = authorizationCodeAuthentication.getClientRegistration();
Map<String, Object> additionalParameters = accessTokenResponse.getAdditionalParameters();
if (!additionalParameters.containsKey("id_token")) {
OAuth2Error invalidIdTokenError = new OAuth2Error("invalid_id_token", "Missing (required) ID Token in Token Response for Client Registration: " + clientRegistration.getRegistrationId(), (String)null);
throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString());
} else {
OidcIdToken idToken = this.createOidcToken(clientRegistration, accessTokenResponse);
this.validateNonce(authorizationRequest, idToken);
OidcUser oidcUser = (OidcUser)this.userService.loadUser(new OidcUserRequest(clientRegistration, accessTokenResponse.getAccessToken(), idToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper.mapAuthorities(oidcUser.getAuthorities());
OAuth2LoginAuthenticationToken authenticationResult = new OAuth2LoginAuthenticationToken(authorizationCodeAuthentication.getClientRegistration(), authorizationCodeAuthentication.getAuthorizationExchange(), oidcUser, mappedAuthorities, accessTokenResponse.getAccessToken(), accessTokenResponse.getRefreshToken());
authenticationResult.setDetails(authorizationCodeAuthentication.getDetails());
return authenticationResult;
}
}
}
}
分享到: