Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
Tags
- 디자인 패턴
- spring batch
- Spring Cloud Netfilx Eureka
- Transaction Pattern
- MSA
- Spring Boot Actuator
- JPA
- Action Pattern
- 생산자 소비자 패턴
- TypeScript
- Resilinece4j
- 알고리즘
- Java
- Parallel Old GC
- 키클락
- The law of Demeter
- 체인 패턴
- 사가 패턴
- 배치
- 디자인패턴
- saga pattern
- thread
- java 정렬
- 타입스크립트
- 멀티스레드
- zipkin
- 스레드
- spring cloud
- 스프링 배치
- Serial GC
Archives
- Today
- Total
PSD( Private-Self-Development )
키클락( keycloak ) 본문
사용 배경
신규 프로젝트를 진행하며,
Auth 서비스를 일일히 구현하는 대신, 오픈 소스 Auth 서비스를 사용해보았다.
키클락 이란?
- 인증 및 세션, 엑세스, 권한 관리를 해주는 ( IAM, SSO )오픈 소스 라이브러리 서버
- Red Hat 에서 주도 개발
키클락의 장점
- OAuth2.0 지원
- 다양한 언어 지원( java, python, js, 안드로이드 등 )
- 외부 사용자 저장소 지원 ( AD, LDAP 등 )
- 토큰 기반 보안
- 개발자 가 구현하는 것 보다 검증된 보안 서비스인 키를록을 사용하는것이 좀더 간단하게 보안성을 향상 시킬 수 있는 방법이다.
- 확장성도 가지고 있다. (서버 하나 더 띄우는듯? 세션은 어캐??)
구현
사용 스택
- java 21 + Spring boot 3.2.5 + security
- AWS EC2( 우분투 ) + Docker
- MariaDB
1. 키클락 서버 구축
Dockerfile.keycloak
FROM quay.io/keycloak/keycloak:26.0.5
COPY ./keycloak.conf /opt/keycloak/conf/keycloak.conf
ENTRYPOINT ["/bin/bash", "-c", "/opt/keycloak/bin/kc.sh show-config && /opt/keycloak/bin/kc.sh start"]
docker-compose.yml
keycloak:
build:
context: .
dockerfile: Dockerfile.keycloak
image: test/test-keycloak
container_name: keycloak
environment:
- KC_HEALTH_ENABLED=true
- KC_BOOTSTRAP_ADMIN_USERNAME=${KC_BOOTSTRAP_ADMIN_USERNAME}
- KC_BOOTSTRAP_ADMIN_PASSWORD=${KC_BOOTSTRAP_ADMIN_PASSWORD}
ports:
- ${KEYCLOAK_EXTERNAL_PORT:-7080}:${KEYCLOAK_PORT:-8080}
depends_on: # 컨테이너 시작 순서 제어
mariadb:
condition: service_healthy # 컨테이너의 상태 지정 가능
networks:
- test-net
2. 디펜던시 추가
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<version>${keycloak.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>${keycloak.admin.client.version}</version>
</dependency>
3. 프로퍼티 설정
keycloak:
realm: ${KEYCLOAK_REALM} # 저장소 명
auth-server-url: ${KEYCLOAK_SERVER_URL} # 키클락 서버 주소
resource: ${KEYCLOAK_CLIENT_ID} # 키클락 클라이언트 ID
credentials:
secret: ${KEYCLOAK_CLIENT_SECRET} # 키클락 클라이언트 secret 키
use-resource-role-mappings: true # 역할 사용 여부
principal-attribute: sub # 주체 속성?
ssl-required: all # ssl 필요 여부
bearer-only: true #
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:7080/realms/렐름명/protocol/openid-connect/certs
# security 에 oauth2 resourceServer 로 등록하면,
# 자동으로 security 가 jwt 토큰을 검사할 일이 있을 때,
# 키클락에게 검증 요청한다.
4. Config 설정
@Profile("prod")
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationConverter jwtAuthenticationConverter;
private final CorsConfigurationSource corsConfigurationSource;
private final CustomJwtAuthenticationFilter customJwtAuthenticationFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(
"/api/v1/auth/**",
"/api/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html"
).permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(
oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)))
.addFilterAfter(customJwtAuthenticationFilter, BearerTokenAuthenticationFilter.class);
return http.build();
}
}
5. Java 구현
계정 생성
// 생성 요청
Response response = adminKeycloakClient.realm(keycloakProperties.realm()).users().create(userRepresentation);
// 응답 데이터에서 authId 추출
authId = extractAuthIdFromResponse(response);
// 사용자에게 역할 할당
assignRolesToUser(authId, request.userRole());
---------------------------------------------------------------------------------
// 키클락 사용자 추가 요청 데이터 생성
private UserRepresentation buildUserRepresentation(
CreateAuthServerUserRequest request
) {
UserRepresentation userRepresentation = new UserRepresentation();
userRepresentation.setUsername(request.username().value());
userRepresentation.setEmail(request.email().value());
userRepresentation.setEnabled(true);
CredentialRepresentation credential = new CredentialRepresentation();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue(request.password());
userRepresentation.setCredentials(Collections.singletonList(credential));
return userRepresentation;
}
// 응답 데이터에서 authId 추출
private String extractAuthIdFromResponse(Response response) {
return response.getLocation().getPath().replaceAll(".*/(.*)$", "$1");
}
// 사용자에게 역할 할당
private void assignRolesToUser(String authId, UserRole role) {
List<RoleRepresentation> roleRepresentations = Collections.singletonList(
adminKeycloakClient.realm(keycloakProperties.realm())
.roles()
.get(role.name())
.toRepresentation()
);
adminKeycloakClient.realm(keycloakProperties.realm())
.users()
.get(authId)
.roles()
.realmLevel()
.add(roleRepresentations);
}
토큰 생성 ( 로그인 )
// 로그인한 사용자로 키클락 클라이언트 생성
Keycloak userKeycloakClient = KeycloakBuilder.builder()
.serverUrl(keycloakProperties.authServerUrl())
.realm(keycloakProperties.realm())
.clientId(keycloakProperties.resource())
.clientSecret(keycloakProperties.credentials().secret())
.username(request.username().value())
.password(request.password())
.grantType(OAuth2Constants.PASSWORD)
.build();
// 토큰 발급
AccessTokenResponse accessTokenResponse = userKeycloakClient.tokenManager().getAccessToken();
토큰 리플래시
// 토큰 리플래시 요청
String response = tokenRefresher.refreshToken(request.refreshToken());
// 응답 맵핑
AccessTokenResponse accessTokenResponse = new ObjectMapper().readValue(response, AccessTokenResponse.class);
// AuthId 추출
String sub = getSub(accessTokenResponse.getToken());
// 토큰 리플래시 요청 데이터 생성
private HttpRequest buildRefreshRequest(String refreshToken) {
String form = String.format(
"grant_type=refresh_token&client_id=%s&client_secret=%s&refresh_token=%s",
encode(keycloakProperties.resource()),
encode(keycloakProperties.credentials().secret()),
encode(refreshToken)
);
return HttpRequest.newBuilder()
.uri(URI.create(buildTokenEndpoint()))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(form))
.build();
}
private String getSub(String jwtToken) throws JsonProcessingException {
String[] parts = jwtToken.split("\\.");
if (parts.length != 3) {
throw new IllegalArgumentException("Invalid JWT token format");
}
String payload = new String(Base64.getUrlDecoder().decode(parts[1]));
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readTree(payload).get("sub").asText();
}
사용자 정보 업데이트
// 키클락에서 사용자 정보 조회
UserRepresentation userAuth =
adminKeycloakClient.realm(keycloakProperties.realm()).users().get(authId)
.toRepresentation();
// 정보 수정
userAuth.setEmail(request.email().value());
CredentialRepresentation credential = new CredentialRepresentation();
credential.setType(CredentialRepresentation.PASSWORD);
credential.setValue(request.password());
userAuth.setCredentials(Collections.singletonList(credential));
// 업데이트 요청
adminKeycloakClient.realm(keycloakProperties.realm()).users().get(authId).update(userAuth);
사용자 삭제
adminKeycloakClient.realm(keycloakProperties.realm()).users().delete(authId)
'Backend > 기타' 카테고리의 다른 글
Maven 다중 프로젝트 설정 (0) | 2024.12.17 |
---|---|
헥사고날 아키텍처(Hexagonal Architecture) (0) | 2024.07.15 |
도커( Doker ) (0) | 2024.04.29 |
최소 지식 원칙( 데메테르의 법칙 ) (0) | 2023.05.23 |
Nginx 란? (0) | 2023.01.19 |