oauth2.0 安全配置中的会话主体可以在spring security server中重用吗?

knsnq2tg  于 2023-03-17  发布在  Spring
关注(0)|答案(1)|浏览(138)

我有一个类LoginUser,它实现了UserDetails。
我还有一个类CustomDaoAuthenticationProvider,它扩展了DaoAuthenticationProvider。
我使用的是Spring Security 5和Spring授权服务器版本0.3.1。
在OAuth2过程中,授权码授予类型,用户需要登录以允许客户端程序获取授权码和令牌。
登录时,在LoginUser中设置用户名和密码,w/c在CustomDaoAuthenticationProvider中完成。
预期结果是LoginUser将成为身份验证的会话主体。
当在端点中请求时,端点方法中的@AuthenticationPrincipal注解参数将能够获取LoginUser....但当我假设LoginUser是值时,我得到的是空值,从而导致NPE。
我将如何在spring授权服务器配置类中配置它?
或者我应该只在安全配置类中配置它?如何配置?
下面是我的SecurityConfig类:

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

这是身份验证服务器配置类:

@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();
  }
}

自定义DaoAuthentication提供程序

@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;
  }
}

自定义用户详细信息

@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());
  }
}
bkkx9g8r

bkkx9g8r1#

根据您提供的代码,您似乎正在实现具有多因素身份验证的授权服务器。您当前正在尝试将授权服务器终结点(授权终结点)与用户身份验证流组合在一起。
我将如何在spring授权服务器配置类中配置它?
或者我应该只在安全配置类中配置它?
我强烈建议在安全配置中而不是在授权服务器配置中关注多因素身份验证。我还建议首先单独实现身份验证流程(构建单独的应用程序进行原型开发/测试),并在添加授权服务器依赖项之前对其进行测试以确保其正常工作。
我不能肯定您的CustomDaoAuthenticationProvider是否会按原样工作,因为它相当复杂,但是请看一看Spring Security mfa sample中多因素身份验证的替代方法。
查看并试用了该示例后,请看一下这个Spring Authorization Server mfa sample,它使用了Spring Security示例中的概念,并在此基础上进行了构建。查看第二个示例时,请注意多因素身份验证和所有自定义实现逻辑的配置与授权服务器配置完全分开。

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

相关问题