【Spring Security OAuth2 Client】基本介绍以及定制开发

这篇具有很好参考价值的文章主要介绍了【Spring Security OAuth2 Client】基本介绍以及定制开发。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

OAuth2协议起来越普及,大多数企业都有自己的一套单点登录系统,通常都会支持OAuth协议,但这个单点登录系统通常会在OAuth标准协议上多多少少会有改造,我们在企业内部开发一个应用服务,需要对接单点登录SSO,只要支持OAuth协议,我们就可以使用spring-boot-starter-oauth2-client组件进行对接,如果是标准的OAuth2协议,基本上通过配置就能完成对接,如果有定制改造和适配,就会有一定的门槛,本文给大家展示如何在spring-boot-starter-oauth2-client基础上进行适配企业自己的SSO系统。

OAuth2 Client端的pom.xml

做为OAuth2协议的客户端,通常既需要跳转SSO登录,也需要通过SSO校验token,因此除了需要引入spring-boot-starter-oauth2-client,还需要引入spring-boot-starter-oauth2-resource-server

  • 完整pom依赖如下
 <dependencies>
     <!-- spring framework module -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-security</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-oauth2-client</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
     </dependency>
     <!-- spring framework module end -->
 </dependencies>

配置文件

spring:
  security:
    oauth2:
      client:
        registration:
          sso:
            authorization-uri: https://${sso.host}:${sso.port}/${sso.context-path}/oauth2/authorize
            token-uri: https://${sso.host}:${sso.port}/${sso.context-path}/oauth2/getToken
            user-info-uri: https://${sso.host}:${sso.port}/${sso.context-path}/oauth2/getUserInfo
            user-info-authentication-method: GET
            user-name-attribute: loginName
      resourceserver:
        opaqueToken:
          client-id: ${sso.client-id}
          client-secret: ${sso.client-secret}
          introspection-uri: https://${sso.host}:${sso.port}/${sso.context-path}/oauth2/checkTokenValid

sso:
  registration-id: sso
  host: sso.xxx.com
  port: 443
  context-path: sso
  client-id: demo-client-id
  client-secret: demo-client-secret
  logout-path: /sso/logout

如果是标准的OAuth2协议对接,上面的配置就可以满足需求了,接下来重点讲解几个关键的定制开发

关键逻辑介绍

  • security.oauth2.client开头的配置项可以参考OAuth2ClientProperties这个类
  • OAuth2协议响应的标准参数字段可以参考org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames这个类
  • sendRedirectForAuthorization重定向到authorization-uri,并且会携带response_typeclient_idscopestateredirect_urinonce参数
  • OAuth2LoginAuthenticationFilteOAuth2LoginAuthenticationProvider
    • OAuth2LoginAuthenticationFilter会对回调地址(携带了codestate)进行处理,调用AuthemticationManager进行认证
    • 背后OAuth2LoginAuthenticationProvider会进行连续token-uriuser-info-uri请求,最后返回完全填充的OAuth2LoginAuthenticationToken
  • 缓存跳转登录前的请求AuthorizationRequestRepository

适配场景1: 认证接口未返回response_type字段

源码分析

查看org.springframework.security.oauth2.core.endpoint.DefaultMapOAuth2AccessTokenResponseConverter这个类,在convert方法里面,会根据SSO响应的参数构造一个OAuth2AccessToken对象,关键源码如下

public OAuth2AccessToken(TokenType tokenType, String tokenValue, Instant issuedAt, Instant expiresAt, Set<String> scopes) {
    super(tokenValue, issuedAt, expiresAt);
    Assert.notNull(tokenType, "tokenType cannot be null");
    this.tokenType = tokenType;
    this.scopes = Collections.unmodifiableSet(scopes != null ? scopes : Collections.emptySet());
}

自定义DefaultMapOAuth2AccessTokenResponseConverter

由于DefaultMapOAuth2AccessTokenResponseConverter类是final,不能继承,所以我们创建一个DemoMapOAuth2AccessTokenResponseConverter,然后把DefaultMapOAuth2AccessTokenResponseConverter源码copy过来,主要修改accessTokenType为空的情况

@Override
public OAuth2AccessTokenResponse convert(Map<String, Object> source) {
    String accessToken = getParameterValue(source, OAuth2ParameterNames.ACCESS_TOKEN);
    OAuth2AccessToken.TokenType accessTokenType = getAccessTokenType(source);
    // 接口没有返回token_type字段,构造OAuth2AccessTokenResponse时会报错
    if(null == accessTokenType) {
        accessTokenType = OAuth2AccessToken.TokenType.BEARER;
    }
    long expiresIn = getExpiresIn(source);
    Set<String> scopes = getScopes(source);
    String refreshToken = getParameterValue(source, OAuth2ParameterNames.REFRESH_TOKEN);
    Map<String, Object> additionalParameters = new LinkedHashMap<>();
    for (Map.Entry<String, Object> entry : source.entrySet()) {
        if (!TOKEN_RESPONSE_PARAMETER_NAMES.contains(entry.getKey())) {
            additionalParameters.put(entry.getKey(), entry.getValue());
        }
    }
    // @formatter:off
    return OAuth2AccessTokenResponse.withToken(accessToken)
            .tokenType(accessTokenType)
            .expiresIn(expiresIn)
            .scopes(scopes)
            .refreshToken(refreshToken)
            .additionalParameters(additionalParameters)
            .build();
    // @formatter:on
}

让DemoMapOAuth2AccessTokenResponseConverter生效

@Bean
public SecurityFilterChain authorizationClientSecurityFilterChain(HttpSecurity http) throws Exception {
    AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry authorizationManagerRequestMatcherRegistry = http.authorizeHttpRequests();
    // 其它请求都需要认证
    authorizationManagerRequestMatcherRegistry.anyRequest().authenticated();
    // Session会话管理
    SessionManagementConfigurer<HttpSecurity> sessionManagementConfigurer = http.sessionManagement();
    sessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
    // OAuth2.0登录配置
    OAuth2LoginConfigurer<HttpSecurity> oAuth2LoginConfigurer = http.oauth2Login();
    // 自定义获取token请求
    oAuth2LoginConfigurer.tokenEndpoint(c->{
        DefaultAuthorizationCodeTokenResponseClient authorizationCodeTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();
        OAuth2AccessTokenResponseHttpMessageConverter oAuth2AccessTokenResponseHttpMessageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
        oAuth2AccessTokenResponseHttpMessageConverter.setAccessTokenResponseConverter(new DemoMapOAuth2AccessTokenResponseConverter());
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), oAuth2AccessTokenResponseHttpMessageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        authorizationCodeTokenResponseClient.setRestOperations(restTemplate);
        c.accessTokenResponseClient(authorizationCodeTokenResponseClient);
    });

    return http.build();
}

适配场景2: 根据access_token获取用户信息

源码分析

查看org.springframework.security.oauth2.client.userinfo.OAuth2UserRequestEntityConverter这个类,convert方法是生成的http请求调用需要的参数,如果参数名、参数结构与标准OAuth2协议不同,那么就需要在这里进行改造,新建一个DemoOAuth2UserRequestEntityConverter,继承OAuth2UserRequestEntityConverter,主要是改造Get请求时的参数构成

@Override
public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
    ClientRegistration clientRegistration = userRequest.getClientRegistration();
    HttpMethod httpMethod = getHttpMethod(clientRegistration);
    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri());

    RequestEntity<?> request;
    if (HttpMethod.POST.equals(httpMethod)) {
        headers.setContentType(DEFAULT_CONTENT_TYPE);
        MultiValueMap<String, String> formParameters = new LinkedMultiValueMap<>();
        formParameters.add(OAuth2ParameterNames.ACCESS_TOKEN, userRequest.getAccessToken().getTokenValue());
        formParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
        request = new RequestEntity<>(formParameters, headers, httpMethod, uriBuilder.build().toUri());
    }
    else {
        uriBuilder
                .queryParam(OAuth2ParameterNames.ACCESS_TOKEN, userRequest.getAccessToken().getTokenValue())
                .queryParam(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
        request = new RequestEntity<>(httpMethod, uriBuilder.build().toUri());
    }

    return request;
}

适配场景3: 获取用户信息接口响应改造

源码分析

DefaultOAuth2UserService这个类的loadUser这个方法,是对用户信息进行解析,不同的SSO会响应不同的错误码等,新建一个DemoOAuth2UserService,继承DefaultOAuth2UserService,主要是对接口响应出错时的处理

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
    Assert.notNull(userRequest, "userRequest cannot be null");
    if (!StringUtils
            .hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
        OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_INFO_URI_ERROR_CODE,
                "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
                        + userRequest.getClientRegistration().getRegistrationId(),
                null);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint()
            .getUserNameAttributeName();
    if (!StringUtils.hasText(userNameAttributeName)) {
        OAuth2Error oauth2Error = new OAuth2Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
                "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
                        + userRequest.getClientRegistration().getRegistrationId(),
                null);
        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }
    RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
    ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
    Map<String, Object> userAttributes = response.getBody();

    // SSO返回错误处理
    if(userAttributes.containsKey("errcode")) {
        String errcode = String.valueOf(userAttributes.get("errcode"));
        String msg = String.valueOf(userAttributes.get("msg"));
        OAuth2Error oauth2Error = null;
        switch (errcode) {
            // 参数access_token不正确或过期
            case "2002":
                oauth2Error = new OAuth2Error("2002", "", null);
                break;
            default:
                oauth2Error = new OAuth2Error("sso_unknown_error_code", msg, null);
                break;
        }

        throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
    }

    Set<GrantedAuthority> authorities = new LinkedHashSet<>();
    authorities.add(new OAuth2UserAuthority(userAttributes));
    OAuth2AccessToken token = userRequest.getAccessToken();
    for (String authority : token.getScopes()) {
        authorities.add(new SimpleGrantedAuthority("SCOPE_" + authority));
    }
    return new DefaultOAuth2User(authorities, userAttributes, userNameAttributeName);
}

让DemoOAuth2UserRequestEntityConverter和DemoOAuth2UserService生效

  • DemoOAuth2UserService构造函数中指定DemoOAuth2UserRequestEntityConverter
public DemoOAuth2UserService() {
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
    this.restOperations = restTemplate;

    requestEntityConverter = new DemoOAuth2UserRequestEntityConverter();
    setRequestEntityConverter(requestEntityConverter);
}
  • DemoOAuth2UserService为上加上@Service注解
  • Oauth2ClientAutoConfiguration中引用
@Resource
private OAuth2UserService<OAuth2UserRequest, OAuth2User> demoOAuth2UserService;
  • 构造SecurityFilterChain中追加
@Bean
public SecurityFilterChain authorizationClientSecurityFilterChain(HttpSecurity http) throws Exception {
    ...
    // 自定义获取用户信息接口
    oAuth2LoginConfigurer.userInfoEndpoint(c->{
        c.userService(demoOAuth2UserService);
    });
}

适配场景4: 校验access_token请求

源码分析

SpringOpaqueTokenIntrospector这个类是负责发起introspection-uri请求,校验access_token,返回用户信息,我们新建一个DemoSpringOpaqueTokenIntrospector,继承SpringOpaqueTokenIntrospector,主要是优化直接调用access_token获取用户,获取用户失败相当于access_token失效

@Override
public OAuth2AuthenticatedPrincipal introspect(String token) {
    ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(iamProperties.getRegistrationId());
    OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, token, null, null);
    OAuth2UserRequest oAuth2UserRequest = new OAuth2UserRequest(clientRegistration, accessToken, Collections.emptyMap());
    try {
        OAuth2User oAuth2User = demoOAuth2UserService.loadUser(oAuth2UserRequest);
        return oAuth2User;
    } catch (OAuth2AuthenticationException e) {
        throw new BadOpaqueTokenException(e.getMessage(), e);
    }
}

让DemoSpringOpaqueTokenIntrospector生效

  • DemoSpringOpaqueTokenIntrospector类上加上@Component注解
  • 创建DemoOpaqueTokenAuthenticationProvider, 把OpaqueTokenAuthenticationProvider源码复制过来,因为OpaqueTokenAuthenticationProviderfinal
@RequiredArgsConstructor
@Component
public class DemoOpaqueTokenAuthenticationProvider implements AuthenticationProvider {
    
    private final OpaqueTokenIntrospector introspector;
    
    private OAuth2AuthenticatedPrincipal getOAuth2AuthenticatedPrincipal(BearerTokenAuthenticationToken bearer) {
        try {
            return this.introspector.introspect(bearer.getToken());
        } catch (BadOpaqueTokenException var3) {
            this.logger.debug("Failed to authenticate since token was invalid");
            throw new InvalidBearerTokenException(var3.getMessage(), var3);
        } catch (OAuth2IntrospectionException var4) {
            throw new AuthenticationServiceException(var4.getMessage(), var4);
        }
    }
}
  • Oauth2ClientAutoConfiguration中引用
@Resource
private IamOpaqueTokenAuthenticationProvider iamOpaqueTokenAuthenticationProvider;

@Bean
public SecurityFilterChain authorizationClientSecurityFilterChain(HttpSecurity http) throws Exception {
    ...
    http.authenticationProvider(iamOpaqueTokenAuthenticationProvider);
    ...
}

调试过程常见问题记录

认证服务

OAuth2AuthorizationCodeRequestAuthenticationValidator

104行,如果RedirectHostlocalhost,会报错

if (requestedRedirectHost == null || requestedRedirectHost.equals("localhost")) {
				// As per https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7.1
				// While redirect URIs using localhost (i.e., "http://localhost:{port}/{path}")
				// function similarly to loopback IP redirects described in Section 10.3.3,
				// the use of "localhost" is NOT RECOMMENDED.
				OAuth2Error error = new OAuth2Error(
						OAuth2ErrorCodes.INVALID_REQUEST,
						"localhost is not allowed for the redirect_uri (" + requestedRedirectUri + "). " +
								"Use the IP literal (127.0.0.1) instead.",
						"https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-07#section-9.7.1");
				throwError(error, OAuth2ParameterNames.REDIRECT_URI,
						authorizationCodeRequestAuthentication, registeredClient);
			}

oauth2Login 和 oauth2Client 之间有什么区别

oauth2Login()将使用 OAuth2(或 OIDC)对用户进行身份验证,使用来自 JWTuserInfo 端点的信息填充 SpringPrincipaloauth2Client()不会对用户进行身份验证,但会向 OAuth2 授权服务器寻求它需要访问的资源(范围)的许可。oauth2Client()您仍然需要对用户进行身份验证,例如通过formLogin().

[access_denied] OAuth 2.0 Parameter: client_id

原因: 在Consent required页面没有任何勾选授权

authorization_request_not_found

资源服务

  • BearerTokenAuthenticationFilter
  • OAuth2ResourceServerProperties

请求认证服务校验token: OpaqueTokenIntrospector

	private Converter<String, RequestEntity<?>> defaultRequestEntityConverter(URI introspectionUri) {
		return (token) -> {
			HttpHeaders headers = requestHeaders();
			MultiValueMap<String, String> body = requestBody(token);
			return new RequestEntity<>(body, headers, HttpMethod.POST, introspectionUri);
		};
	}
	
	private MultiValueMap<String, String> requestBody(String token) {
		MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
		body.add("token", token);
		return body;
	}	

默认是在body放一个json

{"token": "xxxxxxxx"}

获取Bean,默认是SpringOpaqueTokenIntrospector,可以通过BeanPostProcessor修改requestEntityConverter

		OpaqueTokenIntrospector getIntrospector() {
			if (this.introspector != null) {
				return this.introspector.get();
			}
			return this.context.getBean(OpaqueTokenIntrospector.class);
		}

OAuth2UserService

如果需要自定义获取权限authorities,就创建一个Bean,重写loadUser文章来源地址https://www.toymoban.com/news/detail-626187.html

到了这里,关于【Spring Security OAuth2 Client】基本介绍以及定制开发的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Spring Security OAuth2.0 - 学习笔记

    OAuth2.0是一个开放标准,允许用户授权第三方应用程序访问他们存储在另外的服务提供者上的信息,而不需要将用户和密码提供给第三方应用或分享数据的所有内容。 1)授权码模式 2)简化模式 3)密码模式 4)客户端模式 普通令牌只是一个随机的字符串,没有特殊的意义,

    2024年02月16日
    浏览(46)
  • Spring Security OAuth2 远程命令执行漏洞

    cd vulhub/spring/CVE-2016-4977/ docker-compose up -d 访问 http://192.168.10.171:8080/oauth/authorize?response_type=${233*233}client_id=acmescope=openidredirect_uri=http://test 用admin:admin登陆 出现以下报错,表示漏洞存在(response_type里面的命令执行了) poc.py #!/usr/bin/env python message = input(‘Enter message to encode:’) p

    2024年02月09日
    浏览(37)
  • 微服务安全Spring Security Oauth2实战

    Spring Authorization Server 是一个框架,它提供了 OAuth 2.1 和 OpenID Connect 1.0 规范以及其他相关规范的实现。它建立在 Spring Security 之上,为构建 OpenID Connect 1.0 身份提供者和 OAuth2 授权服务器产品提供了一个安全、轻量级和可定制的基础。说白了,Spring Authorization Server 就是一个**认

    2024年02月03日
    浏览(36)
  • Spring Security与OAuth2的完美结合

    OAuth2是一种流行的授权框架,它允许用户授权第三方应用程序访问他们的资源。Spring Security是一个强大的安全框架,它提供了一系列的安全功能。本文将介绍如何将Spring Security与OAuth2整合,以实现更加安全和可靠的应用程序。 OAuth2的基本概念 OAuth2是一个授权框架,它允许用

    2024年02月05日
    浏览(40)
  • Spring Security实现OAuth2协议及实战

    文章篇幅较长,愿读者耐心看完。如有不足之处,请指正。 一.OAuth2介绍 1.1 OAuth2是什么 怎么用 OAuth2是目前最流行的授权协议,用来授权第三方应用,获取用户数据。 举个例子:快递员想要进入小区,有3种方式。1是业主远程开门,2是业主告诉门禁密码,3是使用令牌(Oaut

    2024年02月08日
    浏览(39)
  • Spring Security对接OIDC(OAuth2)外部认证

    前后端分离项目对接OIDC(OAuth2)外部认证,认证服务器可以使用Keycloak。 后端已有用户管理和权限管理,需要外部认证服务器的用户名和业务系统的用户名一致才可以登录。 后台基于Spring Boot 2.7 + Spring Security 流程: 前台浏览器跳转到  后台地址 + /login/oauth2/authorization/my-oid

    2024年02月21日
    浏览(41)
  • SpringCloud微服务整合Spring Security OAuth2

    首先得了解什么是OAuth2,这个的话可以参见博客: https://blog.csdn.net/weixin_42272869/article/details/112260123 https://www.bilibili.com/video/BV1D94y1Z7t1?p=33vd_source=bf9d70f3d2a451db07f40b6407c95a77 本文采用的是使用最广泛的、安全性最高的 授权码模式 进行讲解。 单独创建一个鉴权微服务auth,负责整个

    2024年02月09日
    浏览(50)
  • spring-security -oauth2 整合 JWT

    在这个基础上,进行整合。 spring security oauth2学习 -- 快速入门_本郡主是喵的博客-CSDN博客 先把  reids,common-pools  等依赖删掉。 删掉redis的下相关配置 1.1 导入依赖 1.2 核心代码 创建 jwtTokenConfig.java 在 AuthenticationServer.java 里面新增这些。  运行,启动!  复制这个token去官网解析

    2024年02月09日
    浏览(55)
  • 【OAuth2系列】Spring Cloud Gateway 作为OAuth2 Client接入第三方单点登录代码实践

            在年初的时候我参与了一个项目,当时是很多家公司协同完成这个项目,其中一个公司专门负责登录这块的内容,需要我们的后端接入他们的单点登录(OAuth2 授权码模式),这块接入工作是由我来负责,我们的项目是微服务架构,经过网上各种查阅资料发现网关作为

    2024年02月04日
    浏览(62)
  • Spring Security oauth2.0微信小程序登录

    微信小程序前期开发准备,可以参考这篇文章微信小程序前期准备 1、学习过Spring Secrity oauth2.0的都知道,他有四种登录模式可以选择 authorization code(授权码模式) implicit(简化模式) resource owner password credentials(密码模式) client credentials(客户端模式) 前三种模式都需要用

    2024年02月10日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包