Keycloak认证通过后访问Spring Boot接口返回403 Forbidden问题
Let's work through this 403 issue step by step. You've got authentication working (you can log in via Keycloak) but authorization is failing, which means your Spring Boot app isn't correctly recognizing the role you assigned in Keycloak. Here are the most likely fixes:
1. Fix Role Mapping Between Keycloak and Spring Security
Spring Security doesn't automatically parse Keycloak's realm roles from the JWT token by default. You need to add a custom converter to extract the realm_access.roles claim from the token and map it to Spring Security authorities.
First, add this bean to your SecurityConfig class:
@Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // Point to Keycloak's realm roles claim grantedAuthoritiesConverter.setAuthoritiesClaimName("realm_access"); // Add ROLE_ prefix to match Spring Security's hasRole() behavior grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); converter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return converter; }
Then update your filterChain to use this converter for both OAuth2 login and resource server validation (since after login, your app uses the access token to protect endpoints):
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeRequests(auth -> auth .antMatchers("/public/**").permitAll() .anyRequest().hasRole("test") ) .csrf(csrf -> csrf.disable()) .oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt .jwtAuthenticationConverter(jwtAuthenticationConverter()) ) ) .logout(logout -> logout .addLogoutHandler(keycloakLogoutHandler) .logoutSuccessUrl("/") ); return http.build(); }
2. Verify Keycloak Client Configuration
Double-check these settings in your Keycloak Console for the test2 client:
- Access Type: Make sure it's set to
confidential(you're using a client-secret, so this is required). - Valid Redirect URIs: Ensure it includes your Spring Boot app's callback URL, e.g.,
http://localhost:8081/*(replace 8081 with your app's actual port). - Scopes: Go to the Scopes tab and confirm
rolesis in the Default Client Scopes list. If not, add it—this ensures Keycloak includes role information in the JWT token.
3. Confirm Role Assignment and Case Sensitivity
- Make sure the role you assigned to your user in Keycloak is exactly
test(case-sensitive) to match thehasRole("test")rule in your security config. - Check that the user is actually assigned to this role: Go to Users > [your user] > Role Mappings > select your realm under Client Roles and verify
testis in the Assigned Roles list.
4. Validate the JWT Token
To confirm Keycloak is sending the role correctly:
- After logging in, open your browser's dev tools (F12) and go to the Network tab.
- Look for a request to your protected endpoint and copy the
Authorizationheader's access token (the part afterBearer). - Paste this token into jwt.io (you don't need to verify the signature, just check the payload).
- Ensure the
realm_accessfield exists and includes"test"in therolesarray, like this:"realm_access": { "roles": [ "test", "default-roles-test" ] }
If this field is missing, go back to Keycloak's client scope settings to fix it.
5. Check Issuer URI Consistency
- If your Spring Boot app is running in a Docker container,
http://keycloak:8080/auth/realms/testis correct (since Docker Compose uses service names for DNS). - If you're running the app locally (outside Docker), you can keep using
http://keycloak:8080/...thanks to your hosts file entry, but make sure theissclaim in the JWT matches this URI exactly (Keycloak's issuer is set to the URL it's accessed from, so if you logged in vialocalhost:8080, theisswill behttp://localhost:8080/auth/realms/test—in that case, update yourapplication.propertiesto match).
内容的提问来源于stack exchange,提问作者randomguy

