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
相关产品推荐
相关产品推荐

