• 作者:老汪软件技巧
  • 发表时间: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>

由于我采用的是父子模块,这里先上一下我的版本号

image.png

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 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 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 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 accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
    private Converter> accessTokenResponseParametersConverter = new DefaultOAuth2AccessTokenResponseMapConverter();
    /**
     * 自定义认证成功响应数据结构
     *
     * @param request the request which caused the successful authentication
     * @param response the response
     * @param authentication the Authentication object which was created during
     * the authentication process.
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
                (OAuth2AccessTokenAuthenticationToken) authentication;
        OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
        OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
        Map additionalParameters = accessTokenAuthentication.getAdditionalParameters();
        OAuth2AccessTokenResponse.Builder builder =
                OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
                        .tokenType(accessToken.getTokenType());
        if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
            builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
        }
        if (refreshToken != null) {
            builder.refreshToken(refreshToken.getTokenValue());
        }
        if (!CollectionUtils.isEmpty(additionalParameters)) {
            builder.additionalParameters(additionalParameters);
        }
        OAuth2AccessTokenResponse accessTokenResponse = builder.build();
        Map tokenResponseParameters = this.accessTokenResponseParametersConverter
                .convert(accessTokenResponse);
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        this.accessTokenHttpResponseConverter.write(Result.success(tokenResponseParameters), null, httpResponse);
    }
}

MyAuthenticationFailureHandler

该自定义失败处理类不起作用,要想实现自定义失败json返回必须实现AuthenticationEntryPoint重写commence方法,并且在http.exceptionHandling中进行配置,具体参考上面的配置类代码

/**
 * 认证失败处理器
 */
@Slf4j
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    /**
     * MappingJackson2HttpMessageConverter 是 Spring 框架提供的一个 HTTP 消息转换器,用于将 HTTP 请求和响应的 JSON 数据与 Java 对象之间进行转换
     */
    private final HttpMessageConverter accessTokenHttpResponseConverter = new MappingJackson2HttpMessageConverter();
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        Result result = Result.failure(error.getErrorCode());
        accessTokenHttpResponseConverter.write(result, null, httpResponse);
    }
}

以下是自定义密码模式的关键类

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 OAuth2TokenGeneratorextends 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 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);
    }
    
}

具体需要实现的代码就这么多

image.png

使用apifox工具进行接口测试

image.png

其中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
}

失败返回的结果为

image.png

这里面的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 Collectionextends 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;
    }
}
```
```


上一条查看详情 +Zustand:全局状态管理的利器
下一条 查看详情 +没有了