探索 spring-oauth-server 与用户详细信息服务的集成、身份验证和创建用户。使用用户详细信息自定义令牌声明。
在本文中,我们将看到如何自定义身份验证,其中用户详细信息是从另一个组件/服务通过HTTP获取的。将用户详细信息存储为Principal,并在创建令牌时稍后使用它们来自定义JWT中的声明(本文范围仅涵盖两个流程:客户端凭据和代码流)。
该代码可在GitHub上获取上获取。
github.com/naveen-maanju/spring-oauth2-server/tree/OAuthCodeFlow
为了实现这一目标,需要进行以下更改。
密码编码器
从服务中获取用户详细信息的服务/客户端
UserDetails实体
令牌自定义器
密码编码器
需要一个密码编码器来对提供的密码进行编码,以便在验证/登录时根据数据库中存储的编码密码来验证/验证秘密(在注册或更改密码时)。
请参考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主体包含了额外的声明,例如:
roles
isPasswordChangeRequired
userId
ssn
email
username文章来源:https://www.toymoban.com/diary/system/527.html
我们在令牌的自定义器中提供了这些声明。同样,您可以添加任意多个声明。
使用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将具有额外的声明,例如:
roles
isPasswordChangeRequired
userId
ssn
operatorId
email
username
注意:如果您在服务中使用Spring Security,则检查将由安全层处理。我将在另一篇详细介绍中详细介绍如何在oauth2-resource-server中使用Spring Security文章来源地址https://www.toymoban.com/diary/system/527.html
到此这篇关于Spring OAuth服务器:使用UserDetails服务验证用户身份的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!