我创建了一个 Spring Security 6 项目,在资源服务器中实现了不透明令牌配置。我已经将授权服务器部署到测试/生产环境中。 我的内省器是从下面的自定义类调用的:
@Slf4j
public class CustomAuthoritiesOpaqueTokenIntrospector implements OpaqueTokenIntrospector {
private OpaqueTokenIntrospector delegate;
public CustomAuthoritiesOpaqueTokenIntrospector(String oauthServerUrl, String clientId, String clientSecret) {
this.delegate = new NimbusOpaqueTokenIntrospector(oauthServerUrl, clientId, clientSecret);
}
public OAuth2AuthenticatedPrincipal introspect(String token) {
OAuth2AuthenticatedPrincipal principal = this.delegate.introspect(token);
return new DefaultOAuth2AuthenticatedPrincipal(
principal.getName(), principal.getAttributes(), extractAuthorities(principal));
}
private Collection<GrantedAuthority> extractAuthorities(OAuth2AuthenticatedPrincipal principal) {
List<String> scopes = principal.getAttribute("scope");
return scopes.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
我从本文档
复制的这是我的资源服务器配置:
@Configuration
@EnableWebSecurity
public class ResourceServerConfiguration {
@Value("${spring.profiles.active}")
private String activeProfile;
@Value("${root.url.osb.basic.auth}")
private String osbBasicAuth;
@Value("${auth.server.url}")
private String oauthServerUrl;
@Value("${auth.server.clientId}")
private String clientId;
@Value("${auth.server.clientsecret}")
private String clientSecret;
public static final String BEARER_PREFIX = "Bearer ";
public static final String HEADER_NAME = "Authorization";
private final UserService userService;
public ResourceServerConfiguration(UserService userService) {
this.userService = userService;
}
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.addFilterAfter(new OncePerRequestFilter() {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
// We don't want to allow access to a resource with no token so clear
// the security context in case it is actually an OAuth2Authentication
var authHeader = request.getHeader(HEADER_NAME);
if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, BEARER_PREFIX)) {
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
}, AbstractPreAuthenticatedProcessingFilter.class);
// http.csrf().disable();
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/", "/v1/open/**").permitAll());
http.authorizeHttpRequests(authorize -> authorize.requestMatchers("/pension/**").authenticated())
.httpBasic(Customizer.withDefaults());
if (StringUtils.equalsIgnoreCase(activeProfile, "dev")) {
http.authorizeHttpRequests(authorize ->
authorize.requestMatchers("/webjars/**", "/resources/**", "/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs", "index.html")
.permitAll());
}
http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2
.opaqueToken(Customizer.withDefaults())
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
String[] authParts = osbBasicAuth.split(":");
String username = authParts[0];
String password = authParts[1];
UserDetails userDetails = User.builder()
.username(username)
.password(new BCryptPasswordEncoder().encode(password)).roles()
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public OpaqueTokenIntrospector introspector() {
return new CustomAuthoritiesOpaqueTokenIntrospector(oauthServerUrl + "check_token", clientId, clientSecret);
}
}
在此过程中,通过链接 http://localhost:9696/oauth/token 从授权服务器获取令牌 这是单独调用的,在我的例子中是通过 Postman 调用的。获得令牌后,我通过将此令牌粘贴到授权标头来调用另一个服务。
例如,我调用此端点:http://localhost:9090/v1/lead-management/get-details/18410,此端点是私有的,仅使用令牌执行。 执行它后,发生的第一件事是通过调用 http://localhost:9696/oauth/http://localhost:9696/oauth/check_token 触发我的内省以验证我的令牌,然后它调用最终的端点。令牌验证成功,我获得了证明我的用户已通过身份验证所需的所有详细信息:此处
但是,最终端点会抛出 500 错误。在这里我不知道问题是什么,在我看来,校长没有正确映射或加载。
我已经处理这个烂摊子大约1个月了,但似乎仍然找不到解决方案。我希望你能帮助解决这个问题。谢谢!
有几件事需要改变。
正如 @j-asgarov 的评论所指出的,你不应该报告 500 并忽略日志。
弥补内省效率低下的唯一有效案例是,当用户“会话”(状态)被卸载到前端(单页面或移动应用程序)时,对用户“会话”(状态)保持一定的控制。但由于现在不鼓励这样做而支持OAuth2 BFF模式,因此没有充分的理由选择内省而不是 JWT 解码。
每个授权机制应该有一个
SecurityFilterChain
bean。在这里,您似乎希望某些请求通过 Basic
身份验证进行授权,而其他请求则通过 Bearer
身份验证进行授权。您需要不同的过滤器链(都带有 @Order
以及除最后一个之外的所有过滤器链,按顺序带有 SecurityMatcher
)。
您不需要特定的过滤器来禁止匿名请求。每个过滤器链中的
authenticated()
就足够了。
当您表达应禁止匿名请求的要求(甚至为此创建一个过滤器)时,
permitAll()
的意义何在?
除非消耗资源服务器的内容是在服务器端渲染的(Thymeleaf、JSF 等),否则请研究 OAuth2 BFF 模式。
将资源服务器从内省切换到 JWT 解码(如果尚未配置授权服务器以提供 JWT 访问令牌)。
删除您的自定义过滤器(该功能包含在 Spring 的请求授权中,您很可能需要匿名访问某些资源)。
为
Basic
和 Bearer
授权创建不同的安全过滤器链 bean。