나는 access token을 keycloak API 공식 문서에 있는 방법대로 얻어오는 것을 선택했다.
하지만 대부분의 블로그 글은 spring security랑 연동을 햇다.
그때는 내가 지식이 부족해서 왜 저렇게 하지.. 생각했는데
다 이유가 있었다.
이유인 즉슨, access token을 직접 나처럼 decode 해서 원하는정보를 json 파싱하는 방법은
별로인게.. 일단, MSA 기반에서는 각 서비스가 각각의 서버를 갖고있기 때문에
서버가 다르면 model도 session도 무용지물이다.
그래서 spring-httpSession에서 redis를 사용하는게 많았다.
근데 로그아웃 기능을 구현한다면 redis를 사용해야하지만
로그아웃 기능 빼고 로그인 기능만, redis 없이 토큰 이동이 가능한 경우는
바로 바로~ spring security랑 keycloak이랑 이어서, principal 객체에 토큰정보를 저장시키고,
keycloak Accesstoken을 jwtToken으로 형변환 시켜서
token 안에 있는 userinfo를 getUserAttributes()로 받는것이다.
한번 해보겟음!!!!
package com.dream.gatewayservice.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange().pathMatchers("/menu/**").permitAll()
.and().authorizeExchange().anyExchange().authenticated()
.and().oauth2Login() // to redirect to oauth2 login page.
.and().csrf().disable();
return http.build();
}
}
이렇게 일단 시큐리티 설정해두고
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: http://너님의 키클락사용할 pc url:8080/auth/realms/MSA
registration:
keycloak:
# provider: keycloak
client-id: spring-gateway-client
client-secret: 클라이언트 비번
authorization-grant-type: authorization_code
redirect-uri: '{baseUrl}/login/oauth2/code/keycloak'
keycloak:
realm: MSA
# public-client: false
resource: spring-gateway-client
auth-server-url: http://너님의 키클락사용할 pc url:8080/auth
이런것 추가해준다.
keycloak정보를 알아야하니깐~!
plugins {
id 'org.springframework.boot' version '2.6.3'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.dream'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.1")
set('keyCloakVersion', "16.1.1") //추가
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' //추가
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
//implementation 'org.keycloak:keycloak-spring-boot-starter'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
mavenBom "org.keycloak.bom:keycloak-adapter-bom:${keyCloakVersion}" //추가
}
}
tasks.named('test') {
useJUnitPlatform()
}
//추가 라고 써놓은 부분들을 모두 추가해준다. 그러면 keycloak이 연동이 된것임
package com.dream.manage.config;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class ManageConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and().oauth2ResourceServer()
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
.and().anonymous().disable();
}
private Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(new RealmRoleConverter());
return jwtConverter;
}
public class RealmRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
final Map<String, List<String>> realmAccess = (Map<String, List<String>>) jwt.getClaims()
.get("realm_access");
return realmAccess.get("roles").stream().map(roleName -> "ROLE_" + roleName)
// prefix required by Spring
// Security for roles.
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
}
}
}
먼저 config가 각 서비스별로 있어야한다.
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://keycloak 돌릴 pc url:8080/auth/realms/MSA/protocol/openid-connect/certs
# client:
# provider:
# keycloak:
# issuer-uri: http://keycloak 돌릴 pc url:8080/auth/realms/MSA
# registration:
# keycloak:
## provider: keycloak
# client-id: spring-gateway-client
# client-secret: keycloak client 비번
# authorization-grant-type: authorization_code
# redirect-uri: '{baseUrl}/login/oauth2/code/keycloak'
keycloak:
realm: MSA
bearer-only: true
ssl-required: external
resource: spring-gateway-client
auth-server-url: http://keycloak 돌릴 pc url:8080/auth
credentials:
secret: keycloak client 비번
이거 넣어주고
plugins {
id 'org.springframework.boot' version '2.6.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.dream'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "2021.0.1")
set('keycloakversion', "16.1.1") //추가
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' //추가
implementation 'org.springframework.boot:spring-boot-starter-security' //추가
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.keycloak:keycloak-spring-boot-starter' //추가
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
mavenBom "org.keycloak.bom:keycloak-adapter-bom:${keycloakversion}" //추가
}
}
tasks.named('test') {
useJUnitPlatform()
}
//추가 라고 한 부분들 모두 추가하삼
그다음 controller 에서 principal 객체에서 token 뽑아내고 token에서 필요한 정보를 view로 넘기는거
해보겠삼~
package com.dream.manage.controller;
import java.security.Principal;
import javax.annotation.security.RolesAllowed;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import com.dream.manage.dto.ManageDto;
import com.dream.manage.service.ManageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Controller
@RequiredArgsConstructor
@Slf4j
public class ManageController {
private ManageService service;
@RolesAllowed({ "ADMIN" }) //realm 의 role에 따라 mapping
@GetMapping("/register")
public String register(Principal principal, Model model) {
JwtAuthenticationToken token = (JwtAuthenticationToken) principal; //jwt로 형변환
log.info("toString : "+token.getTokenAttributes().toString());
model.addAttribute("list", token.getTokenAttributes());
return "register";
}
@RolesAllowed({ "ADMIN" })
@PostMapping("/register")
public String register(@ModelAttribute("dto") ManageDto dto) {
log.info("컨트롤러 시작" + dto.getDescription());
service.register(dto);
log.info("컨트롤러 끝");
return "/regist_success";
}
@PostMapping("/modify")
public String modify() {
return "asd";
}
@GetMapping("/delete")
public String delete() {
return "asd";
}
}
register.html에서는
<div th:text="${list.preferred_username}">
으로 꺼내서 쓰면 된다.
이렇게 다른 MS도 하면된다.
먼저 localhost:8000/manage/register에 user로 접근해보겠다.
port번호 8000번은 api gateway 포트번호이다.
@RolesAllowed({ "ADMIN" }) 에 의해 거부되었음
이번엔 admin으로 접근해보겠음
이렇게 user name이 나온것을 알수있다.