init
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package com.tanqidi.survey;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class SurveyServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(SurveyServiceApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,177 @@
|
||||
package com.tanqidi.survey.config;
|
||||
|
||||
import com.nimbusds.jose.util.JSONObjectUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
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.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.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 java.util.*;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
public class OAuth2LoginSecurityConfig {
|
||||
private final Logger log = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@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() //
|
||||
.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 //
|
||||
// https://docs.spring.io/spring-security/reference/servlet/exploits/csrf.html#csrf-token-repository-cookie
|
||||
.ignoringRequestMatchers("/logout", "/api"))
|
||||
.logout(logout -> logout //
|
||||
.addLogoutHandler(new KeycloakLogoutHandler(restTemplateBuilder.build())) //
|
||||
// https://docs.spring.io/spring-security/reference/servlet/authentication/logout.html#clear-all-site-data
|
||||
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(Directive.ALL))));
|
||||
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<String> 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<OidcUserRequest, OidcUser> 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<GrantedAuthority> 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<String, Object> 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;
|
||||
};
|
||||
}
|
||||
|
||||
// Spring OAuth2 uses default Scopes Not Roles for Authorization
|
||||
// org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter
|
||||
private class KeycloakAuthoritiesConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
|
||||
|
||||
@Override
|
||||
public Collection<GrantedAuthority> convert(Jwt jwt) {
|
||||
return convert(jwt.getClaims());
|
||||
}
|
||||
|
||||
public Collection<GrantedAuthority> convert(Map<String, Object> claims) {
|
||||
Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
|
||||
for (String authority : getAuthorities(claims)) {
|
||||
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + authority));
|
||||
}
|
||||
return grantedAuthorities;
|
||||
}
|
||||
|
||||
private Collection<String> getAuthorities(Map<String, Object> claims) {
|
||||
Object realm_access = claims.get("realm_access");
|
||||
log.info("Retrieved realm_access {}", realm_access);
|
||||
if (realm_access instanceof Map) {
|
||||
Map<String, Object> map = castAuthoritiesToMap(realm_access);
|
||||
Object roles = map.get("roles");
|
||||
if (roles instanceof Collection) {
|
||||
return castAuthoritiesToCollection(roles);
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String, Object> castAuthoritiesToMap(Object authorities) {
|
||||
return (Map<String, Object>) authorities;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Collection<String> castAuthoritiesToCollection(Object authorities) {
|
||||
return (Collection<String>) authorities;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
package com.tanqidi.survey.controller;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin")
|
||||
public class AdminController {
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public ResponseEntity<String> getAdminDashboard() {
|
||||
return ResponseEntity.ok("这是管理员仪表盘接口");
|
||||
}
|
||||
|
||||
@GetMapping("/users")
|
||||
public ResponseEntity<String> getAllUsers() {
|
||||
return ResponseEntity.ok("这是获取所有用户的接口");
|
||||
}
|
||||
|
||||
@GetMapping("/system")
|
||||
public ResponseEntity<String> getSystemInfo() {
|
||||
return ResponseEntity.ok("这是系统信息接口");
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
package com.tanqidi.survey.controller;
|
||||
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* com.edw.controller.IndexController
|
||||
* </pre>
|
||||
*
|
||||
* @author Muhammad Edwin < edwin at redhat dot com >
|
||||
* 21 Mar 2023 20:09
|
||||
*/
|
||||
@RestController
|
||||
public class IndexController {
|
||||
|
||||
@GetMapping(path = "/")
|
||||
public Map<String, Object> index() {
|
||||
OAuth2User user = (OAuth2User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
return new HashMap<String, Object>() {{
|
||||
put("name", user.getAttribute("name"));
|
||||
put("email", user.getAttribute("email"));
|
||||
put("allAttributes", user.getAttributes()); // 👈 打印所有属性
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping(path = "/unauthenticated")
|
||||
public HashMap unauthenticatedRequests() {
|
||||
return new HashMap(){{
|
||||
put("this is", "unauthenticated endpoint");
|
||||
}};
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
package com.tanqidi.survey.controller;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/public")
|
||||
public class PublicController {
|
||||
|
||||
@GetMapping("/info")
|
||||
public ResponseEntity<String> getPublicInfo() {
|
||||
return ResponseEntity.ok("这是一个公开接口,任何人都可以访问");
|
||||
}
|
||||
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<String> healthCheck() {
|
||||
return ResponseEntity.ok("服务正常运行中");
|
||||
}
|
||||
}
|
@@ -0,0 +1,36 @@
|
||||
package com.tanqidi.survey.controller;
|
||||
|
||||
import com.tanqidi.survey.dto.UserInfoDTO;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
public class UserController {
|
||||
|
||||
@GetMapping("/profile")
|
||||
public ResponseEntity<String> getUserProfile() {
|
||||
return ResponseEntity.ok("这是用户个人资料接口");
|
||||
}
|
||||
|
||||
@GetMapping("/dashboard")
|
||||
public ResponseEntity<String> getUserDashboard() {
|
||||
return ResponseEntity.ok("这是用户仪表盘接口");
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<UserInfoDTO> getCurrentUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
UserInfoDTO userInfo = new UserInfoDTO();
|
||||
userInfo.setUsername(authentication.getName());
|
||||
userInfo.setRoles(authentication.getAuthorities().stream()
|
||||
.map(authority -> authority.getAuthority())
|
||||
.toArray(String[]::new));
|
||||
|
||||
return ResponseEntity.ok(userInfo);
|
||||
}
|
||||
}
|
40
src/main/java/com/tanqidi/survey/dto/UserInfoDTO.java
Normal file
40
src/main/java/com/tanqidi/survey/dto/UserInfoDTO.java
Normal file
@@ -0,0 +1,40 @@
|
||||
package com.tanqidi.survey.dto;
|
||||
|
||||
public class UserInfoDTO {
|
||||
private String username;
|
||||
private String email;
|
||||
private String[] roles;
|
||||
private String accessToken;
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String[] getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(String[] roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public void setAccessToken(String accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
}
|
27
src/main/resources/application.properties
Normal file
27
src/main/resources/application.properties
Normal file
@@ -0,0 +1,27 @@
|
||||
spring.application.name=survey-service
|
||||
server.port=8080
|
||||
|
||||
## logging
|
||||
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.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=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
|
@@ -0,0 +1,13 @@
|
||||
package com.tanqidi.survey;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class SurveyServiceApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user