Tutorials & Tips/KeyCloak Server

Keycloak spring 예제 연동

PHAROS IAM 2022. 12. 13. 15:34

Keycloak spring 예제 연동

키클락 공식문서를 보게되면 

 

Securing Applications and Services Guide

In order for an application or service to utilize Keycloak it has to register a client in Keycloak. An admin can do this through the admin console (or admin REST endpoints), but clients can also register themselves through the Keycloak client registration

www.keycloak.org

의존성 XML 요소 내에 Spring Boot로 Keycloak를 실행하려면 다음이 필요하다.

 pom.xml

의존성 XML 요소 다음에 Keycloak에 대한 dependencyManagement를 지정해야 한다.

keycloakConfig

package com.example.keycloak.config;

import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class KeycloakSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
	 
	@Override
	    protected void configure(HttpSecurity http) throws Exception {
	        super.configure(http);
	        http
	                .authorizeRequests()
	                .antMatchers("/test/permitAll").permitAll()
	                .antMatchers("/test/authenticated").authenticated()
	                .antMatchers("/test/admin").hasAnyRole("ADMIN")
	                .antMatchers("/test/user").hasAnyRole("USER")
	                
	                .anyRequest()
	                .permitAll();
	        http.csrf().disable();
	    }

		//roles 앞에 ROLE_ 와 같이 접두사를 붙이지 않도록 도와준다.
	    @Autowired
	    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
	        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
	        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
	        auth.authenticationProvider(keycloakAuthenticationProvider);
	    }

	    @Bean
	    @Override
	    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
	        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
	    }
}

 

package com.example.keycloak.config;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class KeycloakConfig {
	
	// keycloak.json 대신 SpringBoot yml 파일을 이용하도록 도와준다.
	@Bean
	public KeycloakSpringBootConfigResolver keycloakConfigResolver() {
		return new KeycloakSpringBootConfigResolver();
	}
}

application.yml

server:
  port: 8081
    
keycloak:
  realm: SpringBootKeycloak
  auth-server-url: http://localhost:8090
  ssl-required: external
  resource: login
  credentials:
    secret: 9ClLRc5SDKziV8I7KFAerchdeMe4tTJa
  use-resource-role-mappings: true
  bearer-only: true
  
logging:
  level:
    root: INFO
    com.example.keycloak: DEBUG

 realm : 필수요소. realm 이름

 auth-server-url : 필수요소. 키클락 서버의 기본 url

 ssl-required : 선택요소. 기본값은 external이며 외부 요청에 기본적으로 https가 필요함을 의미

                       'all', 'external', 'none'이 을 선택할 수 있다.

 resource : 필수요소. client 이름

 credential.secret : client secret 비밀번호

 user-resource-role-mappings : 선택요소. 기본값은 false이며 true로 설정한 경우 사용자에 대한  application role 매핑에                                                                       대한 토큰 내부를 찾는다.

bearer-only : 선택요소. 기본값은 false이며 서비스에 대해 true로 설정해야 한다. true가 된 경우 어댑터는 사용자 인증을                         시도하지 않고 전달자 토큰만 확인합니다.

 

 

TestController

package com.example.keycloak.controller;

import lombok.extern.slf4j.Slf4j;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {

private static final Logger log = LoggerFactory.getLogger(TestController.class);

    @RequestMapping(value = "/permitAll", method = RequestMethod.GET)
    public ResponseEntity<String> permitAll() {
        return ResponseEntity.ok("누구나 접근이 가능합니다.\n");
    }
    
    @RequestMapping(value = "/authenticated", method = RequestMethod.GET)
    public ResponseEntity<String> authenticated(@RequestHeader String Authorization) {
        log.debug(Authorization);
        log.info("Authorization : ", Authorization);
        return ResponseEntity.ok("로그인한 사람 누구나 가능합니다.\n");
    }

    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public ResponseEntity<String> user(@RequestHeader String Authorization) {
     log.debug(Authorization);
     log.info("Authorization : ", Authorization);
        return ResponseEntity.ok("user 가능합니다.\n");
    }

    
    @RequestMapping(value = "/admin", method = RequestMethod.GET)
    public ResponseEntity<String> admin(@RequestHeader String Authorization) {
        log.debug(Authorization);
        log.info("Authorization : ", Authorization);
        return ResponseEntity.ok("admin 가능합니다.\n");
    }

}

- /permitAll는 인증 필요없이 모두 접근가능하다.
- /authenticated 는 인증만 있다면 누구나 가능하다.
- /user, /admin은 각자의 토큰으로 인증해야 한다.

 

토큰확인

토큰확인 방법 1 : CMD 창에서 확인

// username = admin 일 때

curl -X POST "http://localhost:8090/realms/SpringBootKeycloak/protocol/openid-connect/token" ^
--header "Content-Type: application/x-www-form-urlencoded" ^
--data-urlencode "grant_type=password" ^
--data-urlencode "client_id=login" ^
--data-urlencode "username=springboot_admin" ^
--data-urlencode "password=1111" | jq

jwt에서 확인(https://jwt.io/)

 

// username = user 일 때

curl -X POST "http://localhost:8090/realms/SpringBootKeycloak/protocol/openid-connect/token" ^
--header "Content-Type: application/x-www-form-urlencoded" ^
--data-urlencode "grant_type=password" ^
--data-urlencode "client_id=login" ^
--data-urlencode "username=springboot_user" ^
--data-urlencode "password=1111" | jq

로그인 연동 확인

1. /permitAll

2. /authenticated

토큰이 없을 때 인증 문제로 접근이 불가능하다.

 

토큰을 같이 입력하면 접근이 가능하다.

 

3 /admin, /user

 

토큰확인 방법 2 : POSTMAN에서 확인

 

 

1. /permitAll

 

2. /authenticated

토큰이 없을때 인증 문제로 접근이 불가능하다.
토큰을 같이 입력하면 접근이 가능하다.

3 /admin, /user

 

 

 

 

 

 

예제2

Keycloak의 API로 액세스 토큰 생성

다음 URL로 POST 요청을 전송하여 Keycloak에서 액세스 토큰을 획득해야 한다.

요청할 때 x-www-form-urlencoded 형식의 본문이 있어야 한다.

client_id : <your_client_id>

client_secrey : <your_client_secret>

grant_type : password

username : <your_username>

passwrd : <your_password>

 

이에 대한 응답으로 access_token 및 refresh_token을 얻는다.

액세스 토큰은 Authorization 해더에 배치하여 Keycloak으로 보호되는 리소스에 대한 모든 요청에서 사용해야 한다.

headers : {

       'Authorization' : 'Bearer' + access_token

}

 

스프링 부트 애플리케이션 생성

키클락 공식문서를 보게되면 https://www.keycloak.org/docs/latest/securing_apps/index.html

 

Securing Applications and Services Guide

In order for an application or service to utilize Keycloak it has to register a client in Keycloak. An admin can do this through the admin console (or admin REST endpoints), but clients can also register themselves through the Keycloak client registration

www.keycloak.org

의존성 XML 요소 내에 Spring Boot로 Keycloak를 실행하려면 다음이 필요하다.

의존성 XML 요소 다음에 Keycloak에 대한 dependencyManagement를 지정해야 한다.

 

Thymeleaf 웹 페이지

3개의 페이지가 있다.

- external.html - 대중을 위한 외부 웹 페이지

- customers.html - user 역할을 가진 인증 된 사용자로만 액세스가 제한되는 내부 페이지

- layout.html - 외부를 향하는 페이지와 내부를 향하는 페이지 모두 사용되는 간단한 레이아웃

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:include="layout :: headerFragment">
</head>
<body>
<div class="container">
    <div class="jumbotron text-center">
        <h1>Customer Portal</h1>
    </div>
    <div>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam
            erat lectus, vehicula feugiat ultricies at, tempus sed ante. Cras
            arcu erat, lobortis vitae quam et, mollis pharetra odio. Nullam sit
            amet congue ipsum. Nunc dapibus odio ut ligula venenatis porta non
            id dui. Duis nec tempor tellus. Suspendisse id blandit ligula, sit
            amet varius mauris. Nulla eu eros pharetra, tristique dui quis,
            vehicula libero. Aenean a neque sit amet tellus porttitor rutrum nec
            at leo.</p>

        <h2>Existing Customers</h2>
        <div class="well">
            <b>Enter the intranet: </b><a th:href="@{/customers}">customers</a>
        </div>
    </div>
    <div id="pagefoot" th:include="layout :: footerFragment">Footer
    </div>
</div>
<!-- container -->

</body>
</html>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:include="layout :: headerFragment">
</head>
<body>
<div id="container">
    <h1>
        Hello, <span th:text="${username}">--name--</span>.
    </h1>
    <table class="table table-striped">
        <thead>
        <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Address</th>
            <th>Service Rendered</th>
        </tr>
        </thead>
        <tbody>
        <tr th:each="customer : ${customers}">
            <td th:text="${customer.id}">Text ...</td>
            <td th:text="${customer.name}">Text ...</td>
            <td th:text="${customer.address}">Text ...</td>
            <td th:text="${customer.serviceRendered}">Text...</td>
        </tr>
        </tbody>
    </table>
    <div id="pagefoot" th:include="layout :: footerFragment">Footer
    </div>
</div>
<!-- container -->
</body>
</html>
<head th:fragment="headerFragment" xmlns:th="http://www.w3.org/1999/xhtml">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Customer Portal</title>
    <link
            href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
            rel="stylesheet"
            integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
            crossorigin="anonymous"/>
    <link
            href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css"
            rel="stylesheet"/>
</head>

<div id="pagefoot" th:fragment="footerFragment">
    <p>Document last modified 2020/09/27.</p>
    <p>Copyright: Lorem Ipsum</p>
</div>
SecurityConfig.Class
package com.example.demo.config;

import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

@Configuration
@EnableWebSecurity
@ComponentScan(basePackageClasses = KeycloakSecurityComponents.class)
class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
    // Submits the KeycloakAuthenticationProvider to the AuthenticationManager
    // roles 앞에 ROLE_ 와 같이 접두사를 붙이지 않도록 도와준다.
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
        keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(new SimpleAuthorityMapper());
        auth.authenticationProvider(keycloakAuthenticationProvider);
    }


    // Specifies the session authentication strategy
    @Bean
    @Override
    protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
        return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        http.authorizeRequests()
            .antMatchers("/customers*")
            .hasRole("USER")
            .anyRequest()
            .permitAll();
    }
    

}

keycloak admin console에서

user id가 user인 유저에게 clinet_id가 login인 'ROLE_USER' 라는 Role mapping을 해주고

user id가 admin인 유저에게 clinet_id가 login인 'ROLE_ADMIN'이라는 Role mapping을 해주었다.

WebController.class
package com.example.demo.controller;

import java.security.Principal;

import org.keycloak.adapters.springsecurity.token.KeycloakAuthenticationToken;
import org.keycloak.representations.AccessToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import com.example.demo.repository.CustomerRepository;
import com.example.demo.vo.Customer;

@Controller
public class WebController {

    @Autowired
    private CustomerRepository customerDAO;

    @GetMapping(path = "/")
    public String index() {
        return "external";
    }

    @GetMapping(path = "/customers")
    public String customers(Principal principal, Model model) {
    	//KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) principal;
    	//AccessToken accessToken = token.getAccount().getKeycloakSecurityContext().getToken();
    	
    	addCustomers();
        Iterable<Customer> customers = customerDAO.findAll();
        model.addAttribute("customers", customers);
        model.addAttribute("username", principal.getName());
        return "customers";
    }

    // add customers for demonstration
    public void addCustomers() {

        Customer customer1 = new Customer();
        customer1.setAddress("1111 foo blvd");
        customer1.setName("Foo Industries");
        customer1.setServiceRendered("Important services");
        customerDAO.save(customer1);

        Customer customer2 = new Customer();
        customer2.setAddress("2222 bar street");
        customer2.setName("Bar LLP");
        customer2.setServiceRendered("Important services");
        customerDAO.save(customer2);

        Customer customer3 = new Customer();
        customer3.setAddress("33 main street");
        customer3.setName("Big LLC");
        customer3.setServiceRendered("Important services");
        customerDAO.save(customer3);
    }
    
    
    
    
    
}

 

application.properties
server.port=8081

keycloak.realm=SpringBootKeycloak
keycloak.auth-server-url=http://localhost:8090/
keycloak.resource=login
keycloak.credentials.secret=lbwNBEOOBMwZx9aPfUWTKslfLBdnWwh0
keycloak.ssl-required= external
keycloak.principal-attribute=preferred_username
keycloak.enabled=true
keycloak.use-resource-role-mappings=true
Spring Boot 애플리케이션 실행

http://localhost:8081

customers을 클릭해서 들어가면 Login창이 나온다.

 

위의 SecurityConfig.Class를 보게되면  권한을 "USER"만 가지고 있는 사람만 로그인이 가능하도록 구현하였다.

http.authorizeRequests()
            .antMatchers("/customers*")
            .hasRole("USER")
            .anyRequest()
            .permitAll();

ID : user  / PW : 1111

권한이 있는 user로 로그인을 하였을 때는 localhost:8081/customers로 잘 넘어가는것을 확인할 수 있다.

 

ID : ADMIN /  PW : 1111

권한이 없는 user로 로그인 하였을 때는 403 에러가 나오는 것을 확인할 수 있다.