From 01108a17824b13577dc9835fe7df3e798710aef9 Mon Sep 17 00:00:00 2001 From: tanqidi <1729746565@qq.com> Date: Mon, 14 Jul 2025 17:38:58 +0800 Subject: [PATCH] update --- .../config/OAuth2LoginSecurityConfig.java | 225 ++++++------------ src/main/resources/application.properties | 20 +- 2 files changed, 80 insertions(+), 165 deletions(-) diff --git a/src/main/java/com/tanqidi/survey/config/OAuth2LoginSecurityConfig.java b/src/main/java/com/tanqidi/survey/config/OAuth2LoginSecurityConfig.java index 6f6ddba..df7d883 100644 --- a/src/main/java/com/tanqidi/survey/config/OAuth2LoginSecurityConfig.java +++ b/src/main/java/com/tanqidi/survey/config/OAuth2LoginSecurityConfig.java @@ -1,37 +1,20 @@ package com.tanqidi.survey.config; -import com.nimbusds.jose.util.JSONObjectUtils; -import jakarta.servlet.http.HttpServletRequest; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.convert.converter.Converter; -import org.springframework.http.ResponseEntity; -import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.core.Authentication; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.OAuth2AccessToken; -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler; -import org.springframework.security.web.authentication.logout.LogoutHandler; -import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter; -import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.web.cors.CorsConfiguration; import java.util.*; @@ -39,143 +22,85 @@ import java.util.*; @EnableWebSecurity @EnableMethodSecurity public class OAuth2LoginSecurityConfig { - private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Bean + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwt -> { + Collection authorities = new ArrayList<>(); + Object realmAccess = jwt.getClaim("realm_access"); + if (realmAccess instanceof Map map) { + Object roles = map.get("roles"); + if (roles instanceof Collection roleList) { + for (Object role : roleList) { +// System.out.println("ROLE_" + role); + authorities.add(new SimpleGrantedAuthority("ROLE_" + role)); + } + } + } + return authorities; + }); + return converter; + } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http // - .sessionManagement( - // https://docs.spring.io/spring-security/reference/servlet/authentication/session-management.html - // https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.application-properties.server - Customizer.withDefaults()) - .authorizeHttpRequests(authorizeHttpRequests -> authorizeHttpRequests // - .requestMatchers("/login", "/logout").permitAll() // + http + .cors(cors -> cors.configurationSource(request -> { + CorsConfiguration configuration = new CorsConfiguration(); +// String originsProp = env.getProperty("cors.allowed-origins"); + // 你的前端调试地址,防止出现跨域。可以稍微修改下代码做成从application.properties中读取 + String originsProp = "http://localhost:3000,http://localhost:3002,"; + if (!originsProp.isBlank()) { + configuration.setAllowedOrigins(Arrays.asList(originsProp.split(","))); + } + // 只允许常用的安全方法,满足前端常见请求 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + // 允许所有请求头,便于前端携带token等自定义头 + configuration.setAllowedHeaders(Arrays.asList("*")); + // 允许携带cookie和认证信息,适合需要登录态的前后端分离场景 + configuration.setAllowCredentials(true); + return configuration; + })) + .sessionManagement(sessionManagement -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth +// .requestMatchers("/login", "/logout", "/error").permitAll() .requestMatchers("/api/admin/**").hasRole("test-service-admin") // - .requestMatchers("/api/user/**").hasRole("test-service-user") // - .anyRequest().authenticated()) // - .oauth2Login(oauth2 -> oauth2 // - .userInfoEndpoint(userInfo -> userInfo // - .oidcUserService(this.oidcUserService()))) - .csrf(csrf -> csrf - .ignoringRequestMatchers("/logout", "/api")) - .logout(logout -> logout - .addLogoutHandler(new KeycloakLogoutHandler(restTemplateBuilder.build())) - .addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL)))) + .requestMatchers("/api/user/**").hasAnyRole("test-service-user", "test-service-admin") // + .anyRequest().authenticated()) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) // 设置JWT权限转换器,支持自定义角色映射 + .authenticationEntryPoint(unauthorizedEntryPoint()) // 指定自定义401响应,token过期/无效也会返回JSON + ) .exceptionHandling(handling -> handling - .accessDeniedHandler((request, response, exception) -> { - response.setContentType("application/json;charset=UTF-8"); - response.setStatus(HttpServletResponse.SC_FORBIDDEN); - response.getWriter().write("{\"code\":403,\"message\":\"没有权限访问该资源\",\"data\":null}"); - })); + .accessDeniedHandler(accessDeniedHandler())); // 全局无权限处理 return http.build(); } - @Autowired - private RestTemplateBuilder restTemplateBuilder; - - // OpenID Connect 1.0 Logout Does Not work for Angular app, since redirect will violate CORS (Reason: CORS header - // ‘Access-Control-Allow-Origin’ missing) and OidcClientInitiatedLogoutSuccessHandler ignores Spring Security CORS - // https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html#oauth2login-advanced-oidc-logout - // https://github.com/simasch/vaadin-keycloak/blob/main/src/main/java/ch/martinelli/demo/keycloak/security/KeycloakLogoutHandler.java - private class KeycloakLogoutHandler implements LogoutHandler { - private final RestTemplate restTemplate; - - public KeycloakLogoutHandler(RestTemplate restTemplate) { - this.restTemplate = restTemplate; - } - - @Override - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication auth) { - logoutFromKeycloak((OidcUser) auth.getPrincipal()); - } - - private void logoutFromKeycloak(OidcUser user) { - // https://access.redhat.com/documentation/en-us/red_hat_single_sign-on/7.6/html-single/securing_applications_and_services_guide/index#logout - String endSessionEndpoint = user.getIssuer() + "/protocol/openid-connect/logout"; - UriComponentsBuilder builder = UriComponentsBuilder // - .fromUriString(endSessionEndpoint) // - .queryParam("id_token_hint", user.getIdToken().getTokenValue()); - - ResponseEntity logoutResponse = restTemplate.getForEntity(builder.toUriString(), String.class); - if (logoutResponse.getStatusCode().is2xxSuccessful()) { - log.info("Successfully logged out from Keycloak"); - } else { - log.error("Could not propagate logout to Keycloak"); - } - } - } - - // https://docs.spring.io/spring-security/reference/servlet/oauth2/login/advanced.html#oauth2login-advanced-map-authorities-oauth2userservice - private OAuth2UserService oidcUserService() { - final OidcUserService delegate = new OidcUserService(); - - return (userRequest) -> { - // Delegate to the default implementation for loading a user - OidcUser oidcUser = delegate.loadUser(userRequest); - - OAuth2AccessToken accessToken = userRequest.getAccessToken(); - Collection mappedAuthorities = new HashSet<>(); - - // 1) Fetch the authority information from the protected resource using accessToken - // 2) Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities - try { - String[] chunks = accessToken.getTokenValue().split("\\."); - Base64.Decoder decoder = Base64.getUrlDecoder(); - String header = new String(decoder.decode(chunks[0])); - String payload = new String(decoder.decode(chunks[1])); - - Map claims = JSONObjectUtils.parse(payload); - mappedAuthorities = new KeycloakAuthoritiesConverter().convert(claims); - } catch (Exception e) { - log.error("Failed to map Authorities", e); - } - - // 3) Create a copy of oidcUser but use the mappedAuthorities instead - oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo(), - "preferred_username"); - - return oidcUser; + @Bean + public AuthenticationEntryPoint unauthorizedEntryPoint() { + return (request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + Map result = new HashMap<>(); + result.put("code", 401); + result.put("message", "您的访问令牌无效或已过期,请重新登录后再试。"); // 优雅提示 + result.put("data", null); + response.getWriter().write(new ObjectMapper().writeValueAsString(result)); // 返回自定义JSON格式 }; } - // Spring OAuth2 uses default Scopes Not Roles for Authorization - // org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter - private class KeycloakAuthoritiesConverter implements Converter> { - - @Override - public Collection convert(Jwt jwt) { - return convert(jwt.getClaims()); - } - - public Collection convert(Map claims) { - Collection grantedAuthorities = new ArrayList<>(); - for (String authority : getAuthorities(claims)) { - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + authority)); - } - return grantedAuthorities; - } - - private Collection getAuthorities(Map claims) { - Object realm_access = claims.get("realm_access"); - log.info("Retrieved realm_access {}", realm_access); - if (realm_access instanceof Map) { - Map map = castAuthoritiesToMap(realm_access); - Object roles = map.get("roles"); - if (roles instanceof Collection) { - return castAuthoritiesToCollection(roles); - } - } - return Collections.emptyList(); - } - - @SuppressWarnings("unchecked") - private Map castAuthoritiesToMap(Object authorities) { - return (Map) authorities; - } - - @SuppressWarnings("unchecked") - private Collection castAuthoritiesToCollection(Object authorities) { - return (Collection) authorities; - } + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return (request, response, accessDeniedException) -> { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + Map result = new HashMap<>(); + result.put("code", 403); + result.put("message", "权限不足,您无权访问该资源。"); // 简洁优雅提示 + result.put("data", null); + response.getWriter().write(new ObjectMapper().writeValueAsString(result)); + }; } } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d386d6a..60cbc85 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.application.name=survey-service +spring.application.name=test-service server.port=8080 ## logging @@ -6,22 +6,12 @@ logging.level.org.springframework.security=INFO logging.pattern.console=%d{dd-MM-yyyy HH:mm:ss} %magenta([%thread]) %highlight(%-5level) %logger.%M - %msg%n ## keycloak -# OAuth2 Log In Spring Boot 2.x Property Mappings -# https://docs.spring.io/spring-security/reference/servlet/oauth2/login/core.html#oauth2login-boot-property-mappings spring.security.oauth2.client.registration.keycloak.client-id=test-service -spring.security.oauth2.client.registration.keycloak.client-secret=e6LMQn67PmoaeXDxCvxiLIfmypjIIygi -#spring.security.oauth2.client.registration.keycloak.client-authentication-method = -# org.springframework.security.oauth2.core.AuthorizationGrantType +spring.security.oauth2.client.registration.keycloak.client-secret=????????????????????? spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code -#spring.security.oauth2.client.registration.keycloak.authorization-grant-type=urn:ietf:params:oauth:grant-type:jwt-bearer -#spring.security.oauth2.client.registration.keycloak.redirect-uri = spring.security.oauth2.client.registration.keycloak.scope=openid -#spring.security.oauth2.client.registration.keycloak.client-name = - -#spring.security.oauth2.client.provider.keycloak.authorization-uri -#spring.security.oauth2.client.provider.keycloak.token-uri -#spring.security.oauth2.client.provider.keycloak.jwk-set-uri +#spring.security.oauth2.client.provider.keycloak.issuer-uri=https://keycloak.tanqidi.com/realms/tanqidi spring.security.oauth2.client.provider.keycloak.issuer-uri=http://172.31.0.233:31364/realms/tanqidi -#spring.security.oauth2.client.provider.keycloak.user-info-uri -#spring.security.oauth2.client.provider.keycloak.user-info-authentication-method spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username +#spring.security.oauth2.resourceserver.jwt.jwk-set-uri=https://keycloak.tanqidi.com/realms/tanqidi/protocol/openid-connect/certs +spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://172.31.0.233:31364/realms/tanqidi/protocol/openid-connect/certs