KEYCLOAK - 4.(인가)_따라하기

킹콩(King Kong)·2025년 5월 5일

KEYCLOAK 사용법

목록 보기
4/4

0. 인가 개요

  • Keycloak은 <인증>과 <인가>를 위한 도구로써, 이번 포스트에서는 <인가>와 관련한 Keycloak 설정 및 Application 설정과 관련하여 방법을 간단히 기술한다.

    • Keycloak version : 26.1.3
    • 작성일 : 2025년 5월 5일
    • Keycloak을 활용한 인증 기능의 개념을 잡고 싶으면 아래 링크를 먼저 참고한다.
      KEYCLOAK - (인가) 개념잡기
  • 인가 가능 사용을 위하여 Authorization을 활성화 하고, Application에서 정의한 ROLE과 GROUP을 사용자(USER)에게 할당하면, 인증을 통과한 사용자는 인가받은 자원(resource)에 대해서만 접근이 허가(permit) 되도록 하는게 인가(Authorization)이다.

2. Authorization (인가) 따라하기

  • Keycloak 인가를 적용하기 위해 Client의 Authorization을 활성화 한다.

1) Authorization 활성화 하기

  • Client별로 인가를 적용할 것인지 선택할 수 있으며 아래와 같이 인가(Authorization)를 활성화 하면 즉시 인가가 동작한다.
    • Realm Name 선택 : 예) ROLE_TEST
    • Client 선택 : 예) role-test-site1
    • 선택한 Client > Settings 탭 > Capability config에서
    • Authorization : ON 선택하여 활성화
    • Save

  • 인가가 활성화 되면 상단 Sub 메뉴에 'Authorization 탭'이 나타난다.

2) 용어 정의

  • 인가를 적용하기 위하여 아래 4개의 용어에 대한 이해가 필요하다.

    1.자원(resource0: 사용자가 접근하려는 페이지(Web Page) 또는 URIs
    2.범위(scope) : 자원에 대한 동작 (CRUD, 즉 GET/POST/PUT/DELETE)
    3.정책(policy) : ROLE / GROUP / Time 등에 따른 규칙
    4.권한(permission) : 자원에 어떤 작업을 누구에게 허용(or 거부) 할지에 대한 정의

3) 자원(resource) 등록

  • 사용자가 접근하려면 자원은 Web화면(Page)과 데이터 서비스(RESTfull)일 것이다.
  • 이러한 자원(URIs) 전체를 낱개로 일일이 등록하여 통제를 할 수 도 있겠지만 몇개 그룹(패턴)으로 등록하여 관리하면 편리할 수 있다.
  • 예를 들어, 관리자(admin)과 사용자(user)로 구분할 경우,
    • 관리자가 사용하는 화면들은 /admin/ 하위 디렉토리에 모아 놓고, /admin/** URL로 호출하도록 URL 패턴을 정의할 수 있다.
    • 일반 사용자가 사용하는 화면들은 /user/ 하위 디렉토리에 모아 놓고, /user/** URL로 호출하도록 URL 패턴을 정의할 수 있다.
  • 이럴 경우 Keycloak에서 자원 등록은 아래와 같이 할 수 있다.
  • 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

  • Authorization scopes 가 안보이면 범위(scope)을 등록 후 다시 수정 할 수 있다.
  • 나머지 자원들도 URL 패턴으로 반복하여 등록한다.
    • 사용자 : /user/*
    • 관리자 : /manager/*
    • 게시판 : /bbs/*

4) 범위(scope) 등록

  • 인가(Authorization)에서 범위란 자원에 대해 어떤 행위(Action)을 할 수 있는지를 말한다.
  • 일반적으로 Web 환경에서는 C/R/U/D를 처리할때 GET / POST / PUT / DELETE 메소드를 사용한다.
  • 따라서 범위(scope)에는 아래와 같이 4개의 메소드를 등록한다.
  • Realm Name 선택 : 예) ROLE_TEST
  • Client 선택 : 예) role-test-site1
  • Client > Authorization 탭 선택
  • Authorization > Scope 탭 선택
  • Create authorization scope 선택
    • Name : GET
    • Displaey name : GET
  • Save

  • 나머지 POST / PUT / DELETE 도 등록한다.

5) 정책(policy) 등록

  • 정책(policy)에서는 다양한 방식의 정책을 설정 할 수 있다.

  • 예를 들면, ROLE / GROUP / TIME(시간대)에 따른 정책 및 사용자(User) 별 서로 다른 정책 등을 정의하여 자원에 대한 접근을 제어 할 수 있다.

    ROLE 기반 정책

    • 사용자에게 할당된 ROLE 별로 자원에 대해 서로 다르게 접근할 수 있는 정책

    GROUP 기반 정책

    • 사용자에게 할당된 GROUP 별로 자원에 대해 서로 다르게 접근할 수 있는 정책

    Time 기반 정책

    • 사용자에게 특정 시간대에만 자원의 접근 여부를 정의하는 정책
      • 예) 근무 시간대 (09:00 ~ 18:00) 에만 Site에 접속할 수 있게 한다.

    User 기반 정책

    • 특정 리소스에 ROLE/GROUP이 아닌 특정 사용자(user)를 할당하여 그 사용자만 특정 자원에 접근을 허용(거부)하게 하는 정책

[ ROLE 기반 정책을 등록 ]

  • 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

  • Client > Roles 에서 등록한 ROLE 별로 1:1로 Policy를 등록한다.

[ Time 기반 정책을 등록 ]

  • 특정 날짜와 특정 시간대를 설정하여 접근제어를 할 수 있다.

[ 특정일 + 특정 시간대 접근권한 설정 시 (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

  • 위와 같이 설정한 policy를 걸어주면 2025.05.11 하루만 특정 자원을 사용할 수 있다.

  • 근무 시간대(09:00 ~ 18:00)에만 시스템에 접근하기 (반복 설정)

[ 반복 시간대 설정 시 (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

  • 요일을 지정하는 설정은 없음

6) 권한(permission) 등록

  • 권한은 앞에서 등록한 자원(resource) + 범위(scope) + 정책(policy)을 조합하여 권한제어를 할 수 있다.
  • 이런 권한 등록 방식에는 2가지가 있다.
    • Resource 기반 권한 등록 : 자원 vs 권한(ROLE)을 매핑하여 권한을 등록한다.
    • Scope 기반 권한 등록 : 자원 vs 범위 vs 권한, 이 3가지 조합을 맵핑하여 권한을 등록한다.
  • 예를 들어
    • admin-resource 는 전체 scope(GET / POST / PUT / DELETE)과 admin-policy를 조합으로 등록한다. 이러면 admin-resouce는 ADMIN 권한자만 접근할 수 있다.
    • user-resource는 user-policy와 admin-policy를 조합으로 등록하면, 관리자는 user-resource까지 접근할 수 있게 된다.

[ Scope 기반 권한 등록 ]

  • 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 눌러 저장.

  • Scope-based는 특정 자원에 대해 어떤 행동을 할 수 있도록 정책을 맵핍할 수 있다.
  • 반면에 Role-based는 자원 vs 정책 만 맵핑이 가능해 어던 행동까지는 제어를 할 수 없다.

7) Application 에서의 권한 제어

  • Keycloak의 인가(Authorization)을 활성화 시킨 후,
    • Keycloak에서 자원/범위/정책/권한을 셋팅했어도, Application(client)에서는 Keycloak에서 등록한 권한 규칙대로 동작하지 않는다.
    • 그 이유는 Application의 어떤 자원이 어떤 권한에 대해서만 허가(permit) 되었는지를 알 수가 없기 때문이다.
  • 즉, Keycloak과 Application이 연동이 되어 있어도, 인가(Authorization) 규칙에 대해서는 Application이 Keycloak에 그 정보를 요청 해야만 알 수 가 있다.
    • 이를 위해서 Keycloak은 Authorization Endpoint를 제공하고 있고,
    • Application은 이 Endpoint에 사용자의 권한으로 접근할 수 있는 자원목록을 받아서
    • 권한에 맞는 자원의 접근여부에 따라 동작할 수 있도록 제어를 해야한다.

7.1) Keycloak Authorization Endpoint 호출

  • Application 이 권한 제어를 위하여 Keycloak에 Authorization Endpoint를 호출한다.
    • INPUT-1 : Endpoint URL (예: /protocol/openid-connect/token)
    • INPUT-2 : Client Id (예: role-test-site1)
    • INPUT-3 : Access Token (인증 후 Keycloak에서 발급해 준 token)
  • 간단한 Sample을 보면 아래와 같다.
# 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;
}

7.2) Request Party Token (RPT) 샘플

  • Keycloak이 응답을 준 RPT 토큰 샘플이다.
  • 해당 권한으로 접근할 수 있는 자원별 + scope 이 리턴 된다.
    • 자원별로 자원명(rsname), 범위(scope), 자원ID(rsid)이 Object로 리턴된다.
# 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/*"
    }
]

7.3) Application에서의 권한 체크 수행

  • Application은 사용자의 요청이 올때마다 요청한 자원(URL)에 대한 범위(Method)가 사용자의 권한으로 수행할 수 있는지를 아래와 같은 방식으로 체크할 수 있다.
  • 사용자의 모든 수행에 대하여 체크를 수행해야 하므로 아래 방식에서는 Interceptor의 preHandle()함수에서 권한을 체크하도록 구현하였다.
# 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;
}
  • 이때 RPToken에는 URIs 값이 넘어오지 않기 때문에 자원명(rsname)을 활용하여서 권한체크를 수행하도록 한다.

7.4) 인가 체크를 위한 성능 이슈

  • 위와 같은 방식으로 Application의 모든 요청에 대해서 Keycloak Endpoint에 RPT 토큰을 요청해서 체크하면 Keycloak이 부하가 많이 걸린다.
  • 이를 해결하기 위하여 아래와 같이 Cache를 활용하면 성능 이슈를 해결할 수 있다.
    • 아래는 Redis cache를 활용한 샘플 - 사용자(userId) 별로 Cache를 한다.
    • 즉 사용자가 최초 로그인시에 RPT 토큰을 읽어 Cache에 저장, 이후에는 Cache만 읽는다.
# 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;
}

8) Decision strategey

  • 자원 vs 범위 vs 정책을 조합해서 권한(permission)을 정의할 때, 같은 자원에 대해서 한쪽 정책에서는 허용(permit)하지만, 다른 정책에서는 거부(deny)가 될 수도 있다.
  • Keycloak에서 이런 경우를 해결하기 위해 아래와 같은 전략으로 권한을 재조립하게 된다.

1.Affirmative(긍정, 동의)

  • 여러 정책 중에 하나라도 허용이면 전체를 <허용>
  • 가장 관대한 방식

2.Unamimouse(만장일치)

  • 모든 정책이 허용이어야 함, 하나라도 불가가 있으면 전체가 <불가>
  • 가장 엄격한 방식

3.Consensus(합의)

  • 허용의 개수가 불가보다 많으면 <허용>, 불가의 개수가 허용보다 많으면 <불가>
  • 균형있는 다수결 방식

8.0) Decision strategy 선택의 고민

  • 위 3가지 전략중에 어떤 전략을 사용해야 할까 잠깐 고민해 본다.
    • 고민-1 : 사용자에게 권한을 여려개 줄까?
    • 고민-2 : 사용자는 1개의 권한만 가지게 하고, 자원에 여러 정책을 할당 할까?
  • 고민 1/2의 장,단점을 분석해 보면 아래와 같다.
구분장점단점
고민-1자원에 정책을 매번 할당할 필요 없다. 한가지 대표 정책만 할당자원에 정책을 할당하지 않으면 사용자에게 여러가지 권한을 할당해야 하는데, 이렇게 되면 사용자가 접근하지 말아야하는 자원까지 접근할 수 있게 됨
고민-2사용자의 권한은 1개의 대표 권한만 지정, 특정 권한의 모든 자원에 접근하는게 아니라 할당받은 자원에 대해서만 접근이 가능하여 보안이 강화 됨모든 자원에 대해서 어떤 정책을 할당할 때 숙고해야 함

8.1) Affirmative (긍정, 동의)

  • 하나의 자원에 여러 정책을 할당했을때, 어느 하나의 정책만 만족하면 접근 가능

  • 위에는 /user/* 라는 자원은 user-policy 또는 admin-policy 또는 manager-policy 중에 1개의 policy만 있으면 접근할 수 있다.
  • 이런경우 Affirmative 를 지정한다.

8.2) Unanimous (만장일치)

  • 아래는 관리자 화면중에 수정/삭제/생성 등의 Action은 근무시간에만 수행하고, 근무시간이 지나면 자동으로 수행 못하도록 할 수 있다.
  • 이런 경우 admin-plicy 와 workhour-poicy가 동시에 만족해야 한다.
  • 이런 경우 Unamimous 를 지정한다.

8.3) Consensus (합의)

  • 합의는 하나의 자원에 대해 접근을 허용(permit)하는 정책의 개수와, 거부(deny)하는 정책의 개수 중에서 큰 쪽의 정책을 따르게 하는 전략이다.
  • 전략이 모호하여 잘 쓰이지 않을 듯 함. (개인의견)
profile
IT를 쉽게 이해해 보아요~😄

0개의 댓글