add jwk and jwts support

This commit is contained in:
ivamp 2024-11-17 06:16:16 +08:00
parent 90f6e8d05a
commit d5dc891d30
7 changed files with 273 additions and 27 deletions

18
pom.xml
View File

@ -108,6 +108,24 @@
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<version>2.6.0</version>
</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>

View 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;
// }
}

View File

@ -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());
}
}

View File

@ -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());

View File

@ -1,3 +1,5 @@
SPRING_DATASOURCE_URL=jdbc:mysql://localhost:3306/demo?serverTimezone=UTC
SPRING_DATASOURCE_USERNAME=root
SPRING_DATASOURCE_PASSWORD=Qwerty123...
JWK_URL=https://auth.leaflow.cn/.well-known/jwks
#JWK_ISSUER_URL=https://auth.leaflow.cn

View File

@ -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

View 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