Spring OAuth服务器:使用UserDetails服务验证用户身份

探索 spring-oauth-server 与用户详细信息服务的集成、身份验证和创建用户。使用用户详细信息自定义令牌声明。

在本文中,我们将看到如何自定义身份验证,其中用户详细信息是从另一个组件/服务通过HTTP获取的。将用户详细信息存储为Principal,并在创建令牌时稍后使用它们来自定义JWT中的声明(本文范围仅涵盖两个流程:客户端凭据和代码流)。

该代码可在GitHub上获取上获取。

github.com/naveen-maanju/spring-oauth2-server/tree/OAuthCodeFlow

为了实现这一目标,需要进行以下更改。

  1. 密码编码器

  2. 从服务中获取用户详细信息的服务/客户端

  3. UserDetails实体

  4. 令牌自定义器

密码编码器

需要一个密码编码器来对提供的密码进行编码,以便在验证/登录时根据数据库中存储的编码密码来验证/验证秘密(在注册或更改密码时)。

请参考D3PasswordEncoder。
https://github.com/naveen-maanju/spring-oauth2-server/blob/OAuthCodeFlow/src/main/java/org/d3softtech/oauth2/server/crypto/password/D3PasswordEncoder.java

用于获取UserDetails的服务/客户端 

需要一个bean/服务来提供自定义的UserDetails。该服务可以提供硬编码的用户详细信息,从内存存储中获取,或者通过调用另一个服务来获取。在此示例中,我们将重点介绍调用另一个服务(user-detail-service)。

oauth-server中的用户详细信息服务bean实现了spring-security提供的UserDetailsService(因为oauth-server是基于spring-security构建的)。

@Service
public class D3UserDetailsService implements UserDetailsService {

  private final WebClient webClient;

  public D3UserDetailsService(@Value("${user.details.service.base.url}") String userServiceBaseUrl) {
    webClient = WebClient.builder().baseUrl(userServiceBaseUrl).build();

  }

  public UserDetails loadUserByUsername(String username) {
    D3User user = webClient.get()
        .uri(uriBuilder -> uriBuilder.path("/users").path("/{username}").build(username))
        .retrieve()
        .onStatus(httpStatusCode -> httpStatusCode.isSameCodeAs(HttpStatus.NOT_FOUND),
            clientResponse -> Mono.error(new D3Exception("Bad credentials")))
        .bodyToMono(D3User.class).block(
            Duration.ofSeconds(2));

    return new D3UserDetails(user.userId(), user.username(), user.password(), getAuthorities(user.roles()), user.ssn(),
        user.email(), user.isPasswordChangeRequired(), user.roles());
  }


  private List<GrantedAuthority> getAuthorities(List<String> roles) {
    List<GrantedAuthority> authorities = new ArrayList<>(roles.size());
    for (String role : roles) {
      authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
    }
    return authorities;
  }

  @JsonIgnoreProperties(ignoreUnknown = true)
  @Builder
  public record D3User(@JsonProperty("id") Integer userId, @JsonProperty("userName") String username,
                       String password, List<String> roles, String ssn, String email,
                       boolean isPasswordChangeRequired) {

  }
  
}

UserDetails实体

可以定义一个UserDetails实体(不是必须的,除非您想向经过身份验证的用户上下文添加更多详细信息):

@Getter
public class D3UserDetails extends User {

  private final Integer userId;
  private final boolean isPasswordChangeRequired;
  private final List<String> roles;
  private final String ssn;
  private final String email;

  public D3UserDetails(Integer userId, String username, String password, List<GrantedAuthority> authorities,
      String ssn, String email, boolean isPasswordChangeRequired, List<String> roles) {
    super(username, password, authorities);
    this.userId = userId;
    this.ssn = ssn;
    this.email = email;
    this.isPasswordChangeRequired = isPasswordChangeRequired;
    this.roles = roles;
  }
}

此D3UserDetails实体扩展了Spring Security User实体,并提供了附加属性。

令牌自定义器

需要令牌自定义器为access_token提供附加属性/声明:

自包含的JWT

如果access_token格式是自包含的,则需要一个实现Auth2TokenCustomizer<JwtEncodingContext>的自定义器。

public class OAuth2JWTTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {

  private static final Consumer<JwtEncodingContext> AUTHORIZE_CODE_FLOW_CUSTOMIZER = (jwtContext) -> {
    if (AUTHORIZATION_CODE.equals(jwtContext.getAuthorizationGrantType()) && ACCESS_TOKEN.equals(
        jwtContext.getTokenType())) {
      UsernamePasswordAuthenticationToken authenticatedUserToken = jwtContext.getPrincipal();
      D3UserDetails userDetails = (D3UserDetails) authenticatedUserToken.getPrincipal();
      Map.of("userId", userDetails.getUserId(),
              "username", userDetails.getUsername(),
              "isPasswordChangeRequired", userDetails.isPasswordChangeRequired(),
              "roles", userDetails.getRoles(),
              "ssn", userDetails.getSsn(),
              "email", userDetails.getEmail())
          .forEach((key, value) -> jwtContext.getClaims().claim(key, value));
    }
  };

  private static final Consumer<JwtEncodingContext> CLIENT_CREDENTIALS_FLOW_CUSTOMIZER = (jwtContext) -> {
    if (CLIENT_CREDENTIALS.equals(jwtContext.getAuthorizationGrantType()) && ACCESS_TOKEN.equals(
        jwtContext.getTokenType())) {
      OAuth2ClientCredentialsAuthenticationToken clientCredentialsAuthentication = jwtContext.getAuthorizationGrant();
      Map<String, Object> additionalParameters = clientCredentialsAuthentication.getAdditionalParameters();
      additionalParameters.forEach((key, value) -> jwtContext.getClaims().claim(key, value));
    }
  };

  private final Consumer<JwtEncodingContext> jwtEncodingContextCustomizers = AUTHORIZE_CODE_FLOW_CUSTOMIZER.andThen(
      CLIENT_CREDENTIALS_FLOW_CUSTOMIZER);

  @Override
  public void customize(JwtEncodingContext context) {
    jwtEncodingContextCustomizers.accept(context);
  }

由于客户端凭证流程始终是自包含的,因此我们必须在JWTToken中添加对其的支持,以及代码流。在代码流的情况下,我们对用户进行身份验证,并使用从UserService获取的用户详细信息作为JWT中的附加声明。而在客户端凭证流程的情况下,附加参数将作为请求参数提供。

不透明令牌

如果access_token格式是引用的,则需要一个实现OAuth2TokenCustomizer<OAuth2TokenClaimsContext>的自定义器。

@Component
public class OAuth2OpaqueTokenIntrospectionResponseCustomizer implements
    OAuth2TokenCustomizer<OAuth2TokenClaimsContext> {

  private static final Consumer<OAuth2TokenClaimsContext> INTROSPECTION_TOKEN_CLAIMS_CUSTOMIZER = (claimsContext) -> {
    if (AUTHORIZATION_CODE.equals(claimsContext.getAuthorizationGrantType()) && ACCESS_TOKEN.equals(
        claimsContext.getTokenType())) {
      UsernamePasswordAuthenticationToken authenticatedUserToken = claimsContext.getPrincipal();
      D3UserDetails userDetails = (D3UserDetails) authenticatedUserToken.getPrincipal();
      Map.of("userId", userDetails.getUserId(),
              "username", userDetails.getUsername(),
              "isPasswordChangeRequired", userDetails.isPasswordChangeRequired(),
              "roles", userDetails.getRoles(),
              "ssn", userDetails.getSsn(),
              "email", userDetails.getEmail())
          .forEach((key, value) -> claimsContext.getClaims().claim(key, value));
    }
  };

  private final Consumer<OAuth2TokenClaimsContext> claimsContextCustomizer = INTROSPECTION_TOKEN_CLAIMS_CUSTOMIZER;

  @Override
  public void customize(OAuth2TokenClaimsContext jwtContext) {
    claimsContextCustomizer.accept(jwtContext);
  }
}

由于引用令牌与代码流相关联,在成功认证后,当代码被交换为令牌时,授权服务器发布的access_token将不是JWT,而是一个引用。这个引用应该通过introspection端点使用用户详细信息声明和其他声明来交换为access_token。可以参考这里的一个可行的函数测试。

这里有一个GitHub上的可行示例。
https://github.com/naveen-maanju/spring-oauth2-server/tree/OAuthCodeFlow/src/main/java/org/d3softtech/oauth2/server/token/customizer

对于自包含的情况,在代码流的最后,access_token将以包含通过自定义器添加的所有附加声明的JWT形式存在。而对于不透明令牌(引用),需要使用introspection调用来获取响应中以声明形式表示的UserDetails。

响应的样子是怎样的? 

您可以通过在GitHub上添加的测试进行验证:它包含两个涵盖两种情况的测试方法。

自包含的JWT

代码流令牌响应

{
   "access_token":"eyJraWQiOiIxNzdjMzA1MC1lMGY2LTQ4NDctYjJiNy02NTY2ZDVlZGZiMWUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkM3VzZXIiLCJyb2xlcyI6WyJhZG1pbiIsInVzZXIiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MDYwIiwiaXNQYXNzd29yZENoYW5nZVJlcXVpcmVkIjp0cnVlLCJ1c2VySWQiOjEyMywic3NuIjoiMTk3NjExMTE5ODc3IiwiYXVkIjoic3ByaW5nLXRlc3QiLCJuYmYiOjE2OTkzNDcyODMsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJlbWFpbCJdLCJleHAiOjE2OTkzNDc1ODMsImlhdCI6MTY5OTM0NzI4MywiZW1haWwiOiJ0ZXN0LXVzZXJAZDNzb2Z0dGVjaC5jb20iLCJ1c2VybmFtZSI6ImQzdXNlciJ9.RQiLWmGf9_rV4UfKzKomEhuJrncG08a2F34mN-gPDw7vK2csRPGMMDRYh2Gm0Eh-n3JRTaJ9_twdPQG9BgQifKiubPsM_etxpxKLLfQHoTfqzguiP8D53FyXLB9xwhvAgKH0KWLOSRxl-bdZsctpVZpqrMTPZtfdlt7tqcl71tGDY-7Nri76Kod39kyVcKEAuLNNZKt4fhn8tCLUA64jKfmKPM3afmAdvf0PlEwgwqhGhojxtCLnYNtzuO_VQheTaQvZxrzcXw3gNRnO4vppedAyG1gmUV44l4u7cXdhG-vGc1ItU45PSg3EaG7BtHU1axKu3qHB8C7mHAhk3zVuUA",
   "refresh_token":"t9U3CDejVC2k_eNtyvM23RTN3ePpS9x8b8_pVrD-U-ivLij0dWt9NZVO9wn-kIsyr89Yj-fBFpH8BFZoMUIqGI_wZSmKgYqpO0SmNE-C1_hW8DVLqT8zQ7PkhF_Gil7N",
   "scope":"openid profile email",
   "token_type":"Bearer",
   "expires_in":299
}

AccessToken JWT声明如下所示:

{
  "sub": "d3user",
  "roles": [
    "admin",
    "user"
  ],
  "iss": "http://localhost:6060",
  "isPasswordChangeRequired": true,
  "userId": 123,
  "ssn": "197611119877",
  "aud": "spring-test",
  "nbf": 1699347283,
  "scope": [
    "openid",
    "profile",
    "email"
  ],
  "exp": 1699347583,
  "iat": 1699347283,
  "email": "test-user@d3softtech.com",
  "username": "d3user"
}

我们可以看到JWT主体包含了额外的声明,例如:

  1. roles 

  2. isPasswordChangeRequired 

  3. userId 

  4. ssn 

  5. email 

  6. username

我们在令牌的自定义器中提供了这些声明。同样,您可以添加任意多个声明。

使用access_token的检查响应

{
   "active":true,
   "client_id":"spring-test",
   "iat":1698757155,
   "exp":1698760755
}

默认情况下,/oauth2/introspect的响应只会返回access_token的状态。如果需要的话,它也可以进行自定义。

不透明令牌

代码流:代码交换响应

{
   "access_token":"vbHFMLGQPmqAWWOzjLoYNu_RG1jBHc7oifI9Hl9N1eCyG3jdzTgAoN8YXAAK-GfEy1CUhokTAnM2aC4GsDe07OgPBpI_sAGHP60pQgbTDTyBUJj2jO1inIi0FoCpmPcj",
   "refresh_token":"Rj8CpnQexjtFJzCPFJUmhKGVmgdFAJ6RLMB_h6SwYgDItPLwSu6AR7CZ3WpIEQthm7pGEpis7NlrarvIHX5YjwBX6wGwWpwfnIKVSa0OJYJqhFsZfFvOmn8sypi4DS4b",
   "scope":"openid profile email",
   "token_type":"Bearer",
   "expires_in":299
}

在代码流的最后,您将获得封装了access_token、refresh_token、scope、token_type和expires_in的JSON响应。

要获取经过身份验证用户的声明,我们必须针对spring-oauth-server调用/oauth2/introspect端点。

使用access_token的检查响应(无自定义器)

{
   "active":true,
   "sub":"d3user",
   "aud":[
      "spring-reference"
   ],
   "nbf":1698755697,
   "scope":"openid profile email",
   "iss":"http://localhost:6060",
   "exp":1698755997,
   "iat":1698755697,
   "jti":"2b4165c0-68f3-4e3d-b67e-d50c3f7b6110",
   "client_id":"spring-reference",
   "token_type":"Bearer"
}

没有自定义器,它具有所有默认声明,例如代码流中经过身份验证的用户的状态为“active”和主题(sub)。

使用access_token的检查响应(带有自定义器)

{
   "active":true,
   "sub":"d3user",
   "roles":[
      "admin",
      "user"
   ],
   "iss":"http://localhost:6060",
   "isPasswordChangeRequired":true,
   "userId":123,
   "ssn":"197611119877",
   "aud":[
      "spring-reference"
   ],
   "nbf":1698755588,
   "scope":"openid profile email",
   "exp":1698755888,
   "iat":1698755588,
   "operatorId":"197611119877",
   "jti":"c0560938-c413-44f7-a01b-9cbc119eae58",
   "email":"test-user@d3softtech.com",
   "username":"d3user",
   "client_id":"spring-reference",
   "token_type":"Bearer"
}

使用自定义器,access_token将具有额外的声明,例如:

  1. roles

  2. isPasswordChangeRequired

  3. userId

  4. ssn

  5. operatorId

  6. email

  7. username

注意:如果您在服务中使用Spring Security,则检查将由安全层处理。我将在另一篇详细介绍中详细介绍如何在oauth2-resource-server中使用Spring Security文章来源地址https://www.toymoban.com/diary/system/527.html

到此这篇关于Spring OAuth服务器:使用UserDetails服务验证用户身份的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

原文地址:https://www.toymoban.com/diary/system/527.html

如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用
html+css代码和svg两种方式实现爱心图形
上一篇 2023年11月14日 16:09
JWT 介绍及相关概念 (JWS、JWE、JWA、JWK)
下一篇 2023年11月16日 01:15

相关文章

  • Spring OAuth2 授权服务器配置详解

    首先要创建一个Spring Boot Servlet Web项目,这个不难就不赘述了。集成 Spring Authorization Server 需要引入: OAuth2.0 Client 客户端需要注册到授权服务器并持久化, Spring Authorization Server 提供了 JDBC 实现,参见 JdbcRegisteredClientRepository 。为了演示方便这里我采用了H2数据库,需要以下依

    2024年04月13日
    浏览(32)
  • Spring Security OAuth 2.0 资源服务器— JWT

    目录 一、JWT的最小依赖 二、JWT的最基本配置 1、指定授权服务器 2、初始预期(Startup Expectations) 3、运行时预期(Runtime Expectations) 三、JWT认证是如何工作的 四、直接指定授权服务器 JWK Set Uri 五、提供 audiences 六、覆盖或取代启动自动配置 1、使用jwkSetUri() 2、使用decoder()

    2024年02月05日
    浏览(44)
  • 搭建spring security oauth2认证授权服务器

    下面是在spring security项目的基础上搭建spring security oauth2认证授权服务器 spring security oauth2认证授权服务器主要需要以下依赖 Spring Security对OAuth2默认可访问端点 ​/oauth/authorize​ ​​:申请授权码code,涉及类​ ​AuthorizationEndpoint​ ​ ​/oauth/token​ ​​:获取令牌token,涉及类​

    2024年01月21日
    浏览(44)
  • Spring Boot OAuth2 认证服务器搭建及授权码认证演示

    本篇使用JDK版本是1.8,需要搭建一个OAuth 2.0的认证服务器,用于实现各个系统的单点登录。 这里选择Spring Boot+Spring Security + Spring Authorization Server 实现,具体的版本选择如下: Spirng Boot 2.7.14 , Spring Boot 目前的最新版本是 3.1.2,在官方的介绍中, Spring Boot 3.x 需要JDK 17及以上的

    2024年02月15日
    浏览(38)
  • OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client

    title: OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client date: 2023-03-27 01:41:26 tags: OAuth2.0 Spring Authorization Server categories: 开发实践 cover: https://cover.png feature: false 目前 Spring 生态中的 OAuth2 授权服务器是 Spring Authorization Server ,原先的 Spring Security OAuth 已经停止更新

    2024年02月08日
    浏览(46)
  • SpringSecurity学习(八)OAuth2.0、授权服务器、资源服务器、JWT令牌的使用

    OAuth2是一个认证协议,SpringSecurity对OAuth2协议提供了响应的支持,开发者可以非常方便的使用OAuth2协议。 简介 四种授权模式 Spring Security OAuth2 GitHub授权登录 授权服务器与资源服务器 使用JWT OAuth是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密资源

    2024年02月02日
    浏览(46)
  • aaa远程登陆服务器验证

    实 验设备与软件环境 Eve虚拟机,vm 交换机地址192.168.123.131 Radius服务器地址192.168.123.128 拓扑图: 实验过程与结果(可贴图) Radius服务器部分: 在Ubuntu中下载freeradius并配置三个文件 sudo apt update   #更新源,注意流量 sudo apt install freeradius   #下载freeradius注意流量 1Vim /etc/fr

    2024年03月21日
    浏览(46)
  • 关于ipad:无法验证服务器身份

    ipad 连接网络后,有时候会冒出这个弹窗,并且关掉后仍继续弹出 可以尝试以下几种方法:(我是用③解决的) ①. 确保你的iPad连接的是稳定的网络。有时候网络连接不稳定会导致无法验证服务器身份。 我们学校这个校园网很恶心 ↓,凡是果子的设备,经常连接不上,即使

    2024年02月15日
    浏览(32)
  • 已拒绝远程连接,因为未识别出你提供的用户名和密码组合或在远程访问服务器上禁止使用选定的身份验证协议

    VPN连接时,提示:已拒绝远程连接,因为未识别出你提供的用户名和密码组合或在远程访问服务器上禁止使用选定的身份验证协议。  按下面的操作:  

    2024年02月11日
    浏览(35)
  • Windows server : NPS服务(为“微劈恩“搭建身份验证服务器)

    !!!由于微劈恩直接说英文不过审所以用谐音字代替!!! 实验环境:和上一个搭建微劈嗯的一样 微劈恩服务器可以和另外两个服务器通信 一个Windows 10 一个Windows server 2016 (微劈嗯服务器) 一个Windows server 2016(NPS服务器) ip这样安排的 先创建好微劈嗯服务器 1.安装np

    2024年02月09日
    浏览(39)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包