- 키클락이란 인증 서버를 제공해주는 서비스로 바로 사용할 수 있는 구축된 서버를 제공한다.
- 키클락 외에도 Okta, ForgeRock, Amazon Congnito등에서 인증 서버를 제공해준다.
- 키클락은 오픈소스로서 비용이 들지 않음에도 불구하고 안정적이며 주기적인 업데이트가 이뤄진다.
- 외에도, 액세스 토큰 발급, SSO 기능(하나의 인증서버를 통해 다른 모든 어플리케이션에 접근 가능하도록 하는 기능) 제공, 소셜 로그인 기능, 중앙 제어 기능 등등을 제공한다.
- 사이트에서 파일을 다운받은 뒤 터미널에서 bin/kc.bat start-dev로 실행하면 된다.
- 이제 port번호 8080 으로 실행될 것이며 localhost 8080으로 접속하면 관리자 계정을 생성하고 여러가지 권한을 수정할 수 있다.
- 우선 keycloak에 로그인하면 Master realm이란 페이지가 나온다. 여기서 realm이란 인증 서버 내의 공간을 의미한다. 해당 공간에서 user, role, client를 생성할 수 있다.
- Master realm은 모든 어플리케이션을 관리하는 공간이다. 하지만 모든 어플리케이션을 한 곳에서 관리하면 확장성이 매우 떨어진다. 따라서 각각의 어플리케이션마다 realm을 따로 만들어 어플리케이션마다 다른 권한과 역할을 부여해주는것이 효율적이다.
- 따라서 우리 어플리케이션에 적합한 새로운 realm을 생성해야한다.
- 새로운 realm을 생성했으면 내부에서 클라이언트, 역할, 사용자, 그룹등을 원하는만큼 만들 수 있다.
- 위의 방법들을 따라하면 인증 서버를 성공적으로 구축할 수 있다.
- client credentials grant type flow로 엔드유저가 연관돼있지 않은 시나리오다.
- 우선 클라이언트 어플리케이션에서 인증 서버에 보내줄 client 정보를 받아야한다. 그러므로 keycloak 인증 서버에 클라이언트를 등록해보자.
- 우선 eazybank realm에 들어가고 client 메뉴에 들어가면 원하는 만큼 client를 등록할 수 있다. !(키클락)[키클락예시.png]
- 이 시나리오에선 우리가 직접 등록을 하지만 실제에서는 서드 파티에서 요청을 하고 내부 로직에서 타당한지 검사한 후에 keycloak에 클라이언트 등록을 해주는 방식으로 동작해야할것이다.
- 클라이언트 생성을 해보자 먼저 oauth2인지 openid connect 인지 설정해주고 그 다음엔 클라이언트 secret을 통한 인증을 진행할 것인지 정해줘야한다. 이후 인증 유형을 정해줘야한다. !(클라이언트)[클라이언트예시.png]
- 위에서 standard flow는 authentication code flow를 의미하므로 해제 해준다. 우리는 client credentials grant type을 사용할 것이니 Service accounts roles를 선택해준다.
- 이제 저장을 해주면 클라이언트 secret을 확인할 수 있다.
- 우선 백엔드 어플리케이션을 리소스 서버로 동작하게 만들기 위해선 다음과 같은 의존성이 필요하다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
- 위 의존성을 추가하면 백엔드 어플리케이션은 리소스 서버처럼 행동하며, api를 요청하는 유저에게 액세스 토큰을 요청하게 된다.
- 이제 액세스 토큰에서 유저의 정보를 추추하여 백엔드에서 이해할 수 있는 형식으로 바꿔줄 클래스를 만든다.
public class KeycloakRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
- 클래스는 다음과 같이 jwt 토큰을 GrantedAuthority 컬렉션으로 반환하는 Converter를 구현해야한다.
- 해당 인터페이스는 다음과 같은 메소드를 오버라이드 해야한다.
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
Map<String, Object> realmAccess = (Map<String, Object>) jwt.getClaims().get("realm_access");
if (realmAccess == null || realmAccess.isEmpty()) {
return new ArrayList<>();
}
Collection<GrantedAuthority> returnValue = ((List<String>) realmAccess.get("roles"))
.stream().map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return returnValue;
}
- jwt.getClaims를 통해 이름이 realm_access인 Map을 가져온다.
- 이후 이름이 roles인 모든 객체를 반환시켜 리스트로 만들어 반환한다.
- 이제 defaultSecurityFilterchain 내부에 다음과 같은 코드를 통하여 우리가 만든 컨버터를 인식할 수 있게 만들어줘야한다.
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
- 이제 아래와 같은 코드로 리소스 서버의 기능을 수행하도록 설정해준다.
.requestMatchers("/user").authenticated()
.requestMatchers("/notices","/contact","/register").permitAll())
.oauth2ResourceServer(oauth2ResourceServerCustomizer ->
oauth2ResourceServerCustomizer.jwt(jwtCustomizer -> jwtCustomizer.jwtAuthenticationConverter(jwtAuthenticationConverter)));
- 아까 만들어둔 jwtAuthenticationConverter도 등록해주며 jwt 액세스 토큰을 활용할 것이라고 알려준다.
- 리소스 서버의 api는 모든 입력 파라미터를 id 에서 email로 바꿔야한다. 왜냐하면 서드 파티 어플리케이션에서는 절대로 해당 유저의 id를 알 수 없기 때문이다.
- 이제 application.properties에 인증 서버의 url을 남겨줌으로서 인증서버에서 액세스 토큰과 관련된 인증서를 다운받아 액세스 토큰을 검증할 수 있다.
- 인증서에는 키 값이 저장돼 있고 해당 키 값을 통해 서명 부분을 해석하여 토큰이 조작되지 않았는지도 리소스 서버에서 확인이 가능하다.
- 포스트맨에서 api 요청을 통해 Keycloak으로부터 액세스 토큰을 받을 것이다.
- Keycloak에서 액세스 토큰을 받기위한 엔드포인트는 http://example.com/realms/master/protocol/openid-connect/token 이와 같다 여기서 example.com을 localhost:8081로, master를 eazybankdev로 바꿔주면 된다.
- http://localhost:8180/realms/eazybankdev/protocol/openid-connect/token !(1)[포스트맨예시.png]
- 위와 같이 post 요청을 보내면 액세스 토큰을 받을 수 있다.
- 획득한 액세스 토큰을 복호화 시켜보면
"realm_access": {
"roles": [
"offline_access",
"uma_authorization",
"default-roles-eazybankdev"
]
},
- 다음과 같은 realm_access에 roles가 잇는 것을 볼 수 있다. 저 roles를 통해 역할을 추출할 수 있는것이다.
- 그런데 여기엔 문제가 있다. 바로 위의 역할은 우리가 설정한 역할과는 다르다는 것이다.
- 현재는 우리가 인증서버의 관리자이므로 keyCloak에서 역할을 전부 우리의 형식에 맞게 바꿔주면 된다.
- 그러나 페이스북이나 구글 등을 사용할 때는 해당 소셜에서 건네주는 역할을 사용하는 방식으로 하거나 우리가 설정한 역할을 따로 추가해주는 방식으로 해야할 것이다.
- 받은 액세스 토큰을 리소스 서버에 보내려면 Authoriztion 헤더를 만들어 값을 "Bearer" +" " + "액세스토큰" 형식으로 보내면 된다.
- 자바 스크립트 어플리케이션은 코드가 모두 공개되어있어 client secret을 보내는 것이 불가능하다.
- 현재 시나리오는 ui 어플리케이션이 자바 스크립트가 아니라는 가정하에 이뤄진다.
- 위에서 했던 것과 마찬가지로 Keycloak에 클라이언트를 새로 등록해준다. 이번 클라이언트는 authorization code grant flow를 사용한다.
- valid redirect URLs에 인증에 성공한 사용자를 되돌려줄 url을 적어준다.
- authorization code grant flow는 실사용자가 필요하기에 users 탭에 사용자 등록을 해준다.
- authorization_endpoint":"http://localhost:8180/realms/eazybankdev/protocol/openid-connect/auth
- 위의 url로 get요청을 보내면 인증 코드를 받을 수 있다. 다만 query params에 여러 정보를 포함해야한다.
- client_id
- response_type : code
- scope : openid
- redirect_uri : ~~~~
- state_value : csrf 토큰 값
- 받은 인증 코드 값을 바탕으로 post 요청을 보내서 액세스 토큰을 받아야한다.
- 해당 요청은 post 요청이기 때문에 body의 urlendoded를 활용하여 정보를 보내자.
- 넘겨야 할 정보는 다음과 같다
- client_id
- client_secret
- grant_type : authorization_code
- code : 받은 인증 코드
- redirect_uri : ~~~
- scope : openid
- 해당 정보를 보내면 액세스 토큰을 받을 수 있다.
- PKCE flow를 따르면 자바스크립트 기반의 ui에서도 authorization code grant flow를 활용할 수 있다.
- 이 플로우에서 엔드유저가 인증에 성공하고 원래 페이지로 redirect 될 때마다 클라이언트 어플리케이션에서는 code_varifier를 생성한다.
- 그리고 code_varifier로부터 code_challenge를 생성한다.
- code_challenge를 생성하기 위해 sha256 알고리즘을 무작위로 생성된 code_varifier를 활용하여 실행시킨다.
- 해시 string이 생성됐다면 해당 값을 클라이언트 어플리케이션은 base64-URL로 인코딩한다. 이렇게 base64-URL로 인코딩 된 값을 code_challenge라고 한다.
- 인증 코드를 받으려 하고 있으며 아직 액세스 토큰은 요청중이 아닐 때 code_challenge를 인증 서버에 보낸다.
- 그러면 인증 서버는 code_challenge를 저장소에 보관하고 클라이언트 어플리케이션에게는 code_challenge에 대응하는 인증코드를 보낸다.
- 이제 클라이언트 어플리케이션이 액세스 토큰을 받으려 할때 인증 서버에 code_varifier와 아까 받은 인증 코드를 둘 다 보낸다.
- 인증서버는 code_varifier를 sha256 알고리즘에 넣어 해시 값을 얻는다. 얻은 값을 이전에 저장해두었던 값과 비교하고, 인증 코드도 비교한다. 두 값이 모두 일치해야지만 액세스 토큰을 반환한다.
- 인증 코드를 탈취당했다 하더라도 클라이어늩 어플리케이션에만 저장된 code_varifier를 알진 못하기 때문에 보안을 강화할 수 있다.
- 사용자가 리소스 접근 요청을 한다.
- 클라이언트가 인증 서버 로그인 페이지로 보낼테니 로그인을 하라고 한다.
- 사용자를 인증서버로 보내는 것과 동시에 클라이언트는 code_challenge 값과 client id 값을 인증 서버에게 공유한다.
- 인증 서버는 저장소에 code challenge를 저장한다.
- 클라이언트가 인증 코드를 받았다면 인증코드와 code varifier를 인증 서버에보낸다.
- 인증 서버는 code verifier를 동일한 알고리즘으로 해싱하여 해시값을 얻은 후 code challenge와 비교한다. 두 값이 일치한다면 액세스 토큰을 발급해준다.
- 액세스 토큰을 리소스 서버에 보내서 리소스 서버로부터 자원을 받을 수 있다.
2,3단계에서 인증 서버에 보내야 하는 정보들
- client id
- redirect uri
- scope
- state
- response type
- code challenge
- code challenge method
5단계에서 인증 서버에 보내야 하는 정보들
- code
- client id, client secret(optional) - ui에 client secret을 저장하지 못하는 경우에도 사용가능하다.
- grant type
- redirect uri
- code verifier
- 초기에는 클라이언트 secret이 모두에게 보여지는 자바 스크립트 같은 ui를 위해 개발된 프로토콜 이지만 무작위 인증 코드 주입과 같은 공격을 방어하기 유용해 많은 기업에서 선호한다고 한다.
'spring > spring security' 카테고리의 다른 글
[Spring Security] Oauth 2.0 카카오 소셜 로그인 구현하기 2. (코드 구현) (0) | 2024.07.30 |
---|---|
[Spring Security] Oauth 2.0 카카오 소셜 로그인 구현하기 1. (소셜 로그인 흐름 확인하기) (0) | 2024.07.28 |
스프링 시큐리티 기본 개념공부 10. (OAUTH2 란?) (1) | 2024.07.18 |
스프링 시큐리티 기본 개념공부 9. (메소드 레벨 보안이란?) (0) | 2024.07.18 |
스프링 시큐리티 기본 개념공부 8. (JWT 토큰의 개념과 사용 방법) (2) | 2024.06.16 |