SpringBoot3集成Oauth2.1——8自定义认证模式(密码模式)
1.为什么要自定义展密码模式?
由于早些年,我们使用的oauth版本是:spring-cloud-starter-oauth2,针对该版本的Oauth2升级,
从SpringBoot2.1-》2.2-》2.3-》2.4-》2.5-》2.6-》2.7一路是荆棘之路。
<!-- Oauth2 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-oauth2</artifactId><version>2.2.5.RELEASE</version></dependency><!-- Spring security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
以下版本为:SpringBoot2.7.11的时候部分代码,基本上来说,几乎百分之95以上代码都过时了。
但是最为恐怖的存在是:我在里面扩展了很多的认证方式,比如手机验证、指纹验证、用户证书验证等等等。而这些扩展的认证方式的代码,都是参照密码模式来实现的。当然如果从头看我博客的人应该知道,oauth2.0和oauth2.1虽然都是Spring security,但是完全是两个不同的框架了。而在oauth2.1中,更是删除了密码模式。
这也是Spring security的诟病,每次升级都会带来代码上的改动。导致基本一个版本一个样的代码,在新版的oauth2.1中,则已经没有办法在去做升级了,对于原来我扩展的认证方式,其实现方式参照密码模式,因此要重构,先把密码模式实现才有可能迁移。
代码目录结构如下,主要本章节主要包含password,userDetail,以及SecurityConfig
2.password
2.1.定义参数转换器
import java.util.HashMap;
import java.util.Map;import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;import jakarta.servlet.http.HttpServletRequest;/** @Description: 自定义密码模式的请求参数转换器* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月21日 下午1:43:38*/
public class PasswordAuthenticationConverter implements AuthenticationConverter {@Nullable@Overridepublic Authentication convert(HttpServletRequest request) {// 校验授权类型是否为自定义的密码模式String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);if (!OAuth2ParameterNames.PASSWORD.equals(grantType)) {return null;}// 获取客户端认证信息Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();// 提取请求参数MultiValueMap<String, String> parameters = getParameters(request);// 校验用户名(必填)String username = parameters.getFirst(OAuth2ParameterNames.USERNAME);if (!StringUtils.hasText(username) || parameters.get(OAuth2ParameterNames.USERNAME).size() != 1) {throw new OAuth2AuthenticationException("无效请求,用户名不能为空!");}// 校验密码(必填)String password = parameters.getFirst(OAuth2ParameterNames.PASSWORD);if (!StringUtils.hasText(password) || parameters.get(OAuth2ParameterNames.PASSWORD).size() != 1) {throw new OAuth2AuthenticationException("无效请求,密码不能为空!");}// 收集额外参数(排除grant_type、client_id等)Map<String, Object> additionalParameters = new HashMap<>();parameters.forEach((key, value) -> {if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) &&!key.equals(OAuth2ParameterNames.CLIENT_ID) &&!key.equals(OAuth2ParameterNames.CODE)) {additionalParameters.put(key, value.get(0));}});// 返回自定义认证令牌return new PasswordAuthenticationToken(clientPrincipal, additionalParameters);}// 从请求中提取参数到MultiValueMapprivate 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;}
}
2.2.定义认证处理器
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;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.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
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.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.ObjectUtils;import jakarta.annotation.Resource;/** @Description: 自定义密码模式的认证处理器* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月21日 下午1:43:54*/
public class PasswordAuthenticationProvider implements AuthenticationProvider {private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2";@Resourceprivate UserDetailsService userDetailsService;@Resourceprivate PasswordEncoder passwordEncoder;private final OAuth2AuthorizationService authorizationService;private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;public PasswordAuthenticationProvider(OAuth2AuthorizationService authorizationService,OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator) {Assert.notNull(authorizationService, "authorizationService cannot be null");Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");this.authorizationService = authorizationService;this.tokenGenerator = tokenGenerator;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {// 转换为自定义认证令牌PasswordAuthenticationToken authenticationToken = (PasswordAuthenticationToken) authentication;Map<String, Object> additionalParameters = authenticationToken.getAdditionalParameters();// 提取参数AuthorizationGrantType grantType = authenticationToken.getGrantType();String username = (String) additionalParameters.get(OAuth2ParameterNames.USERNAME);String password = (String) additionalParameters.get(OAuth2ParameterNames.PASSWORD);String scopeStr = (String) additionalParameters.get(OAuth2ParameterNames.SCOPE);Set<String> requestedScopes = ObjectUtils.isEmpty(scopeStr) ? Set.of() : Stream.of(scopeStr.split(" ")).collect(Collectors.toSet());// 校验客户端认证OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClient(authenticationToken);RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();// 校验客户端是否支持当前授权类型if (!registeredClient.getAuthorizationGrantTypes().contains(grantType)) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.UNAUTHORIZED_CLIENT);}// 校验用户名密码UserDetails userDetails = userDetailsService.loadUserByUsername(username);if (!passwordEncoder.matches(password, userDetails.getPassword())) {throw new OAuth2AuthenticationException("密码不正确!");}// 构建用户认证令牌UsernamePasswordAuthenticationToken userAuthentication = UsernamePasswordAuthenticationToken.authenticated(userDetails, clientPrincipal, userDetails.getAuthorities());// 构建token上下文DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient).principal(userAuthentication).authorizationServerContext(AuthorizationServerContextHolder.getContext()).authorizationGrantType(grantType).authorizedScopes(requestedScopes).authorizationGrant(authenticationToken);// 构建授权信息OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient).principalName(clientPrincipal.getName()).authorizedScopes(requestedScopes).attribute(Authentication.class.getName(), userAuthentication).authorizationGrantType(grantType);// 生成访问令牌OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();OAuth2Token generatedAccessToken = tokenGenerator.generate(tokenContext);if (generatedAccessToken == null) {throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "生成访问令牌失败", ERROR_URI));}OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,generatedAccessToken.getTokenValue(),generatedAccessToken.getIssuedAt(),generatedAccessToken.getExpiresAt(),tokenContext.getAuthorizedScopes());authorizationBuilder.accessToken(accessToken);// 生成刷新令牌(如果客户端支持)OAuth2RefreshToken refreshToken = null;if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {OAuth2TokenContext refreshTokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();OAuth2Token generatedRefreshToken = tokenGenerator.generate(refreshTokenContext);if (generatedRefreshToken instanceof OAuth2RefreshToken) {refreshToken = (OAuth2RefreshToken) generatedRefreshToken;authorizationBuilder.refreshToken(refreshToken);}}// 保存授权信息OAuth2Authorization authorization = authorizationBuilder.build();authorizationService.save(authorization);// 返回认证结果return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);}@Overridepublic boolean supports(Class<?> authentication) {return PasswordAuthenticationToken.class.isAssignableFrom(authentication);}// 提取并校验客户端认证信息private OAuth2ClientAuthenticationToken getAuthenticatedClient(Authentication authentication) {OAuth2ClientAuthenticationToken clientPrincipal = null;if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) {clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal();}if (clientPrincipal == null || !clientPrincipal.isAuthenticated()) {throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);}return clientPrincipal;}
}
2.3.定义认证标识
import java.util.Map;import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken;
import org.springframework.util.Assert;/** @Description: 自定义密码模式的认证令牌* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月21日 下午1:44:06*/
public class PasswordAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {private static final long serialVersionUID = 1L;public PasswordAuthenticationToken(Authentication clientPrincipal, @Nullable Map<String, Object> additionalParameters) {super(new AuthorizationGrantType(OAuth2ParameterNames.PASSWORD), clientPrincipal, additionalParameters);Assert.notNull(clientPrincipal, "clientPrincipal cannot be null");}
}
3.userDetail
备注,CustomUserDetail和CustomUserDetailsService为非必须的,但是如果我们要想扩展自定义的一些认证方法,则CustomUserDetailsService是非常有必要的。
简单说,org.springframework.security.core.userdetails.UserDetails是用来设置用户信息的,如果想在里面加属性,则需要扩展UserDetails实体类,而org.springframework.security.core.userdetails.UserDetailsService则是我们用来定义查询用户的操作,例如这里默认的密码模式,则是根据用户名查询用户账号和密码。如果你自定义查询,你可以根据证书查询用户,根据手机号查询用户,根据生物特征查询用户等等。
3.1.定义用户实体类
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.pcgy.gis.user.module.auth.entity.UserInfo;import lombok.Data;/** @Description: 定制:UserDetails* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月26日 下午1:56:21*/
@Data
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
@JsonIgnoreProperties(ignoreUnknown = true)
public class CustomUserDetail implements UserDetails {private static final long serialVersionUID = 1L;private String username;private String password;private boolean enabled;//这是我们想要附加的自定义的字段,或者对象private UserInfo userInfo;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return password;}@Overridepublic String getUsername() {return username;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return enabled;}
}
3.2.定义用户查询服务类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;import com.pcgy.gis.user.module.auth.entity.UserInfo;
import com.pcgy.gis.user.module.user.entity.SysUser;
import com.pcgy.gis.user.module.user.service.ISysUserService;@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate ISysUserService sysUserService;/** @Description: 根据用户查询参数加载用户* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年8月21日 下午2:00:48*/@Overridepublic CustomUserDetail loadUserByUsername(String userName) {//这里你可以使用任何的查询方式、查询逻辑来查询你想要的用户SysUser userPO = sysUserService.getById(userName);if(userPO == null) {throw new UsernameNotFoundException("用户不存在或用户密码错误:"+userName);}CustomUserDetail user = new CustomUserDetail();user.setUsername(userPO.getUserName());user.setPassword(userPO.getPassWord());user.setEnabled(true);//这里你可以使用任何方式来获取userInfo,然后放到CustomUserDetail中UserInfo userInfo = new UserInfo();user.setUserInfo(userInfo);return user;}
}
4.SecurityConfig
登录认证的核心代码如下。
@Configuration
@EnableWebSecurity
@Log4j2
public class SecurityConfig {/** @Description: 配置授权服务器(用于登录操作)* @author: 胡涛* @mail: hutao_2017@aliyun.com* @date: 2025年5月23日 下午3:15:35*/@Bean@Order(1)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, OAuth2AuthorizationService authorizationService, RegisteredClientRepository registeredClientRepository, OAuth2TokenGenerator<?> tokenGenerator,PasswordEncoder passwordEncoder) throws Exception {OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =OAuth2AuthorizationServerConfigurer.authorizationServer();http.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()).with(authorizationServerConfigurer, (authorizationServer) ->authorizationServer.oidc(Customizer.withDefaults())).authorizeHttpRequests((authorize) ->authorize.anyRequest().authenticated()).exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(new LoginUrlAuthenticationEntryPoint("/login"),new MediaTypeRequestMatcher(MediaType.TEXT_HTML)));// 设置自定义 UserDetailsServicehttp.userDetailsService(customUserDetailsService);http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).tokenEndpoint(tokenEndpoint ->tokenEndpoint.accessTokenRequestConverter(new PasswordAuthenticationConverter()).authenticationProvider(new PasswordAuthenticationProvider(authorizationService, tokenGenerator)));return http.build();}
5.验证
准备参数
client-id: pcgy-gis-user
client-secret: 3f8d6e9c-7a2b-41c5-9d1e-8f3a4b5c6d7einsert into `oauth2_registered_client`(`id`,`client_id`,`client_id_issued_at`,`client_secret`,`client_secret_expires_at`,`client_name`,`client_authentication_methods`,`authorization_grant_types`,`redirect_uris`,`post_logout_redirect_uris`,`scopes`,`client_settings`,`token_settings`) values
('1','pcgy-gis-user','2025-08-21 02:22:30','$2a$10$uTeUCAANtYXb5yCwJPAj7.1HF7iUsU8ierEGJTRqxgKe41xlheQhC',NULL,'客户端测试','client_secret_basic','authorization_code,refresh_token,client_credentials,password','http://www.baidu.com','https://www.doubao.com/chat/','openid,profile','{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.client.require-proof-key\":false,\"settings.client.require-authorization-consent\":true}','{\"@class\":\"java.util.Collections$UnmodifiableMap\",\"settings.token.reuse-refresh-tokens\":true,\"settings.token.x509-certificate-bound-access-tokens\":false,\"settings.token.id-token-signature-algorithm\":[\"org.springframework.security.oauth2.jose.jws.SignatureAlgorithm\",\"RS256\"],\"settings.token.access-token-time-to-live\":[\"java.time.Duration\",1800.000000000],\"settings.token.access-token-format\":{\"@class\":\"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat\",\"value\":\"self-contained\"},\"settings.token.refresh-token-time-to-live\":[\"java.time.Duration\",3000.000000000],\"settings.token.authorization-code-time-to-live\":[\"java.time.Duration\",300.000000000],\"settings.token.device-code-time-to-live\":[\"java.time.Duration\",300.000000000]}');
5.1.请求头
5.1.请求参数
其他认证方式验证
例如简化模式