Keycloak은 <인증>과 <인가>를 위한 도구로써, 이번 포스트에서는 <인가>와 관련한 Keycloak 설정 및 Application 설정과 관련하여 방법을 간단히 기술한다.
- Keycloak version : 26.1.3
- 작성일 : 2025년 5월 5일
- Keycloak을 활용한 인증 기능의 개념을 잡고 싶으면 아래 링크를 먼저 참고한다.
KEYCLOAK - (인가) 개념잡기
인가 가능 사용을 위하여 Authorization을 활성화 하고, Application에서 정의한 ROLE과 GROUP을 사용자(USER)에게 할당하면, 인증을 통과한 사용자는 인가받은 자원(resource)에 대해서만 접근이 허가(permit) 되도록 하는게 인가(Authorization)이다.
- Realm Name 선택 : 예) ROLE_TEST
- Client 선택 : 예) role-test-site1
- 선택한 Client > Settings 탭 > Capability config에서
- Authorization : ON 선택하여 활성화
- Save

1.자원(resource0: 사용자가 접근하려는 페이지(Web Page) 또는 URIs
2.범위(scope) : 자원에 대한 동작 (CRUD, 즉 GET/POST/PUT/DELETE)
3.정책(policy) : ROLE / GROUP / Time 등에 따른 규칙
4.권한(permission) : 자원에 어떤 작업을 누구에게 허용(or 거부) 할지에 대한 정의
- Realm Name 선택 : 예) ROLE_TEST
- Client 선택 : 예) role-test-site1
- Client > Authorization 탭 선택
- Authorization 선택 후 Resource 탭 선택
- Create resource 선택
- Name : /admin/*
- Display name : /admin/*
- URIs : /admin/*
- Autohrization scops : GET / POST / PUT / DELETE 선택
- Save

- Realm Name 선택 : 예) ROLE_TEST
- Client 선택 : 예) role-test-site1
- Client > Authorization 탭 선택
- Authorization > Scope 탭 선택
- Create authorization scope 선택
- Name : GET
- Displaey name : GET
- Save

정책(policy)에서는 다양한 방식의 정책을 설정 할 수 있다.
예를 들면, ROLE / GROUP / TIME(시간대)에 따른 정책 및 사용자(User) 별 서로 다른 정책 등을 정의하여 자원에 대한 접근을 제어 할 수 있다.
ROLE 기반 정책
- 사용자에게 할당된 ROLE 별로 자원에 대해 서로 다르게 접근할 수 있는 정책
GROUP 기반 정책
- 사용자에게 할당된 GROUP 별로 자원에 대해 서로 다르게 접근할 수 있는 정책
Time 기반 정책
- 사용자에게 특정 시간대에만 자원의 접근 여부를 정의하는 정책
- 예) 근무 시간대 (09:00 ~ 18:00) 에만 Site에 접속할 수 있게 한다.
User 기반 정책
- 특정 리소스에 ROLE/GROUP이 아닌 특정 사용자(user)를 할당하여 그 사용자만 특정 자원에 접근을 허용(거부)하게 하는 정책
- Realm Name 선택 : 예) ROLE_TEST
- Client 선택 : 예) role-test-site1
- Client > Authorization 탭 선택
- Authorization > Policies 탭 선택
- Create client policy 선택
- (정책 유형 팝업에서) Role 선택
- Name : 예) admin-policy
- Description : 예) admin-policy
- Add roles : 예) SITE1_ADMIN
- Save


[ 특정일 + 특정 시간대 접근권한 설정 시 (No repeat) ]
- Realm Name 선택 : 예) ROLE_TEST
- Client 선택 : 예) role-test-site1
- Client > Authorization 탭 선택
- Authorization > Policies 탭 선택
- Create client policy 선택
- (정책 유형 팝업에서) Time 선택
- Name : 예) special-time-20250511
- Description : 예) 특정일 + 특정시간대에만 접근
- Repeat : Not repeat 선택
- Start time : 2025-05-11 00:00
- Expire time: 2100-05-11 23:59
- Save

[ 반복 시간대 설정 시 (Repeat) ]
- Realm Name 선택 : 예) ROLE_TEST
- Client 선택 : 예) role-test-site1
- Client > Authorization 탭 선택
- Authorization > Policies 탭 선택
- Create client policy 선택
- (정책 유형 팝업에서) Time 선택
- Name : 예) workhour-policy
- Description : 예) 근무시간대(09:00~18:00)에만 접근
- Repeat : Repeat 선택
- Month : 1 ~ 12
- Day : 1 ~ 31
- Hour : 9 ~ 17 ( 09:00 ~ 18:00 까지만 접근 가능 )
- Minute : 0 ~ 59 (반드시 59분까지 해야한다. 0 ~ 0 으로 하면 안먹힘)
- Start time : 2025-01-01 00:00
- Expire time: 2100-12-31 00:00
- Save

- Resource 기반 권한 등록 : 자원 vs 권한(ROLE)을 매핑하여 권한을 등록한다.
- Scope 기반 권한 등록 : 자원 vs 범위 vs 권한, 이 3가지 조합을 맵핑하여 권한을 등록한다.
- Realm Name 선택 : 예) ROLE_TEST
- Client 선택 : 예) role-test-site1
- Client > Authorization 탭 선택
- Authorization > Permissions 탭 선택
- Create permission 선택
- 콤보 박스에서 'Create scope-based permission' 선택
- Name : 예) user-permission
- Description : 예) user-permission
- Resource : 예) /user/* (자원을 선택하면 resouce-id로 변경 됨)
- Authorization scops : 예) GET / POST 제어하고 싶은 scope 선택
- Policies : 예) admin-policy, user-policy 등 선택
- Decision strategy : Affirmative / Unanimous / Consensus 중 선택
▶ Affirmative : 여러개 정책(policy) 중 1개만 있어도 허용(permit)
▶ Unamimous : 모든 정책(policy)가 다 있어야 허용(permit)
▶ Consensus : 허용의 개수가 많으면 허용(permit), 불가가 많으면 불가(deny)- Save 눌러 저장.

# Sample 소스
# tokentEndpoint, clientId, accessToken으로 Keycloak에서 RPToken을 받아 온다.
System.out.println("------------------[ keycloakUtil.getRPToken ]----------------------");
String tokenEndpoint = jwtIssuerUri + "/protocol/openid-connect/token";
List<Map<String,String>> rptoken = KeycloakUtil.getRPToken(tokenEndpoint, clientId, accessToken);
# getRPToken()
public static List<Map<String,String>> getRPToken(String tokenEndpoint, String clientId, String accessToken) {
//
try(CloseableHttpClient client = HttpClients.createDefault()) {
HttpPost post = new HttpPost(tokenEndpoint);
post.setHeader("Content-Type", "application/x-www-form-urlencoded");
post.setHeader("Authorization", "Bearer " + accessToken);
//
List<BasicNameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("grant_type", "urn:ietf:params:oauth:grant-type:uma-ticket"));
params.add(new BasicNameValuePair("audience", clientId));
params.add(new BasicNameValuePair("response_mode", "permissions"));
post.setEntity(new UrlEncodedFormEntity(params));
return client.execute(post, httpResponse -> {
if( httpResponse.getStatusLine().getStatusCode() == 200 ) {
ObjectMapper mapper = new ObjectMapper();
List<Map<String, String>> list = mapper.readValue(httpResponse.getEntity().getContent(), List.class);
return list;
} else {
return null;
}
});
} catch (Exception e) {
System.out.println(e.getMessage());
}
return null;
}
# RPT sample
[{
"rsid": "887ca0e3-bf12-4703-9e47-06c5afdd85d9",
"scopes": [
"POST",
"GET"
],
"rsname": "/jwt-decode"
},
{
"rsid": "88110074-6c39-47e2-b4ff-002172b655d4",
"scopes": [
"DELETE",
"POST",
"GET",
"PUT"
],
"rsname": "/user/*"
},
{
"rsname": "Default Resource",
"rsid": "80316867-a6bf-4c3e-b329-5b9c0d83b344"
},
{
"rsid": "6d8166dd-fd3d-4a3f-a67a-417adf73af39",
"scopes": [
"DELETE",
"POST",
"GET",
"PUT"
],
"rsname": "/admin/*"
}
]
# Sample 소스
@Component
public class KeycloakAuthorizationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception {
String resource = request.getRequestURI();
String scope = request.getMethod().toUpperCase();
List<Map<String,String>> rpToken = ; 앞에서 얻은 Token값
return checkPermission(rpToken, resource, scope);
}
# checkPermission()
private boolean checkPermission(List<Map<String,String>> rpToken, String resource, String scope ) {
for(Map<String,String> perm : rpToken) {
String resourcePattern = perm.get("rsname");
String regex = resourcePattern.replace("*", ".*");
//
if( resource.matches(regex) ) {
Object scopes = perm.get("scopes");
if( scopes.toString().indexOf(scope)>=0 ) {
return true;
} else {
return false;
}
}
}
return false;
}
# Redis cache를 활용한 샘플
private List<Map<String,String>> readRPToken(String username, String accessToken) {
// Redis cache에서 먼저 읽는다.
// Redis cache에 없으면 Keycloak 에서 직접 읽는다.
String redisKey = "rpt:" + username;
List<Map<String,String>> cachedRptoken = null;
boolean IS_REDIS_OK = false;
// STEP-1 :: Redis cache READ...
try {
cachedRptoken = (List<Map<String,String>>) redisTemplate.opsForValue().get(redisKey);
IS_REDIS_OK = true;
} catch (Exception e) {
IS_REDIS_OK = false;
System.out.println(e.getMessage());
}
// STEP-2 :: Cache없으면 Keycloak에 요청...
if( cachedRptoken == null ) {
String tokenEndpoint = jwtIssuerUri + "/protocol/openid-connect/token";
List<Map<String,String>> rptoken = KeycloakUtil.getRPToken(tokenEndpoint, clientId, accessToken);
if( rptoken != null ) {
cachedRptoken = rptoken;
// STEP-3 :: 다음에 Cache에서 읽기 위하여 Cache에 기록해 놓는다.
if( IS_REDIS_OK ) {
try {
redisTemplate.opsForValue().set(redisKey, rptoken, Duration.ofMinutes(5));
} catch (Exception e) {
System.out.println("REDIS_WRITE_FAIL=" + e.getMessage());
}
}
}
}
return cachedRptoken;
}
1.Affirmative(긍정, 동의)
- 여러 정책 중에 하나라도 허용이면 전체를 <허용>
- 가장 관대한 방식
2.Unamimouse(만장일치)
- 모든 정책이 허용이어야 함, 하나라도 불가가 있으면 전체가 <불가>
- 가장 엄격한 방식
3.Consensus(합의)
- 허용의 개수가 불가보다 많으면 <허용>, 불가의 개수가 허용보다 많으면 <불가>
- 균형있는 다수결 방식
| 구분 | 장점 | 단점 |
|---|---|---|
| 고민-1 | 자원에 정책을 매번 할당할 필요 없다. 한가지 대표 정책만 할당 | 자원에 정책을 할당하지 않으면 사용자에게 여러가지 권한을 할당해야 하는데, 이렇게 되면 사용자가 접근하지 말아야하는 자원까지 접근할 수 있게 됨 |
| 고민-2 | 사용자의 권한은 1개의 대표 권한만 지정, 특정 권한의 모든 자원에 접근하는게 아니라 할당받은 자원에 대해서만 접근이 가능하여 보안이 강화 됨 | 모든 자원에 대해서 어떤 정책을 할당할 때 숙고해야 함 |

