はじめに
複数プロジェクトでマイクロサービスを利用するために、リクエストごとにKeycloakのレルムの書き換えを行ってみたが、レルムごとにユーザー管理するのは現実的ではないことに気付き、Keycloakで行っていた認証・認可の認可の部分を切り離すことにした。
試したこと
バックエンドのマイクロサービスをSpring Bootで実装していたので、SpringのSecurity Filter Chainに、新しい認可情報を付加するフィルターを追加してみた。
プロジェクトごとに電子メールアドレスをユーザーの一意識別子としてメンバーを登録すれば、KeycloakのJWTトークンの電子メールアドレスでユーザーの名寄せができるという理屈です。
同じユーザーが複数のプロジェクトに参加するユースケースでは、プロジェクトごとに認可情報を管理したいのでKeycloakから切り離した方が自由度が高くなります。
尚、新しいフィルターの追加場所は、SessionManagementFilter.classの後が良さそうです。
ソースコード
public class CustomKeycloakFilter implements Filter {
private final Logger logger = LoggerFactory.getLogger(getClass());
private final ProjectMemberService service;
@Override
public void init(javax.servlet.FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
logger.debug("######## CUSTOM KEYCLOAK FILTER INITIALIZED ########");
}
public CustomKeycloakFilter(ProjectMemberService service) {
this.service = service;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
logger.debug("######## CUSTOM KEYCLOAK FILTER ########");
// extract project id from the request header
String projectId = request.getHeader("project-id");
logger.debug("++++++++ PROJECT ID: {} ++++++++", projectId);
// extract Kycloak authentication token
Principal principal = request.getUserPrincipal();
if (principal != null && principal instanceof KeycloakAuthenticationToken) {
logger.debug("++++++++ AUTHENTICATED BY KEYCLOAK ++++++++", principal.getClass().getName());
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) principal;
KeycloakSecurityContext context = (KeycloakSecurityContext)token.getAccount().getKeycloakSecurityContext();
AccessToken accessToken = context.getToken();
// extract email from the access token as a unique user identifier.
String email = accessToken.getEmail();
logger.debug("++++++++ email: {} ++++++++", email);
if (projectId != null && email != null) {
// retrieve project user permissions from the project database by project id and user email
Collection<? extends GrantedAuthority> authorities = service.getAuthz(UUID.fromString(projectId), email);
List<String> authorityStringList = new ArrayList<>();
authorities.forEach(o -> authorityStringList.add(o.getAuthority()));
logger.debug("++++++++ AUTHOTITIES: {} ++++++++", authorityStringList);
// generates a new authentication token with reloaded user permissions
KeycloakAuthenticationToken newAuthenticationToken = new KeycloakAuthenticationToken(token.getAccount(), false, authorities);
logger.debug("++++++++ NEW AUTHENTICATION TOKEN: {} ++++++++", newAuthenticationToken.toString());
SecurityContext sc = SecurityContextHolder.getContext();
// replaces authentication token with the new one
sc.setAuthentication(newAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
まとめ
Keycloakの認証・認可の認可の部分を切り離すことにより、マルチプロジェクト(マルチテナント)に対応することができた。認可情報をプロジェクトごとに管理できるので、自由度の高いプロジェクト運用ができそうです。