安全配置中的会话主体是否可以在Spring Security服务器中重复使用?

huangapple go评论54阅读模式
英文:

Can the session principal in security config be reused in spring security server?

问题

我有一个实现了UserDetails接口的LoginUser类。

我还有一个扩展了DaoAuthenticationProvider的CustomDaoAuthenticationProvider类。

我正在使用Spring Security 5和Spring Authorization Server版本0.3.1。

在OAuth2流程中,授权码授予类型,用户需要登录以允许客户端程序获取授权码和令牌。

在登录时,用户名和密码被设置在LoginUser类中,这是在CustomDaoAuthenticationProvider类内部完成的。

期望的结果是LoginUser将成为认证的会话主体。

在请求端点时,端点方法中带有@AuthenticationPrincipal注解的参数应该能够获取LoginUser对象...但是我得到的是null,导致了NPE(空指针异常),当我假设LoginUser是值时。

我应该如何在Spring授权服务器配置类中进行配置?

还是应该只在安全配置类中进行配置?如何配置?

英文:

I have a class LoginUser that implements UserDetails.

I also have a class CustomDaoAuthenticationProvider that extends DaoAuthenticationProvider.

I am using Spring Security 5 and Spring Authoration Server version 0.3.1.

In OAuth2 process, authorization code grant type, the user needs to login to allow the client program to get the authorization code and token.

When logging in,the username and password is set in the LoginUser w/c is done inside CustomDaoAuthenticationProvider.

The expected result is that LoginUser will become a session Principal of the authentication.

When requesting in an endpoint, the @AuthenticationPrincipal annotated parameter in the endpoint method will be able to get the LoginUser....but what I get is null causing an NPE when I assume a LoginUser was the value.

How will I configure it in the spring authorization server config class?

Or should I configure it only in security config class? how?

Below is my SecurityConfig class:

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class SessionAuthSecurityConfig {
  public static final String LOGOUT_URL = "/logout";
  public static final String LOGIN_URL = "/login";
  private static final String Custom_SESSION = "Custom_SESSION";

  private final AuthenticationEventPublisher authenticationEventPublisher;
  private final UserDetailsService CustomUserDetailsService;
  private final CustomDaoAuthenticationProvider CustomDaoAuthenticationProvider;
  private final ApplicationEventPublisher eventPublisher;
  private final UserService userService;
  private final AuthenticationConfiguration configuration;
  private final CustomWebAuthenticationDetailsSource customWebAuthenticationDetailsSource;

  @Value("${security.enable-csrf:true}")
  public boolean csrfEnabled;

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.sessionManagement(
            c ->
                c.invalidSessionStrategy(this.invalidSessionStrategy())
                    .maximumSessions(CustomFeatureService.getMaxSession())
                    .expiredSessionStrategy(this.expiredSessionStrategy()))
        .cors()
        .and()
        .exceptionHandling(
            c ->
                c.accessDeniedHandler(accessDeniedHandler())
                    .authenticationEntryPoint(authenticationEntryPoint()))
        .csrf(
            c -> {
              log.debug("csrfEnabled = {}", csrfEnabled);
              if (csrfEnabled) {
                c.requireCsrfProtectionMatcher(new CsrfRequestMatcher())
                    .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
              } else {
                c.disable();
              }
            })
        .authorizeRequests(
            c ->
                c.antMatchers(
                        "/", "/api/**", "/user/**", "/oauth2/authorize")
                    .hasAuthority("ROLE_USER")
                    .antMatchers(
                        "/rest-api/admin/**")
                    .hasAuthority("ROLE_ADMIN")
                    .antMatchers("/link/**", "/login-info", "/reissue-password/**")
                    .permitAll())
        .authorizeHttpRequests(c -> c.mvcMatchers("/Custom-lib/**", "/webjars/**").permitAll())
        .formLogin(
            c ->
                c.authenticationDetailsSource(CustomWebAuthenticationDetailsSource)
                    .loginPage(LOGIN_URL)
                    .loginProcessingUrl("/perform_login")
                    .successHandler(successHandler())
                    .failureHandler(failureHandler())
                    .permitAll())
        .logout(
            c ->
                c.permitAll()
                    .logoutUrl(LOGOUT_URL)
                    .addLogoutHandler(eventSaveLogoutHandler()) // Save Logout Event
                    .invalidateHttpSession(true)
                    .deleteCookies(Custom_SESSION)
                    .logoutSuccessHandler(logoutSuccessHandler()) // Redirect
            )
        .addFilterBefore(new UserIpBlockFilter(), UsernamePasswordAuthenticationFilter.class);

    return http.build();
  }

  @Autowired
  public void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(CustomDaoAuthenticationProvider);
  }

  @Bean
  public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
  }

  private AuthenticationFailureHandler failureHandler() {
    return new CustomAuthFailureHandler(CustomSystemProperties, httpMessageConverter);
  }

  private AuthenticationSuccessHandler successHandler() {
    var handler = new SavedRequestAwareAuthenticationSuccessHandler();
    return handler;
  }
  

  private AuthenticationEntryPoint authenticationEntryPoint() {
    var httpStatusEntryPoint = new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);
    var entryPoints = new LinkedHashMap<RequestMatcher, AuthenticationEntryPoint>();
    entryPoints.put(new AntPathRequestMatcher("/api/**"), httpStatusEntryPoint);
    entryPoints.put(new AntPathRequestMatcher("/system-api/**"), httpStatusEntryPoint);
    var delegate = new DelegatingAuthenticationEntryPoint(entryPoints);
    delegate.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint(LOGIN_URL));
    return delegate;
  }

  private AccessDeniedHandler accessDeniedHandler() {
    AccessDeniedHandlerImpl defaultHandler = new AccessDeniedHandlerImpl();
    defaultHandler.setErrorPage("/access-denied");
    final RequestMatcher apiMatcher = this.apiMatcher();
    return (request, response, accessDeniedException) -> {
      boolean matched = apiMatcher.matches(request);
      if (matched) {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
      } else {
        defaultHandler.handle(request, response, accessDeniedException);
      }
    };
  }

  LogoutSuccessHandler logoutSuccessHandler() {
    SimpleUrlLogoutSuccessHandler handler = new SimpleUrlLogoutSuccessHandler();
    handler.setDefaultTargetUrl(LOGIN_URL);
    return handler;
  }

  LogoutHandler eventSaveLogoutHandler() {
    return (request, response, authentication) -> {
      if (authentication == null || authentication.getPrincipal() == null) {
        log.debug("Not authenticated.");
        return;
      }

      LoginUser loginUser = (LoginUser) authentication.getPrincipal();
      String hostName = HttpRequestHelper.getHostName(request);
      String ipAddress = HttpRequestHelper.getIpAddress(request);
      User user = userService.getUser(loginUser.getId()).orElseThrow();
      eventQueueSendingService.sendLogoutEvent(user, ipAddress, hostName);
    };
  }
}

This is the auth server config class:

@Slf4j
@Configuration
public class AuthorizationServerConfig {

  @Autowired private PasswordEncoder passwordEncoder;
  @Autowired private ApiAccessLogService apiAccessLogService;
  @Autowired private HttpServletRequest request;
  @Autowired private UserDetailsService customUserDetailsService;
  @Autowired private CustomDaoAuthenticationProvider customDaoAuthenticationProvider;
  @Autowired private CustomWebAuthenticationDetailsSource customWebAuthenticationDetailsSource;

  @Bean
  public RegisteredClientRepository registeredClientRepository() {
    RegisteredClient registeredClient =
        RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("ssss")
            .clientSecret(passwordEncoder.encode("123"))
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .redirectUri("http://127.0.0.1:19940/authorized")
            .tokenSettings(tokenSettings())
            .scope("full")
            .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
            .build();

    return new InMemoryRegisteredClientRepository(registeredClient);
  }

  private AuthenticationConverter customAuthorizationRequestConverter() {
    final OAuth2AuthorizationCodeRequestAuthenticationConverter delegate = new OAuth2AuthorizationCodeRequestAuthenticationConverter();

    return (request) -> {
      OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
        (OAuth2AuthorizationCodeRequestAuthenticationToken) delegate.convert(request);
      return authorizationCodeRequestAuthentication;
    };
  }

  @Bean
  @Order(Ordered.HIGHEST_PRECEDENCE)
  public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception {

    OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
      new OAuth2AuthorizationServerConfigurer<>();
    RequestMatcher endpointsMatcher = authorizationServerConfigurer
      .getEndpointsMatcher();

    http
      .requestMatcher(endpointsMatcher)
      .authorizeRequests(authorizeRequests ->
        authorizeRequests.anyRequest().authenticated()
      )
      .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
      .apply(authorizationServerConfigurer);
//

    authorizationServerConfigurer
      .authorizationEndpoint(authorizationEndpoint ->
        authorizationEndpoint
          .authorizationRequestConverter(customAuthorizationRequestConverter())
          .authenticationProvider(CustomDaoAuthenticationProvider)
      );

    http
        // Redirect to the login page when not authenticated from the
        // authorization endpoint
        .exceptionHandling(
        (exceptions) ->
            exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));

    return http.build();
    
  }

  @Bean
  public AuthenticationEventPublisher authenticationEventPublisher(
      ApplicationEventPublisher applicationEventPublisher) {
    return new AuthenticationEventPublisher() {
      @Override
      public void publishAuthenticationSuccess(Authentication authentication) {
        applicationEventPublisher.publishEvent(new AuthenticationSuccessEvent(authentication));
        final Object principal = authentication.getPrincipal();
        String clientId = null;
        Integer userId = null;

        if (principal instanceof LoginUser loginUser) {
          userId = loginUser.getId();
        }

        if (authentication instanceof OAuth2Authorization) {
          final OAuth2Authorization oAuth2Authorization = (OAuth2Authorization) authentication;
          clientId = oAuth2Authorization.getRegisteredClientId();
        }
        apiAccessLogService.saveApiAccessLog(
            userId, request, ApiAccessLog.AccessType.OAUTH2, clientId);
      }

      @Override
      public void publishAuthenticationFailure(
          AuthenticationException exception, Authentication authentication) {
        log.debug("OAuth2 authentication failed");
      }
    };
  }

  @Bean
  public ProviderSettings providerSettings() {
    return ProviderSettings.builder().issuer("http://localhost:19940").build();
  }

  @Bean
  public TokenSettings tokenSettings() {
    return TokenSettings.builder()
        .accessTokenFormat(OAuth2TokenFormat.REFERENCE)
        .accessTokenTimeToLive(Duration.ofSeconds(6000L))
        .build();
  }

  @Bean
  public OAuth2AuthorizationService authorizationService() {
    return new InMemoryOAuth2AuthorizationService();
  }
}

CustomDaoAuthenticationProvider

@RequiredArgsConstructor
@Slf4j
public class CustomDaoAuthenticationProvider extends DaoAuthenticationProvider implements Ordered {

  private final AccountScratchCodeService accountScratchCodeService;
  private final AuthenticationEmailService authenticationEmailService;
  private final Mail2faTokenService mail2faTokenService;
  private final UserService userService;

  private int order = -1;

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // check if the input authentication is for normal authentication
    if (StringUtils.hasText((String) authentication.getPrincipal())
        && StringUtils.hasText((String) authentication.getCredentials())) {
      SecurityContextHolder.clearContext();
      return super.authenticate(authentication);
    }

    // fallback to default authentication check
    return super.authenticate(authentication);
  }

  @Override
  public void additionalAuthenticationChecks(
      UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    LoginUser loginUser = (LoginUser) userDetails;
    Object detailsObj = authentication.getDetails();

    if (detailsObj instanceof CustomWebAuthenticationDetails CustomWebAuthenticationDetails) {

      if (CustomWebAuthenticationDetails.isBasicAuth()) {
        if (!loginUser.isBasicAuthAllowed()) {
          log.debug("Login Failure (User has no permission.)");
          throw new NotAllowedMethodException("Basic Auth is allowed.");
        }
      }

      var userTfaCode = CustomWebAuthenticationDetails.getVerificationCode();
      var userTfaMethod = loginUser.getTwoFa();
      String remoteAddress = CustomWebAuthenticationDetails.getRemoteAddress();
      Locale locale = CustomWebAuthenticationDetails.getLocale();

      if (userTfaMethod == null) {
        // perform default check
        super.additionalAuthenticationChecks(userDetails, authentication);
      }

      if (userTfaMethod != null && userTfaCode == null) {
        // perform default check
        super.additionalAuthenticationChecks(userDetails, authentication);

        if (userTfaMethod == MAIL) {
          sendAuthEmail(loginUser, locale);
          throw new TwoFactorAuthenticationRequiredException(
              "Verification code sent via email.", loginUser, MAIL);
        } else if (userTfaMethod == TOTP) {
          throw new TwoFactorAuthenticationRequiredException(
              "Awaiting verification code from a mobile app.", loginUser, TOTP);
        } else {
          throw new RuntimeException("Unknown two fa method. method=" + userTfaMethod);
        }
      }

      verifyCode(loginUser, userTfaCode);
    }
  }

  private void sendAuthEmail(LoginUser loginUser, Locale locale) {
    String mailAddress = loginUser.getMailAddress();
    User user = userService.getUser(loginUser.getId()).orElseThrow();
    if (!StringUtils.hasText(mailAddress)) {
      throw new MailAuthenticationSendingException("No email specified for email authentication.");
    }

    try {
      authenticationEmailService.sendEmail(user, locale);
    } catch (MessagingException | MailSendException e) {
      throw new MailAuthenticationSendingException("Failed to send authentication email.", e);
    }
  }

  private void verifyCode(LoginUser loginUser, String code) throws AuthenticationException {
    TwoFaType twoFaType = loginUser.getTwoFa();
    if (twoFaType == null) {
      log.debug("Two factor authentication not configured for current user.");
    } else if (twoFaType.equals(MAIL)) {
      verifyMailAuthCode(loginUser, code);
    } else if (twoFaType.equals(TOTP)) {
      verifyTotpCode(loginUser, code);
    }
  }

  private void verifyMailAuthCode(LoginUser loginUser, String code) throws AuthenticationException {
    // Check if the verification code is null or empty
    if (!StringUtils.hasText(code)) {
      throw new TwoFactorAuthenticationException("Empty verification code.", loginUser);
    }

    // Check if the token is recorded in the database
    Mail2faToken token =
        mail2faTokenService
            .find(code)
            .orElseThrow(() -> new TwoFactorAuthenticationException("Token not found.", loginUser));

    // Check if the token is expired
    ZonedDateTime expirationTime = token.getExpirationTime();
    if (expirationTime.isBefore(ZonedDateTime.now(ZoneId.systemDefault()))) {
      mail2faTokenService.delete(token);
      throw new TwoFactorAuthenticationExpiredException("Token expired.");
    }

    // Check that the login user ID matches the user ID registered to the token
    User tokenUser = token.getUser();
    if (!loginUser.getId().equals(tokenUser.getId())) {
      throw new TwoFactorAuthenticationException("Invalid token.", loginUser);
    }

    mail2faTokenService.delete(token);
  }

  private void verifyTotpCode(LoginUser loginUser, String code) throws AuthenticationException {
    // Check if the verification code is null or empty
    if (!StringUtils.hasText(code)) {
      throw new TwoFactorAuthenticationException("Empty verification code.", loginUser);
    }
    // Check if code is non numeric
    if (!code.matches("^\\d+$")) {
      throw new TwoFactorAuthenticationException("Invalid token.", loginUser);
    }

    GoogleAuthenticator gAuth = new GoogleAuthenticator();
    int intCode = Integer.parseInt(code);
    boolean isAuthorized = gAuth.authorize(loginUser.getSecret(), intCode);
    if (!isAuthorized) {
      UserScratchCode usc =
          accountScratchCodeService
              .findByUserIdAndScratchCode(loginUser.getId(), intCode)
              .orElseThrow(() -> new TwoFactorAuthenticationException("Invalid token.", loginUser));
      accountScratchCodeService.delete(usc);
    }
  }

  public int getOrder() {
    return this.order;
  }

  public void setOrder(int i) {
    this.order = i;
  }
}

CustomUserDetails

@Slf4j
@Component
@Profile("default")
public class CustomUserDetailsService implements UserDetailsService {

  private final UserRepository userRepository;
  private final UserPermissionService userPermissionService;
  private final LoginLockoutService loginLockoutService;
  private final LoginUserService loginUserService;
  private final NetworkPermissionService networkPermissionService;

  public CustomUserDetailsService(
      UserRepository userRepository,
      UserPermissionService userPermissionService,
      LoginLockoutService loginLockoutService,
      LoginUserService loginUserService,
      NetworkPermissionService networkPermissionService) {
    this.userRepository = userRepository;
    this.userPermissionService = userPermissionService;
    this.loginLockoutService = loginLockoutService;
    this.loginUserService = loginUserService;
    this.networkPermissionService = networkPermissionService;
  }

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user =
        userRepository
            .findByUsernameAndNotDeletedTrue(username)
            .orElseThrow(
                () -> {
                  String errorMsg = String.format("Username '%s' not found.", username);
                  log.debug(errorMsg);
                  return new UsernameNotFoundException(errorMsg);
                });
    LoginUser loginUser = loginUserService.getLoginUserByUser(user);

    log.debug("load user : {}", loginUser);

    boolean locked = loginLockoutService.isUserLocked(loginUser);
    loginUser.setLocked(locked);
    loginUser.setExpired(this.calcUserIsExpired(loginUser));

    UserPermission userPermission = userPermissionService.getUserPermission(user);
    loginUser.setBasicAuthAllowed(userPermission.isBasicAuth());

    Set<String> ipRangeList = networkPermissionService.createIpRangeList(user);
    loginUser.setIpRangeList(ipRangeList);

    return loginUser;
  }

  private boolean calcUserIsExpired(LoginUser loginUser) {
    final LocalDateTime expirationDate = loginUser.getExpirationDate();
    return expirationDate != null && expirationDate.isBefore(LocalDateTime.now());
  }
}

答案1

得分: 1

基于您提供的代码,看起来您正在实现一个具有多因素认证功能的授权服务器。您目前正试图将授权服务器端点(授权端点)与用户身份验证流程结合起来。

> 我应该如何在Spring授权服务器配置类中配置它?
>
> 还是我只应该在安全性配置类中配置它?

我强烈建议您将重点放在安全性配置中的多因素认证上,而不是在授权服务器配置中。我还建议您先单独实现身份验证流程(为原型/测试构建一个单独的应用程序),并在添加授权服务器依赖之前进行测试,以确保其正常运行。

我无法确定您的CustomDaoAuthenticationProvider是否可以直接使用,因为它相当复杂,但您可以查看一种备选的多因素认证方法,它在 Spring Security 多因素认证示例 中有所介绍。

一旦您查看并尝试了该示例,请参考这个 Spring 授权服务器多因素认证示例,该示例使用了Spring Security示例中的概念,并在其基础上进行了扩展。在查看第二个示例时,请注意,多因素认证的配置和所有自定义实现逻辑与授权服务器配置完全分开。

注意:目前授权服务器示例位于一个稍微陈旧的分支上。

英文:

Based on your provided code, it seems you're working through an implementation of an authorization server with multi-factor authentication. You are currently attempting to combine authorization server endpoints (the Authorization Endpoint) with the user authentication flow.

> How will I configure it in the spring authorization server config class?
>
> Or should I configure it only in security config class?

I would strongly recommend focusing on multi-factor authentication in the security configuration, not in the authorization server config. I would also recommend implementing your authentication flow separately first (build a separate application for prototyping/testing), and test it to ensure it working before adding the authorization server dependency.

I can't say for sure if your CustomDaoAuthenticationProvider will work as-is since it is quite complex, but take a look at an alternative approach to multi-factor authentication in the Spring Security mfa sample.

Once you've reviewed and tried out that sample, take a look at this Spring Authorization Server mfa sample which uses concepts from the Spring Security sample and builds on them. When reviewing the second sample, please notice that configuration for multi-factor authentication and all custom implementation logic is kept completely separate from authorization server configuration.

Note that the authz server sample is on a slightly stale branch at the moment.

huangapple
  • 本文由 发表于 2023年3月10日 01:29:42
  • 转载请务必保留本文链接:https://go.coder-hub.com/75688103.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定