同步操作将从 heguangchuan/spring-authorization-server 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
OAuth 2.1 规范草案中规定,将移除 Oauth2.0中的密码模式,但是提供了自定义 Grant Type 的入口,下面就实现一个自定义的 密码 模式,其实代码并不复杂,具体 的代码可以参考授权码模式,本文也是根据授权码模式的代码一步一步参考下来的。
在单元测试代码中,新建一个客户端端点用于密码模式的测试,同时这个客户端也支持其他模式登录
@Test
void initClient() {
RegisteredClient passwordClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("password-client")
.clientSecret(passwordEncoder.encode("password-client-secret"))
// 基于请求头的客户端认证方式
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
//基于表单参数的客户端认证方式
//.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
//资源服务器使用该客户端获取授权时支持的方式
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
//密码模式
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 授权码模式回调地址
.redirectUri("https://www.baidu.com")
// 该客户端的授权范围,OPENID与PROFILE是IdToken的scope,获取授权时请求OPENID的scope时认证服务会返回IdToken
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
// 自定scope
.scope("message.read")
//设置客户端 是否需要授权,如果为 false 将不会跳到授权页
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
if (registeredClientRepository.findByClientId(passwordClient.getClientId()) == null) {
registeredClientRepository.save(passwordClient);
}
}
由于 Spring Authorization Server 中的某些工具类不能被外部的其他类引用,直接将源代码中的部分源码拷贝,类名未做改变,可以参考同类名的源码
这个对象的定义完全可以参考 授权码 模式的同义对象,封装一些常用的定义
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class PasswordCredentialsAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
private final String username;
private final String password;
private final Set<String> scopes;
protected PasswordCredentialsAuthenticationToken(String username, String password, Authentication clientPrincipal
, Set<String> scopes, Map<String, Object> additionalParameters) {
super(AuthorizationGrantType.PASSWORD, clientPrincipal, additionalParameters);
Assert.hasText(username, "username cannot be empty");
Assert.hasText(username, "password cannot be empty");
this.username = username;
this.password = password;
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
}
public String getUsername() {
return this.username;
}
@Nullable
public String getPassword() {
return this.password;
}
public Set<String> getScopes() {
return this.scopes;
}
}
该对象将从 请求体 中解析参数,返回上面定义的 AuthenticationToken 对象,同理:代码直接参考授权码模式即可
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import java.util.*;
public class PasswordCredentialsAuthenticationConverter implements AuthenticationConverter {
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantType.PASSWORD.getValue().equals(grantType)) {
return null;
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);
// username (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.USERNAME,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// password (REQUIRED)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(password) && parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.PASSWORD,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) && parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ParameterNames.SCOPE,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.USERNAME) &&
!key.equals(OAuth2ParameterNames.PASSWORD) &&
!key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return new PasswordCredentialsAuthenticationToken(username, password, clientPrincipal, requestedScopes, additionalParameters);
}
}
该对象将 converter 解析的 token 对象解析为 OAuth2AccessTokenAuthenticationToken 对象,代码仍旧参考 授权码 模式即可
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import java.security.Principal;
import java.util.*;
import static com.example.authorization.authentication.OAuth2AuthenticationProviderUtils.getAuthenticatedClientElseThrowInvalidClient;
public class PasswordCredentialsAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.3";
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
public PasswordCredentialsAuthenticationProvider(OAuth2AuthorizationService authorizationService,
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator,
UserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
Assert.notNull(userDetailsService, "userDetailsService cannot be null");
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PasswordCredentialsAuthenticationToken passwordCredentialsAuthenticationToken = (PasswordCredentialsAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordCredentialsAuthenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
//校验授权模式
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_GRANT);
}
//校验 scope
Set<String> authorizedScopes = Collections.emptySet();
if (!CollectionUtils.isEmpty(passwordCredentialsAuthenticationToken.getScopes())) {
for (String requestedScope : passwordCredentialsAuthenticationToken.getScopes()) {
if (!registeredClient.getScopes().contains(requestedScope)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
}
authorizedScopes = new LinkedHashSet<>(passwordCredentialsAuthenticationToken.getScopes());
}
UserDetails userDetails;
try {
userDetails = this.userDetailsService.loadUserByUsername(passwordCredentialsAuthenticationToken.getUsername());
} catch (UsernameNotFoundException e) {
throw new OAuth2AuthenticationException("用户不存在");
}
if (userDetails == null) {
throw new OAuth2AuthenticationException("未找到用户信息");
}
if (!this.passwordEncoder.matches(passwordCredentialsAuthenticationToken.getPassword(), userDetails.getPassword())) {
throw new OAuth2AuthenticationException("用户密码不匹配");
}
Authentication principal = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(principal)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizationGrant(passwordCredentialsAuthenticationToken);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(principal.getName())
.attribute(Principal.class.getName(), principal)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizedScopes(authorizedScopes);
// ----- Access token -----
OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
if (generatedAccessToken == null) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the access token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
if (generatedAccessToken instanceof ClaimAccessor) {
authorizationBuilder.token(accessToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()));
} else {
authorizationBuilder.accessToken(accessToken);
}
// ----- Refresh token -----
OAuth2RefreshToken refreshToken = null;
if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
// Do not issue refresh token to public client
!clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the refresh token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
// ----- ID token -----
OidcIdToken idToken;
if (authorizedScopes.contains(OidcScopes.OPENID)) {
// @formatter:off
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
.authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token
.build();
// @formatter:on
OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext);
if (!(generatedIdToken instanceof Jwt)) {
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
"The token generator failed to generate the ID token.", ERROR_URI);
throw new OAuth2AuthenticationException(error);
}
idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(),
generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims());
authorizationBuilder.token(idToken, (metadata) ->
metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims()));
} else {
idToken = null;
}
OAuth2Authorization authorization = authorizationBuilder.build();
this.authorizationService.save(authorization);
Map<String, Object> additionalParameters = Collections.emptyMap();
if (idToken != null) {
additionalParameters = new HashMap<>();
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
}
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
@Override
public boolean supports(Class<?> authentication) {
return PasswordCredentialsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
至此,自定义模式的核心类就完成,下面配置好即可
在配置文件中,将上面的类加入即可
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
//默认的设置,点击源码其实就是配置了认证端点的csrf校验
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
//配置自定义的token 端点
.tokenEndpoint(endpointConfigurer -> endpointConfigurer.authenticationProviders(authenticationProviders
-> authenticationProviders.add(new PasswordCredentialsAuthenticationProvider(
OAuth2ConfigurerUtils.getAuthorizationService(http)
, OAuth2ConfigurerUtils.getTokenGenerator(http)
, userDetailsService()
, passwordEncoder()
))).accessTokenRequestConverters(authenticationConverters -> authenticationConverters.add(new PasswordCredentialsAuthenticationConverter())))
//授权端点
.authorizationEndpoint(endpoint -> {
//配置自定义的授权页面
endpoint.consentPage("/oauth2/consent");
})
// 开启 OpenID Connect 1.0
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
// 未登录时访问认证端点将被重定向至login页面
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
// 处理使用access token访问用户信息端点和客户端注册端点
.oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
到此为止,自定义 grant type 的整个流程就完成了,通过测试,之前测试的各个功能均能正常得到结果。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。