diff --git a/pom.xml b/pom.xml
index 1ba2949..30d2c86 100644
--- a/pom.xml
+++ b/pom.xml
@@ -108,6 +108,24 @@
springdoc-openapi-starter-webmvc-api
2.6.0
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-oauth2-resource-server
+
+
+ org.springframework.security
+ spring-security-test
+ test
+
+
+ org.projectlombok
+ lombok
+ provided
+
diff --git a/src/main/java/com/example/demo/config/SecurityConfig.java b/src/main/java/com/example/demo/config/SecurityConfig.java
new file mode 100644
index 0000000..64836cb
--- /dev/null
+++ b/src/main/java/com/example/demo/config/SecurityConfig.java
@@ -0,0 +1,189 @@
+package com.example.demo.config;
+
+import com.example.demo.exception.CustomAuthenticationEntryPoint;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+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.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+import org.springframework.security.web.SecurityFilterChain;
+
+import java.util.*;
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfig {
+ @Getter
+ @Setter
+ private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
+
+ @Autowired
+ public SecurityConfig(CustomAuthenticationEntryPoint customAuthenticationEntryPoint) {
+ this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
+ }
+
+
+ private static final String[] WHITE_LIST = {"/swagger-ui/**", "/v3/api-docs/**", "/swagger/**"};
+
+// @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
+// String issuerUri;
+
+ @Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
+ private String jwkSetUri;
+
+ String jwtTokenType = "id_token";
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+ http.exceptionHandling(
+ exception -> exception.authenticationEntryPoint(customAuthenticationEntryPoint))
+ .authorizeHttpRequests(authorizeRequests ->
+ authorizeRequests.requestMatchers(WHITE_LIST).permitAll().anyRequest().authenticated()
+ )
+ .sessionManagement(sessionManagement ->
+ sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ )
+ .csrf(AbstractHttpConfigurer::disable)
+ .cors(AbstractHttpConfigurer::disable)
+ .oauth2ResourceServer(oauth2ResourceServerConfigurer ->
+ oauth2ResourceServerConfigurer.jwt(jwtConfigurer -> {
+ jwtConfigurer.decoder(customJwtDecoder());
+ jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter());
+ })
+ );
+ ;
+
+ return http.build();
+ }
+
+ @Bean
+ public JwtDecoder customJwtDecoder() {
+ // 使用NimbusJwtDecoder从JWKS URL读取密钥
+ return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).jwtProcessorCustomizer(jwtProcessor -> {
+ jwtProcessor.setJWETypeVerifier((header, context) -> {
+ if (!header.getType().equals(jwtTokenType)) {
+ throw new AuthenticationServiceException("Invalid token type: " + header.getType());
+ }
+ });
+
+ jwtProcessor.setJWSTypeVerifier((joseObjectType, securityContext) -> {
+ if (!joseObjectType.getType().equals(jwtTokenType)) {
+ throw new AuthenticationServiceException("Invalid token type: " + joseObjectType);
+ }
+ });
+ }).build();
+ }
+
+ @Bean
+ public JwtAuthenticationConverter jwtAuthenticationConverter() {
+ JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
+ // 提取权限
+ converter.setJwtGrantedAuthoritiesConverter(this::extractAuthoritiesFromJwt);
+ return converter;
+ }
+
+ private Collection extractAuthoritiesFromJwt(Jwt jwt) {
+ // 从JWT的claims中提取角色和权限信息
+ Map claims = jwt.getClaims();
+ List authorities = new ArrayList<>();
+
+ // 提取角色
+ if (claims.containsKey("roles") && claims.get("roles") instanceof List) {
+ List roles = (List) claims.get("roles");
+ authorities.addAll(roles.stream()
+ .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
+ .toList());
+ }
+
+ // 提取权限
+ if (claims.containsKey("permissions") && claims.get("permissions") instanceof List) {
+ List permissions = (List) claims.get("permissions");
+ authorities.addAll(permissions.stream()
+ .map(SimpleGrantedAuthority::new)
+ .toList());
+ }
+
+ return authorities;
+ }
+
+
+//
+// @Bean
+// public JwtDecoder customJwtDecoder() {
+// return token -> {
+// try {
+// // 使用Nimbus库解析JWT
+// JWT jwt = JWTParser.parse(token);
+// String typ = jwt.getHeader().getType().toString();
+// // 检查JWT的typ是否为id_token
+// if (!typ.equals(jwtTokenType)) {
+// throw new AuthenticationServiceException("Invalid token type: " + typ);
+// }
+//
+// JWTClaimsSet claims = jwt.getJWTClaimsSet();
+//
+//// Date exp = claims.getExpirationTime();
+//// Date now = new Date();
+// // 检测是否过期
+// if (claims.getExpirationTime().before(new Date())) {
+// throw new AuthenticationServiceException("Token has expired");
+// }
+//
+// // 将JWT转换为Spring Security的Jwt对象
+// return new Jwt(token, jwt.getJWTClaimsSet().getIssueTime().toInstant(), jwt.getJWTClaimsSet().getExpirationTime().toInstant(), jwt.getHeader().toJSONObject(), jwt.getJWTClaimsSet().toJSONObject());
+// } catch (AuthenticationServiceException | ParseException e) {
+// // 401 了
+// throw new AuthenticationServiceException(e.getMessage());
+// }
+// };
+//
+//
+// }
+//
+// @Bean
+// public JwtAuthenticationConverter jwtAuthenticationConverter() {
+// JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
+// // 提取权限
+// converter.setJwtGrantedAuthoritiesConverter(this::extractAuthoritiesFromJwt);
+//
+// return converter;
+// }
+//
+//
+// private Collection extractAuthoritiesFromJwt(Jwt jwt) {
+// // 从JWT的claims中提取角色和权限信息
+// Map claims = jwt.getClaims();
+// List authorities = new ArrayList<>();
+//
+// // 提取角色
+// if (claims.containsKey("roles") && claims.get("roles") instanceof List) {
+// List roles = (List) claims.get("roles");
+// authorities.addAll(roles.stream()
+// .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
+// .toList());
+// }
+//
+// // 提取权限
+// if (claims.containsKey("permissions") && claims.get("permissions") instanceof List) {
+// List permissions = (List) claims.get("permissions");
+// authorities.addAll(permissions.stream()
+// .map(SimpleGrantedAuthority::new)
+// .toList());
+// }
+//
+// return authorities;
+// }
+
+}
diff --git a/src/main/java/com/example/demo/exception/CustomAuthenticationEntryPoint.java b/src/main/java/com/example/demo/exception/CustomAuthenticationEntryPoint.java
new file mode 100644
index 0000000..2fd693a
--- /dev/null
+++ b/src/main/java/com/example/demo/exception/CustomAuthenticationEntryPoint.java
@@ -0,0 +1,21 @@
+package com.example.demo.exception;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
+ @Override
+ public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
+ response.setStatus(HttpStatus.UNAUTHORIZED.value());
+ response.setHeader(HttpHeaders.WWW_AUTHENTICATE, "Bearer error=\"invalid_token\", error_description=\"" + authException.getMessage() + "\"");
+ response.getWriter().write("Unauthorized: " + authException.getMessage());
+ }
+}
diff --git a/src/main/java/com/example/demo/exception/GlobalHttpExceptionHandlerAdvice.java b/src/main/java/com/example/demo/exception/GlobalHttpExceptionHandlerAdvice.java
index 45feaa9..b31cba9 100644
--- a/src/main/java/com/example/demo/exception/GlobalHttpExceptionHandlerAdvice.java
+++ b/src/main/java/com/example/demo/exception/GlobalHttpExceptionHandlerAdvice.java
@@ -34,7 +34,7 @@ public class GlobalHttpExceptionHandlerAdvice {
}
// get type of e
- logger.error("{}, Error: {}", e.getClass().getName(), e.getMessage());
+ logger.error("Server Error, {}, Error: {}", e.getClass().getName(), e.getMessage());
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
diff --git a/src/main/resources/.env.example b/src/main/resources/.env.example
index 7e8efab..4bb483f 100644
--- a/src/main/resources/.env.example
+++ b/src/main/resources/.env.example
@@ -1,3 +1,5 @@
SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC
SPRING_DATASOURCE_USERNAME=root
-SPRING_DATASOURCE_PASSWORD=Qwerty123...
\ No newline at end of file
+SPRING_DATASOURCE_PASSWORD=Qwerty123...
+JWK_URL=https://auth.leaflow.cn/.well-known/jwks
+#JWK_ISSUER_URL=https://auth.leaflow.cn
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
deleted file mode 100644
index 49b93bc..0000000
--- a/src/main/resources/application.properties
+++ /dev/null
@@ -1,25 +0,0 @@
-spring.application.name=demo
-
-spring.jpa.hibernate.ddl-auto=none
-
-spring.datasource.url=${SPRING_DATASOURCE_URL}
-spring.datasource.username=${SPRING_DATASOURCE_USERNAME}
-spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}
-
-spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
-spring.jpa.properties.hibernate.format_sql=true
-spring.jpa.show-sql=true
-
-#spring.shell.script.enabled=true
-#spring.shell.interactive.enabled=true
-
-server.port=8088
-
-# 阻止启动时执行 flyway
-spring.flyway.baseline-on-migrate=false
-spring.flyway.locations=classpath:migrations
-
-springdoc.api-docs.enabled=true
-springdoc.api-docs.path=/v3/api-docs
-springdoc.swagger-ui.enabled=true
-springdoc.swagger-ui.path=/swagger
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 0000000..4c06cfa
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,41 @@
+logging:
+ level:
+ org.springframework.web.client.RestTemplate: DEBUG
+spring:
+ application:
+ name: demo
+ jpa:
+ hibernate:
+ ddl-auto: none
+ properties:
+ hibernate:
+ format_sql: true
+ datasource:
+ url: ${SPRING_DATASOURCE_URL}
+ username: ${SPRING_DATASOURCE_USERNAME}
+ password: ${SPRING_DATASOURCE_PASSWORD}
+ driver-class-name: com.mysql.cj.jdbc.Driver
+ flyway:
+ baseline-on-migrate: false # 阻止启动时执行 flyway
+ locations: classpath:migrations
+ doc:
+ api-docs:
+ enabled: true
+ path: /v3/api-docs
+ swagger-ui:
+ enabled: true
+ path: /swagger
+ shell:
+ interactive:
+ enabled: false
+ security:
+ oauth2:
+ resourceserver:
+ jwt:
+ jwk-set-uri: ${JWK_URL}
+# issuer-uri: ${JWK_ISSUER_URL}
+ jackson:
+ time-zone: PRC
+server:
+ port: 8088
+