Keycloak spring 예제 연동
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 에러가 나오는 것을 확인할 수 있다.