add jwk and jwts support
This commit is contained in:
parent
90f6e8d05a
commit
d5dc891d30
18
pom.xml
18
pom.xml
@ -108,6 +108,24 @@
|
|||||||
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
|
||||||
<version>2.6.0</version>
|
<version>2.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
189
src/main/java/com/example/demo/config/SecurityConfig.java
Normal file
189
src/main/java/com/example/demo/config/SecurityConfig.java
Normal file
@ -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<GrantedAuthority> extractAuthoritiesFromJwt(Jwt jwt) {
|
||||||
|
// 从JWT的claims中提取角色和权限信息
|
||||||
|
Map<String, Object> claims = jwt.getClaims();
|
||||||
|
List<GrantedAuthority> authorities = new ArrayList<>();
|
||||||
|
|
||||||
|
// 提取角色
|
||||||
|
if (claims.containsKey("roles") && claims.get("roles") instanceof List) {
|
||||||
|
List<String> roles = (List<String>) claims.get("roles");
|
||||||
|
authorities.addAll(roles.stream()
|
||||||
|
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取权限
|
||||||
|
if (claims.containsKey("permissions") && claims.get("permissions") instanceof List) {
|
||||||
|
List<String> permissions = (List<String>) 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<GrantedAuthority> extractAuthoritiesFromJwt(Jwt jwt) {
|
||||||
|
// // 从JWT的claims中提取角色和权限信息
|
||||||
|
// Map<String, Object> claims = jwt.getClaims();
|
||||||
|
// List<GrantedAuthority> authorities = new ArrayList<>();
|
||||||
|
//
|
||||||
|
// // 提取角色
|
||||||
|
// if (claims.containsKey("roles") && claims.get("roles") instanceof List) {
|
||||||
|
// List<String> roles = (List<String>) claims.get("roles");
|
||||||
|
// authorities.addAll(roles.stream()
|
||||||
|
// .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||||
|
// .toList());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // 提取权限
|
||||||
|
// if (claims.containsKey("permissions") && claims.get("permissions") instanceof List) {
|
||||||
|
// List<String> permissions = (List<String>) claims.get("permissions");
|
||||||
|
// authorities.addAll(permissions.stream()
|
||||||
|
// .map(SimpleGrantedAuthority::new)
|
||||||
|
// .toList());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return authorities;
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
@ -34,7 +34,7 @@ public class GlobalHttpExceptionHandlerAdvice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get type of e
|
// 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());
|
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC
|
SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC
|
||||||
SPRING_DATASOURCE_USERNAME=root
|
SPRING_DATASOURCE_USERNAME=root
|
||||||
SPRING_DATASOURCE_PASSWORD=Qwerty123...
|
SPRING_DATASOURCE_PASSWORD=Qwerty123...
|
||||||
|
JWK_URL=https://auth.leaflow.cn/.well-known/jwks
|
||||||
|
#JWK_ISSUER_URL=https://auth.leaflow.cn
|
@ -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
|
|
41
src/main/resources/application.yml
Normal file
41
src/main/resources/application.yml
Normal file
@ -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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user