Solución:
Sí, puede usar el flujo de credenciales de cliente de OAuth 2.0 y las cuentas de servicio.
Keycloak sugiere 3 formas de asegurar los servicios REST de SpringBoot:
- con adaptador de bota de resorte Keycloak
- con adaptador de seguridad Spring Keycloak
- con OAuth2 / OpenID Connect
Aquí hay una buena explicación sobre esto con un ejemplo en la forma OAuth2 / OIDC:
- Tutorial de Arun B Chandrasekaran
- Ejemplo de código de Arun B Chandrasekaran
Si sigue este ejemplo, tenga en cuenta:
Tenga cuidado de configurar su cliente como:
- Tipo de acceso: confidencial
- Autorización: habilitada
- Cuenta de servicio (flujo de credenciales de cliente OAuth): habilitado
Tenga cuidado de configurar su servicio de destino como:
- Tipo de acceso: solo portador
Entonces, la persona que llama debe ser confidential
y el servicio de destino debe ser bearer-only
.
Crea tus usuarios, roles, mapeadores … y asigna roles a tus usuarios.
Compruebe que tiene estas dependencias en su proyecto de primavera:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
Configure la autenticación que se utilizará en el cliente REST (application.properties), por ejemplo:
security.oauth2.client.client-id=employee-service
security.oauth2.client.client-secret=68977d81-c59b-49aa-aada-58da9a43a850
security.oauth2.client.user-authorization-uri=${rest.security.issuer-uri}/protocol/openid-connect/auth
security.oauth2.client.access-token-uri=${rest.security.issuer-uri}/protocol/openid-connect/token
security.oauth2.client.scope=openid
security.oauth2.client.grant-type=client_credentials
Implementa tu JwtAccessTokenCustomizer
y SecurityConfigurer
(ResourceServerConfigurerAdapter) como la muestra de Arun.
Y finalmente implemente su controlador de servicio:
@RestController
@RequestMapping("/api/v1/employees")
public class EmployeeRestController {
@GetMapping(path = "/username")
@PreAuthorize("hasAnyAuthority('ROLE_USER')")
public ResponseEntity<String> getAuthorizedUserName() {
return ResponseEntity.ok(SecurityContextUtils.getUserName());
}
@GetMapping(path = "/roles")
@PreAuthorize("hasAnyAuthority('ROLE_USER')")
public ResponseEntity<Set<String>> getAuthorizedUserRoles() {
return ResponseEntity.ok(SecurityContextUtils.getUserRoles());
}
}
Para obtener un tutorial completo, lea el tutorial de Arun mencionado.
Espero eso ayude.
Siguiendo a @ dmitri-algazin para implementar el flujo de trabajo, tiene básicamente dos opciones:
- Si desea cubrir otros IdM además de Keycloak que resuelve de alguna manera el principio de responsabilidad única, usaría
RestTemplate
. A continuación puede encontrar las variables:
//Constants
@Value("${keycloak.url}")
private String keycloakUrl;
@Value("${keycloak.realm}")
private String keycloakRealm;
@Value("${keycloak.client_id}")
private String keycloakClientId;
RestTemplate restTemplate = new RestTemplate();
private static final String BEARER = "BEARER ";
Primero necesitas generar el token de acceso:
@Override
public AccessTokenResponse login(KeycloakUser user) throws NotAuthorizedException {
try {
String uri = keycloakUrl + "/realms/" + keycloakRealm +
"/protocol/openid-connect/token";
String data = "grant_type=password&username="+
user.getUsername()+"&password="+user.getPassword()+"&client_id="+
keycloakClientId;
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/x-www-form-urlencoded");
HttpEntity<String> entity = new HttpEntity<String>(data, headers);
ResponseEntity<AccessTokenResponse> response = restTemplate.exchange(uri,
HttpMethod.POST, entity, AccessTokenResponse.class);
if (response.getStatusCode().value() != HttpStatus.SC_OK) {
log.error("Unauthorised access to protected resource", response.getStatusCode().value());
throw new NotAuthorizedException("Unauthorised access to protected resource");
}
return response.getBody();
} catch (Exception ex) {
log.error("Unauthorised access to protected resource", ex);
throw new NotAuthorizedException("Unauthorised access to protected resource");
}
}
Y luego, con el token, puede recuperar información de los usuarios:
@Override
public String user(String authToken) throws NotAuthorizedException {
if (! authToken.toUpperCase().startsWith(BEARER)) {
throw new NotAuthorizedException("Invalid OAuth Header. Missing Bearer prefix");
}
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", authToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<AccessToken> response = restTemplate.exchange(
keycloakUrl + "/realms/" + keycloakRealm + "/protocol/openid-connect/userinfo",
HttpMethod.POST,
entity,
AccessToken.class);
if (response.getStatusCode().value() != HttpStatus.SC_OK) {
log.error("OAuth2 Authentication failure. "
+ "Invalid OAuth Token supplied in Authorization Header on Request. Code {}", response.getStatusCode().value());
throw new NotAuthorizedException("OAuth2 Authentication failure. "
+ "Invalid OAuth Token supplied in Authorization Header on Request.");
}
log.debug("User info: {}", response.getBody().getPreferredUsername());
return response.getBody().getPreferredUsername();
}
Puede sustituir esta URL por la proporcionada por @ dimitri-algazin para recuperar toda la información de los usuarios.
- Es posible utilizar las dependencias de Keycloak:
<!-- keycloak -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>3.4.3.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<version>3.1.4.Final</version>
</dependency>
Y usa las clases para generar el token:
Keycloak keycloak = KeycloakBuilder
.builder()
.serverUrl(keycloakUrl)
.realm(keycloakRealm)
.username(user.getUsername())
.password(user.getPassword())
.clientId(keycloakClientId)
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(10).build())
.build();
return keycloak.tokenManager().getAccessToken();
Los ejemplos se extraen de aquí. También subimos la imagen a Docker Hub para facilitar la interacción con Keycloak. Por esta razón comenzamos con la opción 2). En este momento estamos en el proceso de cubrir otros IdM y optamos por la opción 1) para evitar incluir dependencias adicionales. Conclusión:
Yo iria por opcion 2 si se apega a Keycloak porque las clases incluyen funcionalidades adicionales para la herramienta Keycloak. Yo iria por Opción 1 para mayor cobertura y otras herramientas de OAuth 2.0.