【发布时间】:2022-01-04 11:00:08
【问题描述】:
我正在尝试使用 spring-mvc 和 spring-security 在 Spring Boot REST Web 服务上实现安全性(HTTP Basic Auth + JWT)。
为了实现这一点,我使用了带有两个 AuthenticationManager 的 SecurityContextRepository。
这是我的 SecurityContextRepository:
@Component
public class MySecurityContextRepository implements SecurityContextRepository {
private final JwtAuthenticationManager jwtAuthenticationManager;
private final HttpBasicAuthAuthorizationManager httpBasicAuthAuthorizationManager;
@Autowired
public MySecurityContextRepository(JwtAuthenticationManager JwtAuthenticationManager,
HttpBasicAuthAuthorizationManager httpBasicAuthAuthorizationManager) {
this.jwtAuthenticationManager = jwtAuthenticationManager;
this.httpBasicAuthAuthorizationManager = httpBasicAuthAuthorizationManager;
}
@Override
public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
HttpServletRequest request = requestResponseHolder.getRequest();
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader == null) {
return new SecurityContextImpl(null);
}
if (authHeader.startsWith("Bearer")) {
String authToken = authHeader.substring(7);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return new SecurityContextImpl(jwtAuthenticationManager.authenticate(auth));
} else if (authHeader.startsWith("Basic")) {
String authToken = authHeader.substring(6);
Authentication auth = new UsernamePasswordAuthenticationToken(authToken, authToken);
return new SecurityContextImpl(httpBasicAuthAuthorizationManager.authenticate(auth));
} else {
return null;
}
}
@Override
public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
// Nothing to save here
}
@Override
public boolean containsContext(HttpServletRequest request) {
return request.getHeader(HttpHeaders.AUTHORIZATION) != null
&& (request.getHeader(HttpHeaders.AUTHORIZATION).startsWith("Bearer") ||
request.getHeader(HttpHeaders.AUTHORIZATION).startsWith("Basic"));
}
}
这是我的 AuthenticationManager 之一
@Slf4j
@Component
public class JwtAuthenticationManager implements AuthenticationManager {
private final ObjectMapper mapper;
private final JwtParser parser;
public wtAuthenticationManager(ObjectMapper mapper, SecurityProperties security) {
this.mapper = mapper;
this.parser = Jwts.parser().setSigningKey(security.getSigningKey());
}
@Override
public Authentication authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
if (!validateToken(authToken)) {
return null;
}
Claims claims = getAllClaimsFromToken(authToken);
String json = new JSONObject(claims).toString();
try {
HttpAuthUser user = mapper.readValue(json, HttpAuthUser.class);
return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
} catch (JsonProcessingException ex) {
LOGGER.warn("Could not parse JWT", ex);
return null;
}
}
private Claims getAllClaimsFromToken(String token) {
return parser.parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
try {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
} catch (JwtException jwtException) {
LOGGER.trace("Expired JWT", jwtException);
return true;
}
}
private Date getExpirationDateFromToken(String token) {
return getAllClaimsFromToken(token).getExpiration();
}
private Boolean validateToken(String token) {
return !isTokenExpired(token);
}
}
这是我的 WebSecurityConfigurerAdapter:
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(SecurityProperties.class)
public class Security extends WebSecurityConfigurerAdapter {
private final SecurityProperties securityProperties;
private final SecurityProblemSupport problemSupport;
private final MySecurityContextRepository mySecurityContextRepository;
@Autowired
public Security(SecurityProperties securityProperties, SecurityProblemSupport problemSupport,
MySecurityContextRepository mySecurityContextRepository) {
this.securityProperties = securityProperties;
this.problemSupport = problemSupport;
this.mySecurityContextRepository = mySecurityContextRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/*
* Roles authorisation is handled in the controllers.
*
* Domain object authorisations are handled by @Rbac* annotations
*/
http.cors().disable()
.csrf().disable()
.httpBasic().disable()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().hasAnyRole("regular", "admin")
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.securityContext().securityContextRepository(securityContextRepository)
.and()
.exceptionHandling()
.authenticationEntryPoint(
(request, response, authException) -> {
response.addHeader(AuthHelper.HEADER_WWW_AUTH, AuthHelper.HEADER_JWT_PREFIX);
problemSupport.commence(request, response, authException);
})
.accessDeniedHandler(problemSupport);
}
}
当我启动我的网络服务并使用 curl 进行尝试时,一切都按预期工作:经过身份验证的请求就像魅力一样工作,而未经身份验证的请求被拒绝。
但我的问题是当我运行测试时:我的所有请求都被拒绝了。
这是失败的测试之一(与基本身份验证相同的问题):
@SpringBootTest(classes = TestConfig.class)
@ExtendWith(SpringExtension.class)
@AutoConfigureMockMvc
public class AuthenticationTest {
@Test
@WithAnonymousUser
public void testValidJwtToken() throws Exception {
MvcResult response = this.mvc.perform(post("/api/auth/token")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\": \"test@test.com\", \"password\": \"super_password\"}"))
.andExpect(status().isCreated())
.andReturn();
HttpAuth auth = mapper.readValue(response.getResponse().getContentAsString(),
HttpAuth.class);
String token = auth.getAccessToken();
this.mvc.perform(get("/api/users").header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}
java.lang.AssertionError: Status expected:<200> but was:<401> 失败
在测试的调试输出中,我可以看到一个标题“Authorization: Bearer foo.bar.foobar”,在调试模式下,我通过了我的JwtAuthenticationManager。
HttpBasicAuthAuthorizationManager 也会发生同样的事情
我在某处遗漏了什么吗?
【问题讨论】:
-
请阅读 spring 文档,了解如何正确实现 JWT 身份验证。您已经编写了很多不需要的自定义安全性,以及对 Spring Security 中已经存在的 JWT 的手动解析。 docs.spring.io/spring-security/reference/servlet/oauth2/… 我不知道你为什么选择使用安全库,然后坚持实现已经存在的功能。那么使用图书馆有什么意义呢?自定义安全性也是不好的做法。
-
我不使用 OAuth 2.0 并且与 Http Basic Auth 有同样的问题。无论如何,我读了这篇文章marcobehler.com/guides/spring-security 试图了解 Spring 安全
-
您正在使用 oauth,因为您正在阅读授权标头。你知道授权标头是 oauth 的一部分吗?
-
您发布的文章讨论了使用 formLogin 登录或使用用户名和密码进行基本登录。另一方面,您正在使用授权标头获取令牌,该标头是 oauth 的“资源服务器”文档的一部分。您基本上是在构建一个自定义登录解决方案,将 formLogin 与 oauth2 资源服务器混合在一起。这是不好的做法。构建自定义安全解决方案是不好的做法。这就是存在安全标准的原因。
-
basic auth 使用用户名和密码,formlogin 使用用户名和密码。令牌是 oauth2 标准的一部分,使用令牌对某人进行身份验证是 oauth2 资源服务器标准的一部分。如果您只想使用令牌登录某人,您应该使用我发布的内容而不是构建自定义安全解决方案。
标签: spring spring-boot spring-security