如何正确设置 Spring boot 3 安全配置?

Use*_*r69 4 java spring-security jwt spring-boot

我正在尝试将更新的 Spring Security 集成到我的项目中,而不是使用已弃用的extending WebSecurityConfigurerAdapter. 我已经建立了一个很好的系统,用户可以在其中进行身份验证(User实现UserDetails- 我正在使用 Hibernate)并生成令牌。我在这次登录中得到了 200 并收到了一个令牌。这个认证部分工作正常。

现在的问题是我的用户具有角色(例如ADMIN,,USER...)这些角色被添加到生成的令牌中。我的控制器获得@PreAuthorize注释。请求无法通过这些注释并受到禁止。当我不使用 时@PreAuthorize,请求将使用令牌进行验证。

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
    private RSAKey rsaKey;
    private final DefaultUserDetailsService defaultUserDetailsService;
    
    public SecurityConfig(DefaultUserDetailsService defaultUserDetailsService) {
        this.defaultUserDetailsService = defaultUserDetailsService;
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        
    return http 
               .cors(Customizer.withDefaults())
               .csrf(AbstractHttpConfigurer::disable)
               .authorizeHttpRequests(auth -> auth
                   .requestMatchers("/auth/**").permitAll()
                   .anyRequest().authenticated()
               )            
               .userDetailsService(defaultUserDetailsService)
               .sessionManagement(session ->  session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
               .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
               .headers(headers -> headers
                   .frameOptions().sameOrigin()
               )
               .httpBasic(withDefaults())
               .build();
    }
    
    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        rsaKey = Jwks.generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }
    
    @Bean
    JwtDecoder jwtDecoder() throws JOSEException {
        return NimbusJwtDecoder.withPublicKey(rsaKey.toRSAPublicKey()).build();
   }
    
    @Bean
    JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwks) {
        return new NimbusJwtEncoder(jwks);
    }
    
    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
        
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:4200"));
        configuration.setAllowedMethods(List.of("GET","POST","DELETE"));
        configuration.setAllowedHeaders(List.of("Authorization","Content-Type"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**",configuration);
        return source;
    }

}
Run Code Online (Sandbox Code Playgroud)
@Component
public class KeyGeneratorUtils {

    private KeyGeneratorUtils() {}

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

Run Code Online (Sandbox Code Playgroud)
public class Jwks {
       private Jwks() {}

        public static RSAKey generateRsa() {
            KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
            RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
            return new RSAKey.Builder(publicKey)
                    .privateKey(privateKey)
                    .keyID(UUID.randomUUID().toString())
                    .build();
        }
}

Run Code Online (Sandbox Code Playgroud)
@Service
public class DefaultTokenService implements TokenService {
    private final JwtEncoder encoder;

    public DefaultTokenService(JwtEncoder encoder) {
        this.encoder = encoder;
    }
    
    @Override
    public String generateToken(Authentication authentication) {
        Instant now = Instant.now();
        String scope = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));
        
        System.out.println("scope: " + scope);
        
        JwtClaimsSet claims = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .expiresAt(now.plus(1, ChronoUnit.HOURS))
                .subject(authentication.getName())
                .claim("scope", scope)
                .build();
        return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }
}

Run Code Online (Sandbox Code Playgroud)
public class UserDetailsImpl implements UserDetails{
      private static final long serialVersionUID = 1L;
      private final Long id;
      private final String username;
      private final String riziv;
      private final boolean verified;
      @JsonIgnore
      private final String password;
      private final Collection<? extends GrantedAuthority> authorities;
    
        public UserDetailsImpl(Long id, String username, String riziv, String password,
                Collection<? extends GrantedAuthority> authorities, boolean verified) {
                this.id = id;
                this.username = username;
                this.riziv = riziv;
                this.password = password;
                this.authorities = authorities;
                this.verified = verified;
            }

        public static UserDetailsImpl build(AuthUser authUser) {
            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority(authUser.getRol().toString()));
          
            
            return new UserDetailsImpl(
                    authUser.getId(),
                    authUser.getUsername(),
                    authUser.getRiziv(),
                    authUser.getPassword(),
                    authorities, authUser.isVerified());
        }
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorities;
        }
        public Long getId() {
            return id;
        }
        public boolean isVerified() {
            return verified;
        }
        public String getRiziv() {
            return riziv;
        }
        @Override
        public String getUsername() {
            return username;
        }
        @Override
        public String getPassword() {
            return password;
        }
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
        @Override
        public boolean isEnabled() {
            return true;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;
            UserDetailsImpl klant = (UserDetailsImpl) o;
            return Objects.equals(id, klant.id);
        }

}
Run Code Online (Sandbox Code Playgroud)
@Service
public class DefaultUserDetailsService implements UserDetailsService {
    private final AuthUserService authUserService;
    
    public DefaultUserDetailsService(AuthUserService authUserService) {
        this.authUserService = authUserService;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        AuthUser authUser = authUserService.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));

        return UserDetailsImpl.build(authUser);
    }

}
Run Code Online (Sandbox Code Playgroud)
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
Run Code Online (Sandbox Code Playgroud)

我在某处犯了配置错误,但我似乎找不到它。Spring 文档非常非常难以理解,但我一直在坚持不懈地阅读它们。关于这些主题还没有很多明确的信息。我可以找到 youtube 视频教程和一些相关主题,但它们只解释小部分,而不是完整的设置。

我在下面添加了我的securityConfigKeyGeneratorJwks服务tokengenerate。我还刚刚添加了Userdetailsimpl和 服务。我userdetailsImpl使用静态构建方法构建了用户。这看起来可能是一个奇怪的结构,但它确实有效,因为我是最后做安全的,之前没有想到这一点。我还添加了我的一个例子@Preauthorize

我非常接近,这对于尝试实现此功能的其他用户来说可能是一个很好的例子,因为我似乎无法在某处找到示例。有人有设置 Spring Boot 3 安全性的经验吗?他们可以告诉我如何设置我配置错误吗?为什么我的角色没有被“读取” @PreAuthorize

Vee*_*Dee 5

好吧,事情是这样的,因为您正在实现资源服务器,所以类:

org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter 
Run Code Online (Sandbox Code Playgroud)

负责将 JWT 令牌内的范围转换为授予的权限。现在,此类在所有权限前面加上SCOPE_前缀。

既然你正在使用

org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter 
Run Code Online (Sandbox Code Playgroud)

该方法内部调用,

hasAnyRole('ADMIN', 'USER',...)
Run Code Online (Sandbox Code Playgroud)

方法,其中包含defaultRolePrefixasROLE_和 the roleNameas 您传入的值。

内部实现:

hasAnyAuthorityName(defaultRolePrefix, roleName) 
Run Code Online (Sandbox Code Playgroud)

另一方面,该hasAnyAuthority方法调用相同的方法,但null传递给defaultRolePrefix.

现在既然你正在使用:

@Override
public final boolean hasAnyRole(String... roles) {
    return hasAnyAuthorityName(this.defaultRolePrefix, roles);
}

@Override
public final boolean hasAnyAuthority(String... authorities) {
    return hasAnyAuthorityName(null, authorities);
}
Run Code Online (Sandbox Code Playgroud)

在安全配置中,它使用AuthenticationConverterJWT 令牌的默认值,即

org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
Run Code Online (Sandbox Code Playgroud)

这进一步调用了

org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
Run Code Online (Sandbox Code Playgroud)

根据 中的实现JwtGrantedAuthoritiesConverter,JWT 令牌中的所有范围都以SCOPE_我前面提到的前缀。

现在假设您授予的权限ADMIN作为角色之一返回。将其添加到scopeJWT 中后,默认转换器将SCOPE_ADMIN作为权限返回,同样,如果您在范围内返回,它将默认ROLE_ADMIN转换为。SCOPE_ROLE_ADMIN

该类JwtAuthenticationConverter返回一个实例

org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
Run Code Online (Sandbox Code Playgroud)

因此,可以通过以下方式修复:

或者,通过附加到您在范围中设置的角色名称hasAnyAuthority来检查权限。SCOPE_如果您的角色名称是ADMIN或者ROLE_ADMIN您应该使用

.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
Run Code Online (Sandbox Code Playgroud)

等等。如果你想使用hasAnyRole检查那么你必须使用

org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
Run Code Online (Sandbox Code Playgroud)

分别为ADMINROLE_ADMIN值。

或者,实现自定义权限转换器并将其传递到oauth2ResourceServer安全配置,如下所示,

自定义转换器示例

org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
Run Code Online (Sandbox Code Playgroud)

然后将 Spring Security 配置更新为:

org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
Run Code Online (Sandbox Code Playgroud)

使用第二个选项,您可以按照您想要的方式在 JWT 令牌和

@PreAuthorize("hasAnyAuthority('SCOPE_ADMIN')")
@PreAuthorize("hasAnyAuthority('SCOPE_ROLE_ADMIN')") 
Run Code Online (Sandbox Code Playgroud)

检查应该获取ROLE_ADMIN范围ADMIN,而不是ROLE_SCOPE_ADMIN现在的情况。