我的应用程序有一个自定义身份验证机制。我有
/api/**
端点的令牌身份验证以及 /manager/**
和 /viewer/**
的登录表单。我有 @RestController
api 和 @Contoller
网页。当我访问页面时出现问题 - 静态内容未加载并且请求以 net::ERR_ABORTED 401 (Unauthorized)
响应。对页面的请求 (/viewer/home
) 也以 401 响应,但具有实际的 html 正文,然后将其呈现并加载 dtos。值得一提的是,在浏览器中调用http://localhost:8080/css/operator.css
会返回一个没有错误的css文件。
@GetMapping("/viewer/home")
public String operatorHome(Model model) {
// logic...
model.addAttribute("files", dtos);
return "operator-home";
}
安全配置为:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Autowired
private AuthenticationService authService;
@Bean
@Order(1)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/manager/**").hasAuthority(Role.MANAGER.name())
.requestMatchers("/viewer/**").hasAuthority(Role.OPERATOR.name())
.requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().permitAll()
)
.formLogin((form) -> form.successHandler(new CustomAuthenticationSuccessHandler()))
.logout(LogoutConfigurer::permitAll)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/**").hasAuthority(Role.USER.name())
.anyRequest().permitAll()
)
.addFilterBefore(new TokenAuthenticationFilter(authService), UsernamePasswordAuthenticationFilter.class)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults())
;
return http.build();
}
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(authService);
}
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}
即使我注释掉第二个过滤器链(api)并保留
anyRequest().permitAll()
,问题仍然存在。我在 Postman 中测试了 REST API,安全性工作得很好。
HTML:
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous"> <!-- this is fine -->
<link rel="stylesheet" type="text/css" th:href="@{/css/operator.css}"> <!-- this is not loaded -->
检索 HTML 页面的日志:
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.security.web.FilterChainProxy : Securing GET /viewer/home
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=test_operator, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[OPERATOR]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=9E3858A4DC3CD548374F4874C21539D6], Granted Authorities=[OPERATOR]]]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.security.web.FilterChainProxy : Secured GET /viewer/home
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet : GET "/viewer/home", parameters={}
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.lavkatech.audiorecognition.controller.WebController#operatorHome(Model)
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] org.hibernate.SQL : select ao1_0.id,ao1_0.audio_len,ao1_0.checked_by_id,ao1_0.checked_on,ao1_0.file_loc,ao1_0.file_name,ao1_0.file_size,ao1_0.file_text,ao1_0.is_checked,ao1_0.op_end_time,ao1_0.op_start_time,ao1_0.requested_by_value from files ao1_0
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.w.s.v.ContentNegotiatingViewResolver : Selected 'text/html' given [text/html, application/xhtml+xml, image/avif, image/webp, image/apng, application/xml;q=0.9, */*;q=0.8, application/signed-exchange;v=b3;q=0.7]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet : Completed 401 UNAUTHORIZED
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Securing GET /css/operator.css
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.security.web.FilterChainProxy : Secured GET /css/operator.css
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : GET "/css/operator.css", parameters={}
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] o.s.web.servlet.DispatcherServlet : Completed 401 UNAUTHORIZED
DEBUG 13628 --- [XXXXXXXXXXXX] [nio-8080-exec-4] w.c.HttpSessionSecurityContextRepository : Retrieved SecurityContextImpl [Authentication=UsernamePasswordAuthenticationToken [Principal=org.springframework.security.core.userdetails.User [Username=test_operator, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, CredentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[OPERATOR]], Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=9E3858A4DC3CD548374F4874C21539D6], Granted Authorities=[OPERATOR]]]
正如@dur 在评论中指出的那样,第二个过滤器从未达到。配置中还初始化了两个过滤器。
1:
@Bean
public TokenAuthenticationFilter tokenAuthenticationFilter() {
return new TokenAuthenticationFilter(authService);
}
2:
.addFilterBefore(new TokenAuthenticationFilter(authService), UsernamePasswordAuthenticationFilter.class)
因为过滤器暴露在 bean 中,所以它与 Dao 一起用于授权,Dao 在
webFilterChain
中指定为 formLogin
。为了处理它,我使用 securityMatcher
来区分负责的过滤器链。
http.securityMatcher( request ->
Optional.ofNullable(
request.getHeader(HttpHeaders.AUTHORIZATION))
.map(h -> h.startsWith("Bearer ")
).orElse(false)
);
还去掉了带有过滤器的bean并交换了顺序。 完整的安全配置是:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
@Autowired
private AuthenticationService authService;
@Bean
@Order(1)
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
http.securityMatcher( request ->
Optional.ofNullable(
request.getHeader(HttpHeaders.AUTHORIZATION))
.map(h -> h.startsWith("Bearer ")
).orElse(false)
);
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/**").hasAuthority(Role.USER.name())
)
.addFilterBefore(new TokenAuthenticationFilter(authService), UsernamePasswordAuthenticationFilter.class)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults())
;
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain webFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/manager/**").hasAuthority(Role.MANAGER.name())
.requestMatchers("/viewer/**").hasAuthority(Role.OPERATOR.name())
.requestMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**").permitAll()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form.successHandler(new CustomAuthenticationSuccessHandler()))
.logout(LogoutConfigurer::permitAll)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(10);
}
}