You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vaadin 23无状态认证:如何为JWT添加自定义属性?

实现方法:为Vaadin无状态认证的JWT令牌添加Keycloak会话ID

1. 自定义JWT令牌增强器

通过实现JwtTokenEnhancer接口,在生成JWT时注入Keycloak会话ID自定义声明:

import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoderContext;
import org.springframework.security.oauth2.jwt.JwtTokenEnhancer;

public class KeycloakSessionIdTokenEnhancer implements JwtTokenEnhancer {

    @Override
    public JwtClaimsSet enhance(JwtClaimsSet claims, JwtEncoderContext context) {
        // 从认证主体中提取预先存入的Keycloak会话ID
        String keycloakSessionId = (String) context.getPrincipal().getDetails();
        
        return JwtClaimsSet.from(claims)
                .claim("keycloak_session_id", keycloakSessionId)
                .build();
    }
}

2. 配置Vaadin无状态认证,注入自定义增强器

修改安全配置类,将自定义增强器添加到JWT生成器中:

import com.vaadin.flow.spring.security.VaadinWebSecurity;
import com.vaadin.flow.spring.security.authentication.JwtTokenGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;

@Configuration
public class SecurityConfig extends VaadinWebSecurity {

    private final RSAKey rsaKey;

    public SecurityConfig() {
        // 初始化RSA密钥对(实际项目建议从配置/密钥管理服务获取)
        this.rsaKey = generateRsaKey();
    }

    private RSAKey generateRsaKey() {
        // 实现RSA密钥生成逻辑,示例省略
        return null;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        // 启用无状态认证并绑定自定义JWT生成器
        setStatelessAuthentication(http, jwtTokenGenerator());
        // 配置OAuth2登录及自定义成功处理器
        http.oauth2Login(oauth2 -> oauth2.successHandler(new KeycloakVaadinAuthenticationSuccessHandler()));
    }

    @Bean
    public JwtTokenGenerator jwtTokenGenerator() {
        JwtEncoder encoder = new NimbusJwtEncoder(jwkSource());
        JwtTokenGenerator tokenGenerator = new JwtTokenGenerator(encoder);
        // 添加自定义令牌增强器
        tokenGenerator.addTokenEnhancer(new KeycloakSessionIdTokenEnhancer());
        return tokenGenerator;
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() {
        JWK jwk = rsaKey;
        JWKSet jwkSet = new JWKSet(jwk);
        return new ImmutableJWKSet<>(jwkSet);
    }
}

3. 在认证成功处理器中捕获并存储Keycloak会话ID

修改KeycloakVaadinAuthenticationSuccessHandler,从OAuth2认证令牌中提取Keycloak会话ID,存入认证主体的详情中:

import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import com.vaadin.flow.spring.security.VaadinSavedRequestAwareAuthenticationSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

public class KeycloakVaadinAuthenticationSuccessHandler extends VaadinSavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        if (authentication instanceof OAuth2AuthenticationToken oauthToken) {
            OAuth2User oauth2User = oauthToken.getPrincipal();
            // 从Keycloak返回的用户属性中获取会话ID(需确认Keycloak客户端配置中已映射该属性)
            String keycloakSessionId = oauth2User.getAttribute("session_id");
            // 将会话ID存入认证主体详情,供令牌增强器读取
            authentication.setDetails(keycloakSessionId);
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }
}

4. 登出时解析JWT并终止对应Keycloak会话

从当前认证的JWT中提取会话ID,调用Keycloak Admin API终止特定会话:

import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.client.RestTemplate;

public class LogoutService {

    private final String keycloakAdminUrl = "http://你的Keycloak地址/auth/admin/realms/你的领域";
    private final String adminToken = "你的Keycloak管理员令牌"; // 建议通过客户端凭证流动态获取,不要硬编码

    public void terminateCurrentKeycloakSession() {
        Jwt jwt = (Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String sessionId = jwt.getClaimAsString("keycloak_session_id");
        
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.delete(
                keycloakAdminUrl + "/sessions/" + sessionId,
                request -> request.getHeaders().setBearerAuth(adminToken)
        );
    }
}

注意事项

  • 需在Keycloak客户端配置中,将session_id添加为用户属性映射,确保认证时能返回该值。
  • 管理员令牌建议通过Keycloak的客户端凭证授权流动态获取,避免硬编码泄露。
  • 确保JWT签名密钥在应用重启前后一致,保证令牌能正常解析。

内容的提问来源于stack exchange,提问作者alexanoid

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 00:25:30