使用 JWT 和基本身份验证保护 REST 应用程序是否有意义?
Does securing a REST application with a JWT and Basic authentication make sense?
我有一个 Spring REST 应用程序,它最初使用基本身份验证进行保护。
然后我添加了一个登录控制器,它创建了一个 JWT JSON Web 令牌,用于后续请求。
我可以将以下代码移出登录控制器并移入安全过滤器吗?然后我就不再需要登录控制器了。
tokenAuthenticationService.addTokenToResponseHeader(responseHeaders, credentialsResource.getEmail());
或者我可以删除基本身份验证吗?
将基本身份验证与 JWT 混合使用是否是一个好的设计?
虽然一切正常,但我对如何最好地设计这种安全性有点不知所措。
假设所有通信都使用 100% TLS - 在登录期间和登录后始终如此 - 通过基本身份验证使用 username/password 进行身份验证并接收 JWT 作为交换是一个有效的用例。这就是 几乎 OAuth 2 流程之一 ('password grant') 的工作原理。
想法是最终用户通过一个端点进行身份验证,例如/login/token
使用您想要的任何机制,并且响应应包含将在所有后续请求中发回的 JWT。 JWT 应该是具有适当 JWT 过期 (exp
) 字段的 JWS(即加密签名的 JWT):这确保客户端无法操纵 JWT 或使其寿命超过应有的时间。
您也不需要 X-Auth-Token
header:HTTP 身份验证 Bearer
方案是为这个确切的用例创建的:基本上是跟踪 Bearer
方案名称是'bearer' 应验证的信息。您只需设置 Authorization
header:
Authorization: Bearer <JWT value here>
但是,话虽这么说,如果您的 REST 客户端是 'untrusted'(例如 JavaScript-enabled 浏览器),我什至不会这样做:HTTP 响应中可通过 JavaScript - 基本上任何 header 值或响应 body 值 - 都可以通过 MITM XSS 攻击嗅探和拦截。
最好将 JWT 值存储在 secure-only、http-only cookie 中(cookie 配置:setSecure(true)、setHttpOnly(true))。这保证浏览器将:
- 仅通过 TLS 连接传输 cookie,并且
- 永远不要让 cookie 值可用于 JavaScript 代码。
此方法几乎 为best-practices 安全所需的一切。最后一件事是确保您对每个 HTTP 请求都有 CSRF 保护,以确保向您的站点发起请求的外部域无法正常工作。
执行此操作的最简单方法是设置一个具有随机值的仅安全(但不是仅 http)cookie,例如一个 UUID。
然后,在每次向您的服务器发出请求时,确保您自己的 JavaScript 代码读取 cookie 值并将其设置在自定义 header 中,例如X-CSRF-Token 并验证服务器中每个请求的值。除非外部客户端通过 HTTP 选项请求获得授权,否则外部域客户端无法为对您域的请求设置自定义 headers,因此任何 CSRF 攻击尝试(例如,在 IFrame 中,无论如何)都将失败。
据我们所知,这是当今网络上不受信任的 JavaScript 客户端可用的同类最佳安全措施。如果您好奇的话,Stormpath 也写了一篇关于 these techniques 的文章。 HTH!
这里有一些代码来支持在 Spring 中如何执行此操作的已接受答案...只需扩展 UsernamePasswordAuthenticationFilter
并将其添加到 Spring 安全...这适用于 HTTP 基本身份验证 + Spring 安全性
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
ApplicationUser creds = new ObjectMapper()
.readValue(req.getInputStream(), ApplicationUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
使用 JWT 库:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
spring 启动配置 class
package com.vanitysoft.payit.security.web.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.vanitysoft.payit.util.SecurityConstants;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll()
.antMatchers("/user/**").authenticated()
.and()
.httpBasic()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout()
.permitAll();
}
}
我有一个 Spring REST 应用程序,它最初使用基本身份验证进行保护。
然后我添加了一个登录控制器,它创建了一个 JWT JSON Web 令牌,用于后续请求。
我可以将以下代码移出登录控制器并移入安全过滤器吗?然后我就不再需要登录控制器了。
tokenAuthenticationService.addTokenToResponseHeader(responseHeaders, credentialsResource.getEmail());
或者我可以删除基本身份验证吗?
将基本身份验证与 JWT 混合使用是否是一个好的设计?
虽然一切正常,但我对如何最好地设计这种安全性有点不知所措。
假设所有通信都使用 100% TLS - 在登录期间和登录后始终如此 - 通过基本身份验证使用 username/password 进行身份验证并接收 JWT 作为交换是一个有效的用例。这就是 几乎 OAuth 2 流程之一 ('password grant') 的工作原理。
想法是最终用户通过一个端点进行身份验证,例如/login/token
使用您想要的任何机制,并且响应应包含将在所有后续请求中发回的 JWT。 JWT 应该是具有适当 JWT 过期 (exp
) 字段的 JWS(即加密签名的 JWT):这确保客户端无法操纵 JWT 或使其寿命超过应有的时间。
您也不需要 X-Auth-Token
header:HTTP 身份验证 Bearer
方案是为这个确切的用例创建的:基本上是跟踪 Bearer
方案名称是'bearer' 应验证的信息。您只需设置 Authorization
header:
Authorization: Bearer <JWT value here>
但是,话虽这么说,如果您的 REST 客户端是 'untrusted'(例如 JavaScript-enabled 浏览器),我什至不会这样做:HTTP 响应中可通过 JavaScript - 基本上任何 header 值或响应 body 值 - 都可以通过 MITM XSS 攻击嗅探和拦截。
最好将 JWT 值存储在 secure-only、http-only cookie 中(cookie 配置:setSecure(true)、setHttpOnly(true))。这保证浏览器将:
- 仅通过 TLS 连接传输 cookie,并且
- 永远不要让 cookie 值可用于 JavaScript 代码。
此方法几乎 为best-practices 安全所需的一切。最后一件事是确保您对每个 HTTP 请求都有 CSRF 保护,以确保向您的站点发起请求的外部域无法正常工作。
执行此操作的最简单方法是设置一个具有随机值的仅安全(但不是仅 http)cookie,例如一个 UUID。
然后,在每次向您的服务器发出请求时,确保您自己的 JavaScript 代码读取 cookie 值并将其设置在自定义 header 中,例如X-CSRF-Token 并验证服务器中每个请求的值。除非外部客户端通过 HTTP 选项请求获得授权,否则外部域客户端无法为对您域的请求设置自定义 headers,因此任何 CSRF 攻击尝试(例如,在 IFrame 中,无论如何)都将失败。
据我们所知,这是当今网络上不受信任的 JavaScript 客户端可用的同类最佳安全措施。如果您好奇的话,Stormpath 也写了一篇关于 these techniques 的文章。 HTH!
这里有一些代码来支持在 Spring 中如何执行此操作的已接受答案...只需扩展 UsernamePasswordAuthenticationFilter
并将其添加到 Spring 安全...这适用于 HTTP 基本身份验证 + Spring 安全性
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req,
HttpServletResponse res) throws AuthenticationException {
try {
ApplicationUser creds = new ObjectMapper()
.readValue(req.getInputStream(), ApplicationUser.class);
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
creds.getUsername(),
creds.getPassword(),
new ArrayList<>())
);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
protected void successfulAuthentication(HttpServletRequest req,
HttpServletResponse res,
FilterChain chain,
Authentication auth) throws IOException, ServletException {
String token = Jwts.builder()
.setSubject(((User) auth.getPrincipal()).getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
res.addHeader(HEADER_STRING, TOKEN_PREFIX + token);
}
}
使用 JWT 库:
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
spring 启动配置 class
package com.vanitysoft.payit.security.web.impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import com.vanitysoft.payit.util.SecurityConstants;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(bCryptPasswordEncoder);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, SecurityConstants.SIGN_UP_URL).permitAll()
.antMatchers("/user/**").authenticated()
.and()
.httpBasic()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.logout()
.permitAll();
}
}