我正在尝试使用 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]
我尝试了两件事:
解决方案均无效
我建议您避免使用
Date
。而是使用:
LocalDate
用于 日历日期 定义一天(如 birthdate
)Instant
用于 时间戳(如 createdAt
和 updatedAt
)String
日历日期(ISO 格式:yyyy/MM/dd
)Long
表示时间戳(纪元秒或毫秒,具体取决于 OAuth2 / OpenID 所需的精度和约束)所有框架都具有 ISO 格式的日历日期和纪元(毫秒)秒的时间戳的构造函数,并且可以轻松且无歧义地(反)序列化有效负载。