程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

基于Spring Security OAuth2认证中心授权模式扩展

balukai 2024-12-30 01:58:31 文章精选 15 ℃

介绍

Spring Security OAuth2 默认实现的四种授权模式在实际的应用场景中往往满足不了预期。 需要扩展如下需求:

  • 手机号+短信验证码登陆
  • 微信授权登录

本次主要通过继承Spring Security OAuth2 抽象类和接口,来实现对oauth2/token接口的手机号+短信的认证授权。


代码

https://gitee.com/LearningTech/springoatuh2

开发环境

  • JDK 17
  • Spring Boot 3

核心概念和流程

  • SecurityFilterChain: 表示Spring Security的过滤器链。实现安全配置和认证扩展配置
  • RegisteredClientRepository: 表示自定义的授权客户端信息,需要进行配置。这个客户端信息是oauth2/token中需要进行认证的信息。
  • AbstractAuthenticationToken: 表示用户认证信息。 需要对其进行扩展
  • AuthenticationProvider: 验证登录信息,实现token的生成。需要对其进行扩展
  • AuthenticationConverter: 实现对AbstractAuthenticationToken自定义扩展类的转换。

主要流程就是,实现上述AbstractAuthenticationToken、AuthenticationProvider、AuthenticationConverter三个抽象类和接口的扩展。并通过实现AuthenticationSuccessHandler扩展类,用来返回token给http response中。


AuthorizationServerConfig.java

Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
            .tokenEndpoint(tokenEndpoint -> tokenEndpoint
                    // 自定义授权模式转换器
                    .accessTokenRequestConverter(new MobilePhoneAuthenticationConverter())
                    .accessTokenRequestConverter(new UsernamePasswordGrantAuthenticationConverter())
                    // 自定义授权响应
                    .accessTokenResponseHandler(new CustomizerAuthenticationSuccessHandler())
                    .errorResponseHandler(new CustomizerAuthenticationFailureHandler())
            )
            .oidc(Customizer.withDefaults());

        http.exceptionHandling((exceptions) -> exceptions
            .authenticationEntryPoint((request, response, authException) -> {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType(MediaType.APPLICATION_JSON_VALUE);

                OAuth2Error error = new OAuth2Error(
                    "unauthorized",
                    authException.getMessage(),
                    "https://tools.ietf.org/html/rfc6750#section-3.1"
                );

                new ObjectMapper().writeValue(response.getOutputStream(), error);
            })
        );

         // 添加自定义的认证提供者
        http.authenticationProvider(mobilePhoneAuthenticationProvider);

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("mobile-client")
            .clientSecret(passwordEncoder.encode("secret"))
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(new AuthorizationGrantType("mobile_phone")) // 自定义授权类型
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/mobile-client")
            .scope("message.read")
            .scope("message.write")
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.REFERENCE) // 设置访问令牌格式为 REFERENCE
                        .build())
            .build();

        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder().build();
    }


SecurityConfig.java


 @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests((authorize) -> authorize
                .requestMatchers("/send-sms", "/oauth2/token").permitAll()
                .anyRequest().authenticated()
            )
            .csrf((csrf) -> csrf.ignoringRequestMatchers("/send-sms", "/oauth2/token"));

        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


MobilePhoneAuthenticationConverter.java

public class MobilePhoneAuthenticationConverter implements AuthenticationConverter {

    @Override
    public Authentication convert(HttpServletRequest request) {
        String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
        if (!"mobile_phone".equals(grantType)) {
            return null;
        }

        String phoneNumber = request.getParameter("phone_number");
        String smsCode = request.getParameter("sms_code");
        String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID);

        if (phoneNumber == null || smsCode == null || clientId == null) {
            throw new OAuth2AuthenticationException(new OAuth2Error("invalid_request"));
        }

        return new MobilePhoneAuthenticationToken(phoneNumber, smsCode, clientId);
    }
}

MobilePhoneAuthenticationProvider.java

@Component
public class MobilePhoneAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private OAuth2AuthorizationService authorizationService;

    @Autowired
    private OAuth2TokenGenerator<OAuth2Token> tokenGenerator;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        MobilePhoneAuthenticationToken mobilePhoneAuthentication = (MobilePhoneAuthenticationToken) authentication;

        // 验证手机号和验证码的逻辑...
        String phoneNumber = (String) mobilePhoneAuthentication.getPrincipal();
        String smsCode = (String) mobilePhoneAuthentication.getCredentials();

        // 这里应该添加实际的验证逻辑
        if (!"123456".equals(smsCode)) {  // 示例验证,实际应该查询数据库或缓存
            throw new BadCredentialsException("Invalid SMS code");
        }

        OAuth2ClientAuthenticationToken clientPrincipal =
                getAuthenticatedClientElseThrowInvalidClient();
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();

        OAuth2TokenContext tokenContext = DefaultOAuth2TokenContext.builder()
                .registeredClient(registeredClient)
                .principal(mobilePhoneAuthentication)
                .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                .authorizedScopes(registeredClient.getScopes())
                .tokenType(OAuth2TokenType.ACCESS_TOKEN)
                .authorizationGrantType(new AuthorizationGrantType("mobile_phone"))
                .build();


        OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
        if (!(generatedAccessToken instanceof OAuth2AccessToken)) {
            throw new OAuth2AuthenticationException(new OAuth2Error("server_error", "The token generator failed to generate the access token.", null));
        }

        OAuth2AccessToken accessToken = (OAuth2AccessToken) generatedAccessToken;

        OAuth2RefreshToken refreshToken = null;
        if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN)) {
            tokenContext = DefaultOAuth2TokenContext.builder()
                    .registeredClient(registeredClient)
                    .principal(mobilePhoneAuthentication)
                    .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                    .authorizedScopes(registeredClient.getScopes())
                    .tokenType(OAuth2TokenType.REFRESH_TOKEN)
                    .authorizationGrantType(new AuthorizationGrantType("mobile_phone"))
                    .build();

            OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext);
            if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) {
                throw new OAuth2AuthenticationException(new OAuth2Error("server_error", "The token generator failed to generate the refresh token.", null));
            }
            refreshToken = (OAuth2RefreshToken) generatedRefreshToken;
        }

        OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(registeredClient)
                .principalName(phoneNumber)
                .authorizationGrantType(new AuthorizationGrantType("mobile_phone"))
                .token(accessToken)
                .refreshToken(refreshToken)
                .build();

        this.authorizationService.save(authorization);

        return new OAuth2AccessTokenAuthenticationToken(
                registeredClient, clientPrincipal, accessToken, refreshToken,  Collections.emptyMap());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return MobilePhoneAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient() {
        // 这里需要实现获取当前认证的客户端逻辑
        // 例如,从 SecurityContextHolder 中获取
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication instanceof OAuth2ClientAuthenticationToken) {
            return (OAuth2ClientAuthenticationToken) authentication;
        }
        throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
    }

MobilePhoneAuthenticationToken.java

public class MobilePhoneAuthenticationToken extends AbstractAuthenticationToken {
    private final String phoneNumber;
    private final String smsCode;
    private final String clientId;

    public MobilePhoneAuthenticationToken(String phoneNumber, String smsCode, String clientId) {
        super(null);
        this.phoneNumber = phoneNumber;
        this.smsCode = smsCode;
        this.clientId = clientId;
        setAuthenticated(false);
    }

    public MobilePhoneAuthenticationToken(String phoneNumber, String smsCode, String clientId, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.phoneNumber = phoneNumber;
        this.smsCode = smsCode;
        this.clientId = clientId;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.smsCode;
    }

    @Override
    public Object getPrincipal() {
        return this.phoneNumber;
    }

    public String getClientId() {
        return this.clientId;
    }
}

测试验证

Tags:

最近发表
标签列表