- 作者:老汪软件技巧
- 发表时间:2024-12-10 04:04
- 浏览量:
一、引言
随着 OAuth 2.1 标准的推广,密码模式(Password Grant)因安全性问题逐渐被弃用。然而,在许多实际场景中,仍存在对密码模式的需求。那么,为什么在 Spring Authorization Server 中需要自定义密码模式?它的实现有哪些挑战和注意点?本文将从密码模式的背景、需求以及技术实现三方面展开分析。
二、密码模式的背景与现状1. 密码模式的定义
优点:简单易用,特别适合第一方应用。
缺点:直接暴露用户名和密码,容易被攻击。
2. OAuth 2.1 标准不再推荐密码模式
Spring Authorization Server 默认不支持密码模式。
3. 为什么还有密码模式的需求?三、为什么需要自定义密码模式?1. Spring Authorization Server 不再提供内置支持2. 现有项目的需求场景
需求的权衡:
四、自定义密码模式的实现步骤1.pom.xml
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-oauth2-authorization-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-oauth2-resource-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>com.mysqlgroupId>
<artifactId>mysql-connector-jartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
dependency>
dependencies>
由于我采用的是父子模块,这里先上一下我的版本号
2.先在AuthorizationServerConfig配置类中先添加我们要自定义的类
/**
* 该类是授权服务器的配置类
*/
@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
@Resource
private MyUserDetailServiceImpl myUserDetailServiceImpl; //该类是我自定义的UserDetail用于密码模式从数据库中查询校验用户信息
@Resource
private OAuth2AuthorizationService oAuth2AuthorizationService;
/**
* 配置密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
@Bean //自定义的身份验证
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(myUserDetailServiceImpl);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
/**
* 配置认证管理器(AuthenticationManager)
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
/*
配置注册客户端
*/
@Bean //这里会自动注入我自定义的配置
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
RegisteredClient build = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client") // 第三方客户端的名称
.clientSecret(passwordEncoder.encode("coin-secret"))// 客户端秘钥
.scope("all") //第三方客户端额度授权范围 指定客户端的权限范围,例如允许访问用户的只读数据或完全控制
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)//授权码模式
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) //刷新token模式
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) //客户端模式 不涉及用户的授权流程,仅基于客户端的 client_id 和 client_secret 验证。通常用于服务之间的授权。
.authorizationGrantType(AuthorizationGrantType.PASSWORD) //自定义的密码模式
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1))
.refreshTokenTimeToLive(Duration.ofDays(7)).build())
// .redirectUri("http://localhost:9999/login/oauth2/code/demo-client") ////认证回调地址,接收认证服务器回传的code,需要和客户端配置的一致
.redirectUri("https://www.baidu.com")
//客户端设置用户需要确认授权
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// .tokenSettings(TokenSettings.builder().build())
//客户端的权限范围
// .scope(OidcScopes.OPENID)
.build();
return new InMemoryRegisteredClientRepository(build);
}
/**
* Spring Authorization Server 相关配置
* 主要配置OAuth 2.1和OpenID Connect 1.0
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, AuthenticationManager authenticationManager, OAuth2TokenGenerator extends OAuth2Token> tokenGenerator,CustomAuthenticationEntryPoint customAuthenticationEntryPoint)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写) 就会返回一个ID token
.oidc(Customizer.withDefaults())
//以下是自定义端点
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverter(new OAuth2ResourceOwnerPasswordAuthenticationConverter())
.authenticationProvider(new OAuth2ResourceOwnerPasswordAuthenticationProvider(authenticationManager, oAuth2AuthorizationService, tokenGenerator)));
// .accessTokenResponseHandler(new MyAuthenticationSuccessHandler())
// .errorResponseHandler(new MyAuthenticationFailureHandler()));
http
//将需要认证的请求,通过json错误信息响应回去
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
customAuthenticationEntryPoint,
new MediaTypeRequestMatcher(MediaType.APPLICATION_JSON)
)
)
// 使用jwt处理接收到的access token
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
/**
* 该方法是spring security的配置
*
* @param http
* @return
* @throws Exception
*/
@Bean
@Order(2)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http.csrf(customize -> customize.disable())
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.formLogin(Customizer.withDefaults()) // 使用表单登录(适合手动测试)
.build();
}
@Bean //用于签署访问令牌。
public JWKSource jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); //公钥
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); //私钥
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
/*
深沉RSA密钥对,给上面jwkSource() 方法提供密钥对
*/
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
/*
配置JWT解析器
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置授权服务器请求地址
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
//什么都不配置,则使用默认地址
return AuthorizationServerSettings.builder().build();
}
@Bean
public OAuth2TokenGenerator extends OAuth2Token> tokenGenerator() {
NimbusJwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
jwtGenerator.setJwtCustomizer(tokenCustomizer());
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
@Bean
public OAuth2TokenCustomizer tokenCustomizer() {
return context -> {
Authentication principal = context.getPrincipal();
if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
Set authorities = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
context.getClaims().claim("authorities", authorities);
}
if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
Set authorities = principal.getAuthorities().stream().map(GrantedAuthority::getAuthority)
.collect(Collectors.toSet());
context.getClaims().claim("authorities", authorities);
}
};
}
大部分代码可以参考官网
其中比较重要的步骤是
/**
* Spring Authorization Server 相关配置
* 主要配置OAuth 2.1和OpenID Connect 1.0
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(
HttpSecurity http,
AuthenticationManager authenticationManager,
OAuth2TokenGenerator extends OAuth2Token> tokenGenerator,
CustomAuthenticationEntryPoint customAuthenticationEntryPoint)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
//开启OpenID Connect 1.0(其中oidc为OpenID Connect的缩写) 就会返回一个ID token
.oidc(Customizer.withDefaults())
//以下是自定义端点
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverter(new OAuth2ResourceOwnerPasswordAuthenticationConverter())
.authenticationProvider(new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager,
oAuth2AuthorizationService,
tokenGenerator)));
// .accessTokenResponseHandler(new MyAuthenticationSuccessHandler())
// .errorResponseHandler(new MyAuthenticationFailureHandler()));
http
//将需要认证的请求,通过json错误信息响应回去
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
customAuthenticationEntryPoint,
new MediaTypeRequestMatcher(MediaType.APPLICATION_JSON)
)
)
// 使用jwt处理接收到的access token
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
添加了自定义端点
.tokenEndpoint(tokenEndpoint -> tokenEndpoint
.accessTokenRequestConverter(new OAuth2ResourceOwnerPasswordAuthenticationConverter())
.authenticationProvider(new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager,
oAuth2AuthorizationService,
tokenGenerator)));
// .accessTokenResponseHandler(new MyAuthenticationSuccessHandler())
// .errorResponseHandler(new MyAuthenticationFailureHandler()));
这里面我还添加了自定义处理认证成功和失败返回的json,但是不知道因为某种原因添加之后只有自定义成功的handler会被处理,失败的都不会被执行,后来发现需要在http.exceptionHandling中处理失败的情况
http
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(customAuthenticationEntryPoint,
new MediaTypeRequestMatcher(MediaType.APPLICATION_JSON) ) )
以下是自定义成功失败返回结果的类的编写
CustomAuthenticationEntryPoint
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
// 设置响应类型为 JSON
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8"); // 设置编码为 UTF-8
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 状态码
// 自定义错误消息,可以根据异常类型或内容设置不同的错误消息
String errorMessage = "Authentication failed: " + authException.getMessage();
// 创建 JSON 格式的错误消息
String jsonResponse = String.format("{"error":"%s", "message":"%s"}", "Unauthorized", errorMessage);
// 写入响应内容
PrintWriter out = response.getWriter();
out.write(jsonResponse);
out.flush();
}
}
MyAuthenticationSuccessHandler
/**
* 认证成功处理器
*/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
/**
* MappingJackson2HttpMessageConverter 是 Spring 框架提供的一个 HTTP 消息转换器,用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换
*/
private final HttpMessageConverter
MyAuthenticationFailureHandler
该自定义失败处理类不起作用,要想实现自定义失败json返回必须实现AuthenticationEntryPoint重写commence方法,并且在http.exceptionHandling中进行配置,具体参考上面的配置类代码
/**
* 认证失败处理器
*/
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
/**
* MappingJackson2HttpMessageConverter 是 Spring 框架提供的一个 HTTP 消息转换器,用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换
*/
private final HttpMessageConverter
以下是自定义密码模式的关键类
public class OAuth2ResourceOwnerPasswordAuthenticationConverter 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;
}
MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request);
// 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 requestedScopes = null;
if (StringUtils.hasText(scope)) {
requestedScopes = new HashSet<>(
Arrays.asList(StringUtils.delimitedListToStringArray(scope, " ")));
}
// 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);
}
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
if (clientPrincipal == null) {
OAuth2EndpointUtils.throwError(
OAuth2ErrorCodes.INVALID_REQUEST,
OAuth2ErrorCodes.INVALID_CLIENT,
OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
}
MapObject> additionalParameters = parameters
.entrySet()
.stream()
.filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE) &&
!e.getKey().equals(OAuth2ParameterNames.SCOPE))
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));
return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal, requestedScopes, additionalParameters);
}
}
public class OAuth2ResourceOwnerPasswordAuthenticationProvider implements AuthenticationProvider {
private static final Logger LOGGER = LogManager.getLogger();
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN);
private final AuthenticationManager authenticationManager;
private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator extends OAuth2Token> tokenGenerator;
/**
* Constructs an {@code OAuth2ResourceOwnerPasswordAuthenticationProviderNew} using the provided parameters.
*
* @param authenticationManager the authentication manager
* @param authorizationService the authorization service
* @param tokenGenerator the token generator
* @since 0.2.3
*/
public OAuth2ResourceOwnerPasswordAuthenticationProvider(AuthenticationManager authenticationManager,
OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator extends OAuth2Token> tokenGenerator) {
Assert.notNull(authorizationService, "authorizationService cannot be null");
Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
this.authenticationManager = authenticationManager;
this.authorizationService = authorizationService;
this.tokenGenerator = tokenGenerator;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
OAuth2ResourceOwnerPasswordAuthenticationToken resouceOwnerPasswordAuthentication = (OAuth2ResourceOwnerPasswordAuthenticationToken) authentication;
OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(resouceOwnerPasswordAuthentication);
RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Retrieved registered client");
}
if (!registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.PASSWORD)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);
}
Authentication usernamePasswordAuthentication = getUsernamePasswordAuthentication(resouceOwnerPasswordAuthentication);
Set authorizedScopes = registeredClient.getScopes(); // Default to configured scopes
Set requestedScopes = resouceOwnerPasswordAuthentication.getScopes();
if (!CollectionUtils.isEmpty(requestedScopes)) {
Set unauthorizedScopes = requestedScopes.stream()
.filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope))
.collect(Collectors.toSet());
if (!CollectionUtils.isEmpty(unauthorizedScopes)) {
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE);
}
authorizedScopes = new LinkedHashSet<>(requestedScopes);
}
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Validated token request parameters");
}
// @formatter:off
DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
.registeredClient(registeredClient)
.principal(usernamePasswordAuthentication)
.authorizationServerContext(AuthorizationServerContextHolder.getContext())
.authorizedScopes(authorizedScopes)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizationGrant(resouceOwnerPasswordAuthentication);
// @formatter:on
// ----- 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 (LOGGER.isTraceEnabled()) {
LOGGER.trace("Generated access token");
}
OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
// @formatter:off
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.authorizedScopes(authorizedScopes)
.attribute(Principal.class.getName(), usernamePasswordAuthentication);
// @formatter:on
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 (LOGGER.isTraceEnabled()) {
LOGGER.trace("Generated refresh token");
}
refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
authorizationBuilder.refreshToken(refreshToken);
}
// ----- ID token -----
OidcIdToken idToken;
if (requestedScopes.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);
}
if (LOGGER.isTraceEnabled()) {
LOGGER.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();
this.authorizationService.save(authorization);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Saved authorization");
}
Map additionalParameters = Collections.emptyMap();
if (idToken != null) {
additionalParameters = new HashMap<>();
additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue());
}
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Authenticated token request");
}
return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
}
@Override
public boolean supports(Class> authentication) {
boolean supports = OAuth2ResourceOwnerPasswordAuthenticationToken.class.isAssignableFrom(authentication);
LOGGER.debug("supports authentication=" + authentication + " returning " + supports);
return supports;
}
private Authentication getUsernamePasswordAuthentication(OAuth2ResourceOwnerPasswordAuthenticationToken resouceOwnerPasswordAuthentication) {
Map additionalParameters = resouceOwnerPasswordAuthentication.getAdditionalParameters();
String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);
String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);
Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
return usernamePasswordAuthentication;
}
private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) {
OAuth2ClientAuthenticationToken clientPrincipal = null;
if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {
clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();
}
if (clientPrincipal != null && clientPrincipal.isAuthenticated()) {
return clientPrincipal;
}
throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
}
}
public class OAuth2ResourceOwnerPasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = -6067207202119450764L;
private final AuthorizationGrantType authorizationGrantType;
private final Authentication clientPrincipal;
private final Set<String> scopes;
private final Map<String, Object> additionalParameters;
/**
* Constructs an {@code OAuth2ClientCredentialsAuthenticationToken} using the provided parameters.
*
* @param clientPrincipal the authenticated client principal
*/
public OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType authorizationGrantType,
Authentication clientPrincipal, @Nullable Set<String> scopes, @Nullable Map<String, Object> additionalParameters) {
super(Collections.emptyList());
Assert.notNull(authorizationGrantType, "authorizationGrantType cannot be null");
Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");
this.authorizationGrantType = authorizationGrantType;
this.clientPrincipal = clientPrincipal;
this.scopes = Collections.unmodifiableSet(scopes != null ? new HashSet<>(scopes) : Collections.emptySet());
this.additionalParameters = Collections.unmodifiableMap(additionalParameters != null ? new HashMap<>(additionalParameters) : Collections.emptyMap());
}
/**
* Returns the authorization grant type.
*
* @return the authorization grant type
*/
public AuthorizationGrantType getGrantType() {
return this.authorizationGrantType;
}
@Override
public Object getPrincipal() {
return this.clientPrincipal;
}
@Override
public Object getCredentials() {
return "";
}
/**
* Returns the requested scope(s).
*
* @return the requested scope(s), or an empty {@code Set} if not available
*/
public Set<String> getScopes() {
return this.scopes;
}
/**
* Returns the additional parameters.
*
* @return the additional parameters
*/
public Map<String, Object> getAdditionalParameters() {
return this.additionalParameters;
}
}
以上代码都是参考的spring官方实现其他模式比如授权码模式的源码实现的,应该说大部分都是源码,源码中还用到了工具类,因为类权限问题无法直接调用,所以只能copy出来
public class OAuth2EndpointUtils {
static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";
private OAuth2EndpointUtils() {
}
public static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
for (String value : values) {
parameters.add(key, value);
}
}
});
return parameters;
}
public static boolean matchesPkceTokenRequest(HttpServletRequest request) {
return AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(
request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) &&
request.getParameter(OAuth2ParameterNames.CODE) != null &&
request.getParameter(PkceParameterNames.CODE_VERIFIER) != null;
}
public static void throwError(String errorCode, String parameterName, String errorUri) {
OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri);
throw new OAuth2AuthenticationException(error);
}
}
具体需要实现的代码就这么多
使用apifox工具进行接口测试
其中login_type可不写,这是我自定义的,在我的后续的代码中有用。
可以正常获取token
"access_token": "eyJraWQiOiIyYTQ3ODUwNC0wMjU3LTRmNzUtOTU2NC0yMGU5NjBiMGFmYjUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMDEwMTAxMDEwMTAxMDEwMTAxIiwiYXVkIjoiY2xpZW50IiwibmJmIjoxNzMzNTgzNDcwLCJzY29wZSI6WyJhbGwiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5OTk5IiwiZXhwIjoxNzMzNTg3MDcwLCJpYXQiOjE3MzM1ODM0NzAsImp0aSI6ImRjMDE4YjAyLTY0YTUtNDQyZS1iNTk4LTg2N2NiOTAwN2Q3YyIsImF1dGhvcml0aWVzIjpbInRyYWRlX2NvaW5fdXBkYXRlIiwiY2FzaF9yZWNoYXJnZV9hdWRpdF9xdWVyeSIsInVzZXJfdXBkYXRlIiwiY2FzaF93aXRoZHJhd19hdWRpdF9leHBvcnQiLCJjb2luX3dpdGhkcmF3X3N0YXRpc3RpY3NfcXVlcnkiLCJzeXNfcm9sZV9xdWVyeSIsIndlYl9jb25maWdfcXVlcnkiLCJsb2dpbl9zdGF0aXN0aWNzX3F1ZXJ5Iiwid29ya19pc3N1ZV9xdWVyeSIsInRyYWRlX2FyZWFfdXBkYXRlIiwidHJhZGVfbWFya2V0X2RlbGV0ZSIsInVzZXJfYXV0aF9hdWRpdCIsInN5c19wcml2aWxlZ2VfZGVsZXRlIiwiY2FzaF9yZWNoYXJnZV9zdGF0aXN0aWNzX3F1ZXJ5Iiwic3lzX3VzZXJfbG9nX3F1ZXJ5IiwidHJhZGVfYXJlYV9kZWxldGUiLCJ3ZWJfY29uZmlnX2RlbGV0ZSIsImFjY291bnRfZGV0YWlsX3F1ZXJ5IiwiYWRtaW5fYmFua19xdWVyeSIsInN5c19wcml2aWxlZ2VfdXBkYXRlIiwic3lzX3JvbGVfY3JlYXRlIiwidHJhZGVfY29pbl90eXBlX3F1ZXJ5IiwidXNlcl93YWxsZXRfcXVlcnkiLCJ3ZWJfY29uZmlnX3VwZGF0ZSIsInRyYWRlX3N0YXRpc3RpY3NfcXVlcnkiLCJjYXNoX3dpdGhkcmF3X2F1ZGl0X3F1ZXJ5IiwiY29uZmlnX2NyZWF0ZSIsInRyYWRlX21hcmtldF9jcmVhdGUiLCJ0cmFkZV9tYXJrZXRfcXVlcnkiLCJ0cmFkZV9kZWFsX29yZGVyX2V4cG9ydCIsImFjY291bnRfZXhwb3J0Iiwic3lzX3VzZXJfdXBkYXRlIiwidXNlcl9hdXRoX3F1ZXJ5IiwiYWRtaW5fYmFua19kZWxldGUiLCJ0cmFkZV9jb2luX3R5cGVfY3JlYXRlIiwiYWNjb3VudF9xdWVyeSIsImNvbmZpZ191cGRhdGUiLCJzeXNfdXNlcl9jcmVhdGUiLCJjb25maWdfcXVlcnkiLCJ0cmFkZV9jb2luX3F1ZXJ5IiwidHJhZGVfYXJlYV9xdWVyeSIsIndvcmtfaXNzdWVfdXBkYXRlIiwidHJhZGVfZW50cnVzdF9vcmRlcl9leHBvcnQiLCJub3RpY2VfdXBkYXRlIiwic3lzX3JvbGVfdXBkYXRlIiwidHJhZGVfY29pbl90eXBlX2RlbGV0ZSIsInRyYWRlX2NvaW5fdHlwZV91cGRhdGUiLCJyZWdpc3Rlcl9zdGF0aXN0aWNzX3F1ZXJ5IiwidHJhZGVfZW50cnVzdF9vcmRlcl9xdWVyeSIsImFjY291bnRfc3RhdHVzX3VwZGF0ZSIsImNhc2hfcmVjaGFyZ2VfYXVkaXRfZXhwb3J0IiwidHJhZGVfYXJlYV9jcmVhdGUiLCJhY2NvdW50X3JlY2hhcmdlX2NvaW5feG4iLCJ0cmFkZV9kZWFsX29yZGVyX3F1ZXJ5Iiwibm90aWNlX2RlbGV0ZSIsInVzZXJfZXhwb3J0IiwiY2FzaF93aXRoZHJhd19zdGF0aXN0aWNzX3F1ZXJ5Iiwic3lzX3ByaXZpbGVnZV9jcmVhdGUiLCJzeXNfcm9sZV9kZWxldGUiLCJ1c2VyX3dhbGxldF9hZGRyZXNzX3F1ZXJ5IiwiYWNjb3VudF9kZXRhaWxfZXhwb3J0IiwidHJhZGVfY29pbl9jcmVhdGUiLCJjb2luX3JlY2hhcmdlX2V4cG9ydCIsInN5c19wcml2aWxlZ2VfcXVlcnkiLCJjb2luX3JlY2hhcmdlX3F1ZXJ5IiwiY29pbl9yZWNoYXJnZV9zdGF0aXN0aWNzX3F1ZXJ5IiwidXNlcl9iYW5rX3F1ZXJ5IiwiYWRtaW5fYmFua19jcmVhdGUiLCJzeXNfdXNlcl9kZWxldGUiLCJ1c2VyX3F1ZXJ5Iiwid2ViX2NvbmZpZ19jcmVhdGUiLCJjYXNoX3JlY2hhcmdlX2F1ZGl0XzIiLCJ0cmFkZV9tYXJrZXRfdXBkYXRlIiwibm90aWNlX3F1ZXJ5IiwiY2FzaF9yZWNoYXJnZV9hdWRpdF8xIiwiY29pbl93aXRoZHJhd19leHBvcnQiLCJjb25maWdfZGVsZXRlIiwiY29pbl93aXRoZHJhd19xdWVyeSIsIm5vdGljZV9jcmVhdGUiLCJzbXNfcXVlcnkiLCJzeXNfdXNlcl9xdWVyeSIsImFkbWluX2JhbmtfdXBkYXRlIiwiY2FzaF93aXRoZHJhd19hdWRpdF8xIiwiY2FzaF93aXRoZHJhd19hdWRpdF8yIiwidHJhZGVfY29pbl9kZWxldGUiLCJjb2luX3dpdGhkcmF3X2F1ZGl0XzIiLCJjb2luX3dpdGhkcmF3X2F1ZGl0XzEiXX0.UEUkxDjAcyV0BaLRI8OOya8xA5hyj1SV8frt0g5f6XmM0fElTSGE4x8VpDsC0OhTwFCV4yzu4eM3KWzlr34RRB2W0MnXrOe1tv2tjUnqN3F8yMX_RdghZZZp5jKazMUmXR7QU_wHE2LPu7OuH5QfkKVY3saG4xaF0j8SRHYpVyoO1ttC1yZGekRr3bz9wDipQe-a4r_G_63JS2G7ZFLF0rnqjvndCJlGd1kt5DkGDWXyDR1E9Mf5KdA_BnrEwq16YMS_-EZujTdm98paJfFbvm85lJLQstLcXOvDDPoTlD7XmnrkeLLqgf2-Fck_tpWzsudGWxVAx61lmCX8UfEY4w",
"refresh_token": "oM3b-MepSxqqKsnaaC0783_-6Vn943J1FEVFnyN2KqEOdpxXrgk1UdHbCy6GjIq-sywjb3oKphalJtoqrKgrRAHp7tK-0gd_LfP2Sqw6FyPi9kaxF891pq2ZA6GqExHg",
"scope": "all",
"token_type": "Bearer",
"expires_in": 3599
}
失败返回的结果为
这里面的json信息都可自定义
以下是我自定义的UserDetailService,使用JdbcTemplate查询数据库(你可以自定义你自己的)
@Service
public class MyUserDetailServiceImpl implements UserDetailsService {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//这里面向数据库查询数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String loginType = requestAttributes.getRequest().getParameter("login_type");//在请求参数中获取该该参数来区分是否是管理员还是普通用户
if (StrUtil.isBlank(loginType)) {
//如果该参数是空的,则直接报异常
throw new AuthenticationServiceException("登录类型不能为空");
}
//使用分支进行转跳到对应的角色鉴权
UserDetails userDetails = null;
try {
switch (loginType) {
case LoginConstant.ADMIN_TYPE:
userDetails = loadSysUserByUsername(username);
break;
case LoginConstant.MEMBER_TYPE:
userDetails = loadMemberUserByUsername(username);
break;
default:
throw new AuthenticationServiceException("暂不支持这种方式:" + loginType);
}
} catch (IncorrectResultSizeDataAccessException e) { //用户不存在的情况
throw new UsernameNotFoundException("用户名" + username + "不存在");
}
return userDetails;
}
/**
* 后台管理人员登录
*
* @param username
* @return
*/
private UserDetails loadSysUserByUsername(String username) {
//1.使用用户名查询用户
return jdbcTemplate.queryForObject(LoginConstant.QUERY_ADMIN_SQL, new RowMapper() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
if (rs.wasNull()) {
throw new UsernameNotFoundException("用户名" + username + "不存在");
}
long id = rs.getLong("id");
String password = rs.getString("password");
int status = rs.getInt("status");
return new User( //3.封装成一个UserDetail对象返回
String.valueOf(id), //这里使用id代替了username ,方便了后续的查表
password,
status == 1,
true,
true,
true,
getSysUserPermissions(id) //2.查询这个用户对应的权限
);
}
}, username);
}
/**
* //2.查询这个用户对应的权限
* 通过用户id查询用户的权限数据
*
* @param id
* @return
*/
private Collection extends GrantedAuthority> getSysUserPermissions(long id) {
//查询用户权限分为两种情况
//1.当用户为超级管理员时,拥有所有的权限 查角色
String roleCode = jdbcTemplate.queryForObject(LoginConstant.QUERY_ROLE_CODE_SQL, String.class, id);
//查询所有的权限名称
List permissions = null;
if (roleCode != null && roleCode.equals(LoginConstant.ADMIN_ROLE_CODE)) { //超级管理员用户
permissions = jdbcTemplate.queryForList(LoginConstant.QUERY_ALL_PERMISSIONS, String.class);
} else {
//普通用户 //2.普通用户 需要通过角色查询他的权限数据
permissions = jdbcTemplate.queryForList(LoginConstant.QUERY_PERMISSION_SQL, String.class);
}
if (permissions.isEmpty()) {
//如果权限为空则,没有被查询到,意味着没有权限
return Collections.emptyList();
}
return permissions.stream().distinct() //进行一个去重
.map(perm -> new SimpleGrantedAuthority(perm))
.collect(Collectors.toSet());
}
/**
* 会员登录
*
* @param username
* @return
*/
private UserDetails loadMemberUserByUsername(String username) {
return null;
}
}
```
```