Spring 授权服务器 - 无法使字段“java.time.Instant#seconds”可访问

问题描述 投票:0回答:1

我正在尝试使用 spring oauth2 授权服务器构建自定义授权服务器。

爪哇:17

Spring引导版本:3.3.1

依赖关系:

  • spring-boot-starter-oauth2-授权服务器

  • spring-boot-starter-security

  • spring-boot-starter-web

  • spring-boot-starter-data-jpa

  • mysql-连接器-j

我创建了一个 OAuth2TokenCustomizer 的 bean ,它看起来像

@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer(JdbcUserInfoService jdbcUserInfoService) {
    return (context) -> {
        if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            context.getClaims().claims((claims) -> {
                Set<String> roles = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities())
                        .stream()
                        .map(c -> c.replaceFirst("^ROLE_", ""))
                        .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
                claims.put("roles", roles);
            });
        }

        if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
            OidcUserInfo userInfo = jdbcUserInfoService.loadByUsername(context.getPrincipal().getName());
            context.getClaims().claims((claims) -> {
                context.getAuthorizedScopes().forEach((scope) -> {
                    switch (scope) {
                        case OidcScopes.EMAIL:
                            claims.put(StandardClaimNames.EMAIL, userInfo.getEmail());
                            claims.put(StandardClaimNames.EMAIL_VERIFIED, userInfo.getEmailVerified());
                            break;
                        case OidcScopes.PHONE:
                            claims.put(StandardClaimNames.PHONE_NUMBER, userInfo.getPhoneNumber());
                            claims.put(StandardClaimNames.PHONE_NUMBER_VERIFIED, userInfo.getPhoneNumberVerified());
                            break;
                        case OidcScopes.ADDRESS:
                            claims.put(StandardClaimNames.ADDRESS, userInfo.getAddress());
                            claims.put(StandardClaimNames.EMAIL, userInfo.getEmail());
                            claims.put(StandardClaimNames.PHONE_NUMBER, userInfo.getPhoneNumber());
                            break;
                        case OidcScopes.PROFILE:
                            claims.put(StandardClaimNames.NAME, userInfo.getPreferredUsername());
                            claims.put(StandardClaimNames.FAMILY_NAME, userInfo.getFamilyName());
                            claims.put(StandardClaimNames.GIVEN_NAME, userInfo.getGivenName());
                            claims.put(StandardClaimNames.MIDDLE_NAME, userInfo.getMiddleName());
                            claims.put(StandardClaimNames.PICTURE, userInfo.getPicture());
                            claims.put(StandardClaimNames.UPDATED_AT, userInfo.getUpdatedAt());
                            break;
                    }
                });
            });
        }
    };
}

UserInfoEntity 看起来像:

import jakarta.persistence.*;
import lombok.Data;

import java.time.Instant;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.UUID;

@Data
@Entity
@Table(name = "`user_details`")
public class UserInfoEntity {
    @Id
    private String id;
    @Column(unique = true, length = 50, nullable = false)
    private String username;
    @Column(length = 50, nullable = false)
    private String givenName;
    @Column(length = 50, nullable = false)
    private String familyName;
    @Column(length = 50)
    private String middleName;
    @Column(length = 1000)
    private String picture;
    @Column(length = 100, unique = true, nullable = false)
    private String email;
    private Boolean emailVerified;
    @Column(length = 50)
    private String phoneNumber;
    private Boolean phoneNumberVerified;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    @Temporal(TemporalType.DATE)
    private Date birthdate;
    private TimeZone zoneInfo;
    private Locale locale;
    @Column(length = 500)
    private String address;
    @Temporal(TemporalType.TIMESTAMP)
    private Date updatedAt;
    @Temporal(TemporalType.TIMESTAMP)
    private Date createdAt;

    public enum Gender {
        MALE,
        FEMALE,
        PREFER_NOT_TO_SAY;
    }

    @PrePersist
    public void defaults() {
        Date currentDate = Date.from(Instant.now());
        this.id = UUID.randomUUID().toString();

        if (this.createdAt == null)
            this.createdAt = currentDate;
        this.updatedAt = currentDate;

        if (this.gender == null)
            this.gender = Gender.PREFER_NOT_TO_SAY;
    }
}

JdbcUserInfoService:

import in.anekdote.authn.entity.UserInfoEntity;
import in.anekdote.authn.repository.JdbcUserInfoRepository;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.stereotype.Service;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

@Service
public class JdbcUserInfoServiceImpl implements JdbcUserInfoService {
    private final JdbcUserInfoRepository userInfoRepository;

    public JdbcUserInfoServiceImpl(JdbcUserInfoRepository userInfoRepository) {
        this.userInfoRepository = userInfoRepository;
    }

    @Override
    public OidcUserInfo loadByUsername(String username) {
        UserInfoEntity userInfoEntity = this.userInfoRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException(username));

        return this.getOidcUserInfoFromEntity(userInfoEntity);
    }

    private OidcUserInfo getOidcUserInfoFromEntity(UserInfoEntity userInfoEntity) {
        String birthdate = null;
        String zoneInfo = null;
        String locale = null;
        String updatedAt = null;

        if (userInfoEntity.getBirthdate() != null) {
            birthdate = this.dateToString(new Date(userInfoEntity.getBirthdate().getTime()));
        }

        if (userInfoEntity.getZoneInfo() != null) {
            zoneInfo = userInfoEntity.getZoneInfo().getDisplayName();
        }

        if (userInfoEntity.getLocale() != null) {
            locale = userInfoEntity.getLocale().getDisplayName();
        }

        if (userInfoEntity.getUpdatedAt() != null) {
            updatedAt = this.dateToString(new Date(userInfoEntity.getUpdatedAt().getTime()));
        }

        return OidcUserInfo.builder()
                .subject(userInfoEntity.getUsername())
                .preferredUsername(userInfoEntity.getUsername())
                .givenName(userInfoEntity.getGivenName())
                .familyName(userInfoEntity.getFamilyName())
                .middleName(userInfoEntity.getMiddleName())
                .picture(userInfoEntity.getPicture())
                .email(userInfoEntity.getEmail())
                .emailVerified(userInfoEntity.getEmailVerified())
                .phoneNumber(userInfoEntity.getPhoneNumber())
                .phoneNumberVerified(userInfoEntity.getPhoneNumberVerified())
                .gender(userInfoEntity.getGender().name())
                .birthdate(birthdate)
                .zoneinfo(zoneInfo)
                .locale(locale)
                .address(userInfoEntity.getAddress())
                .updatedAt(updatedAt)
                .build();
    }

    private String dateToString(Date date) {
        if (date != null) {
            DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            return dateFormat.format(date);
        }

        return null;
    }
}

当我尝试获取令牌时,出现以下错误:

com.nimbusds.jose.shaded.gson.JsonIOException: Failed making field 'java.time.Instant#seconds' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
See https://github.com/google/gson/blob/main/Troubleshooting.md#reflection-inaccessible
    at com.nimbusds.jose.shaded.gson.internal.reflect.ReflectionHelper.makeAccessible(ReflectionHelper.java:76) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.internal.bind.ReflectiveTypeAdapterFactory.getBoundFields(ReflectiveTypeAdapterFactory.java:388) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.internal.bind.ReflectiveTypeAdapterFactory.create(ReflectiveTypeAdapterFactory.java:161) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.Gson.getAdapter(Gson.java:628) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.write(TypeAdapterRuntimeTypeWrapper.java:57) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:222) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.internal.bind.MapTypeAdapterFactory$Adapter.write(MapTypeAdapterFactory.java:154) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.Gson.toJson(Gson.java:944) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.Gson.toJson(Gson.java:899) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.Gson.toJson(Gson.java:848) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.shaded.gson.Gson.toJson(Gson.java:825) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.util.JSONObjectUtils.toJSONString(JSONObjectUtils.java:541) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.Payload.toString(Payload.java:363) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.Payload.toBytes(Payload.java:395) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.Payload.toBase64URL(Payload.java:412) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.JWSObject.composeSigningInput(JWSObject.java:193) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jose.JWSObject.<init>(JWSObject.java:112) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at com.nimbusds.jwt.SignedJWT.<init>(SignedJWT.java:60) ~[nimbus-jose-jwt-9.39.3.jar:9.39.3]
    at org.springframework.security.oauth2.jwt.NimbusJwtEncoder.serialize(NimbusJwtEncoder.java:146) ~[spring-security-oauth2-jose-6.3.1.jar:6.3.1]
    at org.springframework.security.oauth2.jwt.NimbusJwtEncoder.encode(NimbusJwtEncoder.java:111) ~[spring-security-oauth2-jose-6.3.1.jar:6.3.1]
    at org.springframework.security.oauth2.server.authorization.token.JwtGenerator.generate(JwtGenerator.java:187) ~[spring-security-oauth2-authorization-server-1.3.1.jar:1.3.1]
    at org.springframework.security.oauth2.server.authorization.token.JwtGenerator.generate(JwtGenerator.java:61) ~[spring-security-oauth2-authorization-server-1.3.1.jar:1.3.1]
    at org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator.generate(DelegatingOAuth2TokenGenerator.java:59) ~[spring-security-oauth2-authorization-server-1.3.1.jar:1.3.1]
    at org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationProvider.authenticate(OAuth2AuthorizationCodeAuthenticationProvider.java:267) ~[spring-security-oauth2-authorization-server-1.3.1.jar:1.3.1]
    at org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182) ~[spring-security-core-6.3.1.jar:6.3.1]

我尝试了两件事:

  1. 为了解决这个问题,我为 Instant 类编写了一个自定义 TypeAdapter 来处理其序列化和反序列化。
  2. 我尝试在项目模块中添加 module-info.java 文件,并尝试将 java.time 打开到 com.nimbusds.jose

解决方案均无效

java spring-boot spring-security spring-authorization-server nimbus-jose-jwt
1个回答
0
投票

我建议您避免使用

Date
。而是使用:

  • 在您的实体(使用 JPA 保存在数据库中的域对象)中使用:
    • LocalDate
      用于 日历日期 定义一天(如
      birthdate
    • Instant
      用于 时间戳(如
      createdAt
      updatedAt
  • 在 DTO(HTTP 请求有效负载)中,使用:
    • String
      日历日期(ISO 格式:
      yyyy/MM/dd
    • Long
      表示时间戳(纪元秒或毫秒,具体取决于 OAuth2 / OpenID 所需的精度和约束)

所有框架都具有 ISO 格式的日历日期和纪元(毫秒)秒的时间戳的构造函数,并且可以轻松且无歧义地(反)序列化有效负载。

© www.soinside.com 2019 - 2024. All rights reserved.