LDAP-Grant-Type
一、前言
二、分析
三、编码
1. LdapGrantAuthenticationToken
package com.light.sas.authorization.ldap;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 自定义LDAP登录Token类
*/
public class LdapGrantAuthenticationToken extends AbstractAuthenticationToken {
/**
* 本次登录申请的scope
*/
private final Set<String> scopes;
/**
* 客户端认证信息
*/
private final Authentication clientPrincipal;
/**
* 当前请求的参数
*/
private final Map<String, Object> additionalParameters;
/**
* 认证方式
*/
private final AuthorizationGrantType authorizationGrantType;
public LdapGrantAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal,
@Nullable Set<String> scopes,
@Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
this.scopes = Collections.unmodifiableSet(
scopes != null ?
new HashSet<>(scopes) :
Collections.emptySet());
this.clientPrincipal = clientPrincipal;
this.additionalParameters = Collections.unmodifiableMap(
additionalParameters != null ?
new HashMap<>(additionalParameters) :
Collections.emptyMap());
this.authorizationGrantType = authorizationGrantType;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return clientPrincipal;
}
/**
* 返回请求的scope(s)
*
* @return 请求的scope(s)
*/
public Set<String> getScopes() {
return this.scopes;
}
/**
* 返回请求中的authorization grant type
*
* @return authorization grant type
*/
public AuthorizationGrantType getAuthorizationGrantType() {
return this.authorizationGrantType;
}
/**
* 返回请求中的附加参数
*
* @return 附加参数
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}
2. LdapGrantAuthenticationConverter
package com.light.sas.authorization.ldap;
import com.light.sas.constant.LdapParameterNames;
import com.light.sas.utils.SecurityUtils;
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.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* LDAP登录Token转换器
*/
public class LdapGrantAuthenticationConverter implements AuthenticationConverter {
static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
@Override
public Authentication convert(HttpServletRequest request) {
// grant_type (REQUIRED)
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!LdapParameterNames.GRANT_TYPE_LDAP.equals(grantType)) {
return null;
}
// 这里目前是客户端认证信息
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
// 获取请求中的参数
MultiValueMap<String, String> parameters = SecurityUtils.getFormParameters(request);
// scope (OPTIONAL)
String scope = parameters.getFirst(OAuth2ParameterNames.SCOPE);
if (StringUtils.hasText(scope) &&
parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) {
SecurityUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Parameter: " + OAuth2ParameterNames.SCOPE,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
Set<String> requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// Mobile phone number (REQUIRED)
String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);
if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {
SecurityUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Parameter: " + OAuth2ParameterNames.USERNAME,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// SMS verification code (REQUIRED)
String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);
if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {
SecurityUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Parameter: " + OAuth2ParameterNames.PASSWORD,
ACCESS_TOKEN_REQUEST_ERROR_URI);
}
// 提取附加参数
Map<String, Object> additionalParameters = new HashMap<>();
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&
!key.equals(OAuth2ParameterNames.CLIENT_ID)) {
additionalParameters.put(key, value.get(0));
}
});
// 构建AbstractAuthenticationToken子类实例并返回
return new LdapGrantAuthenticationToken(new AuthorizationGrantType(LdapParameterNames.GRANT_TYPE_LDAP), clientPrincipal, requestedScopes, additionalParameters);
}
}
3. AuthenticationProviderAdapter
package com.light.sas.authorization.baisc.adapter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
/**
* 短信验证码登录认证提供者
*/
@Slf4j
public abstract class AuthenticationProviderAdapter implements AuthenticationProvider {
protected OAuth2TokenGenerator<?> tokenGenerator;
protected AuthenticationManager authenticationManager;
protected OAuth2AuthorizationService authorizationService;
protected AuthenticationProvider authenticationProvider;
protected static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
protected static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
public void setTokenGenerator(OAuth2TokenGenerator<?> tokenGenerator) {
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.tokenGenerator = tokenGenerator;
}
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
Assert.notNull(authorizationService, "authenticationManager cannot be null");
this.authenticationManager = authenticationManager;
}
public void setAuthenticationProvider(AuthenticationProvider authenticationProvider) {
Assert.notNull(authenticationProvider, "authenticationProvider cannot be null");
this.authenticationProvider = authenticationProvider;
}
public void setAuthorizationService(OAuth2AuthorizationService authorizationService) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
this.authorizationService = authorizationService;
}
}
4. LdapGrantAuthenticationProvider
package com.light.sas.authorization.ldap;
import com.light.sas.authorization.baisc.adapter.AuthenticationProviderAdapter;
import com.light.sas.utils.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
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.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.util.ObjectUtils;
import java.security.Principal;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* LDAP登录认证提供者
*/
@Slf4j
public class LdapGrantAuthenticationProvider extends AuthenticationProviderAdapter {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
LdapGrantAuthenticationToken authenticationToken = (LdapGrantAuthenticationToken) authentication;
// Ensure the client is authenticated
OAuth2ClientAuthenticationToken clientPrincipal =
SecurityUtils.getAuthenticatedClientElseThrowInvalidClient(authenticationToken);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
// Ensure the client is configured to use this authorization grant type
if (registeredClient == null || !registeredClient.getAuthorizationGrantTypes().contains(authenticationToken.getAuthorizationGrantType())) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
// 验证scope
Set<String> authorizedScopes = getAuthorizedScopes(registeredClient, authenticationToken.getScopes());
// 进行认证
Authentication authenticate = getAuthenticatedUser(authenticationToken);
// 以下内容摘抄自OAuth2AuthorizationCodeAuthenticationProvider
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(authenticate)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(authenticationToken.getAuthorizationGrantType())
.authorizationGrant(authenticationToken);
// Initialize the OAuth2Authorization
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
// 2023-07-15修改逻辑,加入当前用户认证信息,防止刷新token时因获取不到认证信息而抛出空指针异常
// 存入授权scope
.authorizedScopes(authorizedScopes)
// 当前授权用户名称
.principalName(authenticate.getName())
// 设置当前用户认证信息
.attribute(Principal.class.getName(), authenticate)
.authorizationGrantType(authenticationToken.getAuthorizationGrantType());
// ----- 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);
}
if (log.isTraceEnabled()) {
log.trace("Generated access token");
}
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);
}
if (log.isTraceEnabled()) {
log.trace("Generated refresh token");
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
// ----- ID token -----
OidcIdToken idToken;
if (authorizedScopes.contains(OidcScopes.OPENID)) {
tokenContext = tokenContextBuilder
.tokenType(ID_TOKEN_TOKEN_TYPE)
// ID token customizer may need access to the access token and/or refresh token
.authorization(authorizationBuilder.build())
.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);
}
if (log.isTraceEnabled()) {
log.trace("Generated id token");
}
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();
// Save the OAuth2Authorization
this.authorizationService.save(authorization);
Map<String, Object> additionalParameters = new HashMap<>(1);
if (idToken != null) {
// 放入idToken
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
}
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
/**
* 获取认证过的scope
*
* @param registeredClient 客户端
* @param requestedScopes 请求中的scope
* @return 认证过的scope
*/
private Set<String> getAuthorizedScopes(RegisteredClient registeredClient, Set<String> requestedScopes) {
// Default to configured scopes
Set<String> authorizedScopes = registeredClient.getScopes();
if (!ObjectUtils.isEmpty(requestedScopes)) {
Set<String> unauthorizedScopes = requestedScopes.stream()
.filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope))
.collect(Collectors.toSet());
if (!ObjectUtils.isEmpty(unauthorizedScopes)) {
SecurityUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"OAuth 2.0 Parameter: " + OAuth2ParameterNames.SCOPE,
ERROR_URI);
}
authorizedScopes = new LinkedHashSet<>(requestedScopes);
}
if (log.isTraceEnabled()) {
log.trace("Validated token request parameters");
}
return authorizedScopes;
}
/**
* 获取认证过的用户信息
*
* @param authenticationToken converter构建的认证信息,这里是包含手机号与验证码的
* @return 认证信息
*/
public Authentication getAuthenticatedUser(LdapGrantAuthenticationToken authenticationToken) {
// 获取手机号密码
Map<String, Object> additionalParameters = authenticationToken.getAdditionalParameters();
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
// 构建UsernamePasswordAuthenticationToken通过AbstractUserDetailsAuthenticationProvider及其子类对手机号与验证码进行校验
// 这里就是我说的短信验证与密码模式区别不大,如果是短信验证模式则在SmsCaptchaLoginAuthenticationProvider中加一个校验,
// 使框架支持手机号、验证码校验,反之不加就是账号密码登录
UsernamePasswordAuthenticationToken unauthenticated = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
Authentication authenticate = null;
try {
authenticate = authenticationProvider.authenticate(unauthenticated);
} catch (Exception e) {
SecurityUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
"认证失败:用户名或密码错误.",
ERROR_URI
);
}
return authenticate;
}
@Override
public boolean supports(Class<?> authentication) {
return LdapGrantAuthenticationToken.class.isAssignableFrom(authentication);
}
}