ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Keycloak spring 예제 연동
    Tutorials & Tips/KeyCloak Server 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 에러가 나오는 것을 확인할 수 있다.

     

     

     

     

     

     

     

    댓글

Designed by Tistory.