使用Boot 2.1.14.Release / security 5.1.10
我有以下需要指定安全性的端点
/token/exchange
-仅当请求具有Okta JWT时,此端点才应允许访问。它返回一个我通过JJWT手动创建的自定义JWT。基本上,用户无需传递用户凭据,而是已经通过Okta进行了身份验证,并将提供该令牌作为其凭据。
我已经添加了Okta入门程序,并且可以正常运行
/api/**
-/api
下的任何端点都需要在Authorization标头中使用我的自定义JWT
我具有以下安全配置:
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
@Configuration
public class AppWebSecurityConfigurerAdapter {
@Configuration
@Order(1)
public static class OktaWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/token/exchange") <------- this should "pin" this config right?
.authorizeRequests()
.antMatchers("/token/exchange").authenticated() <--- is this needed?
.and()
.oauth2ResourceServer().jwt();
http.cors();
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
Okta.configureResourceServer401ResponseBody(http);
}
}
@Configuration
@Order(2)
@RequiredArgsConstructor
public static class ApiWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
private final CustomSecurityConfig customSecurityConfig; <--- JWT secret key in here
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/api/**")
.authorizeRequests()
.anyRequest().authenticated()
.and()
.addFilter(new JwtFilter(authenticationManager(), customSecurityConfig))
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.cors();
}
}
}
和以下JwtFilter
@Slf4j
public class JwtFilter extends BasicAuthenticationFilter {
private final CustomSecurityConfig customSecurityConfig;
public JwtFilter(AuthenticationManager authenticationManager, CustomSecurityConfig customSecurityConfig) {
super(authenticationManager);
this.customSecurityConfig = customSecurityConfig;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
try {
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if (authentication == null) {
chain.doFilter(request, response);
return;
}
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (Exception exception){
log.error("API authentication failed", exception);
SecurityContextHolder.clearContext();
}
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = new DefaultBearerTokenResolver().resolve(request);
if (token == null) {
return null;
}
Algorithm algorithm = Algorithm.HMAC256(customSecurityConfig.getSecret());
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(CustomSecurityConfig.ISSUER)
.build();
DecodedJWT jwt = verifier.verify(token);
return new UsernamePasswordAuthenticationToken(
jwt.getClaim("user_name").asString(),
null,
jwt.getClaim("authorities")
.asList(String.class)
.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList()));
}
}
我的/api
调用所有返回401及其返回值,因为它们是由BearerTokenAuthenticationFilter
(由我的OktaWebSecurityConfigurerAdapter
使用)而不是我的JwtFilter
处理的。自然,两个令牌之间的签名不匹配。我很困惑为什么我的/api
调用甚至被该过滤器处理,因为我只为我的Okta处理程序应用了.oauth2ResourceServer().jwt();
配置
我的日志如下:
SecurityContextHolder now cleared, as request processing completed
Checking match of request : '/api/entities'; against '/token/exchange'
Checking match of request : '/api/entities'; against '/api/**'
/api/entities at position 1 of 13 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
/api/entities at position 2 of 13 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
/api/entities at position 3 of 13 in additional filter chain; firing Filter: 'HeaderWriterFilter'
/api/entities at position 4 of 13 in additional filter chain; firing Filter: 'CorsFilter'
/api/entities at position 5 of 13 in additional filter chain; firing Filter: 'LogoutFilter'
Trying to match using Ant [pattern='/logout', GET]
Checking match of request : '/api/entities'; against '/logout'
Trying to match using Ant [pattern='/logout', POST]
Request 'GET /api/entities' doesn't match 'POST /logout'
Trying to match using Ant [pattern='/logout', PUT]
Request 'GET /api/entities' doesn't match 'PUT /logout'
Trying to match using Ant [pattern='/logout', DELETE]
Request 'GET /api/entities' doesn't match 'DELETE /logout'
No matches found
/api/entities at position 6 of 13 in additional filter chain; firing Filter: 'BearerTokenAuthenticationFilter'
Authentication attempt using org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider
No event was found for the exception org.springframework.security.oauth2.core.OAuth2AuthenticationException
Authentication request for failed: org.springframework.security.oauth2.core.OAuth2AuthenticationException: An error occurred while attempting to decode the Jwt: Signed JWT rejected: Another algorithm expected, or no matching key(s) found
Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@13691de5
SecurityContextHolder now cleared, as request processing completed
为此,我整日不停地动脑筋....谢谢您的帮助!
答案 0 :(得分:0)
我将从您的问题的答案开始(我认为)。您正在使用addFilter(...)
添加过滤器,该过滤器未指定任何订单信息。请改用addFilterBefore(...)
。
我会提醒您不要使用JWT作为某种会话令牌,除非有办法撤销它们。 https://developer.okta.com/blog/2017/08/17/why-jwts-suck-as-session-tokens
在您的情况下,听起来您可能正在将一个令牌交换为“较弱”的令牌。您可以采取一些措施来减轻这种风险,例如限制令牌有效的持续时间等。我没有用例的全部内容或使用方式,因此请仔细考虑盐:)
答案 1 :(得分:0)
我在JJWT自述文件中看到了这一点
但是您可能已经注意到了一些-如果您的应用程序不只使用单个SecretKey或KeyPair,该怎么办?如果可以使用不同的SecretKeys或或两者的组合来创建JWS,怎么办??如果您不能先检查JWT,如何知道指定哪个密钥?
这使我重新考虑了设计。
基本上,我所有的路线都需要Bearer令牌。其中一位(/token/exchange
)希望令牌来自Okta。我所有的/api/**
路由都希望令牌由API服务器本身进行签名。为此,我像这样设置了一个单一的安全配置:
@Configuration
@RequiredArgsConstructor
public class MyCustomSecurityConfiguration extends WebSecurityConfigurerAdapter {
private final JwtFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
http
.cors().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
我的JwtFilter
@Slf4j
@Component
public class JwtFilter extends OncePerRequestFilter {
private static final String HEADER = HttpHeaders.AUTHORIZATION;
private final OktaTokenUtils oktaTokenUtils;
private final ApiTokenUtils apiTokenUtils;
public JwtFilter(OktaTokenUtils oktaTokenUtils, ApiTokenUtils apiTokenUtils) {
this.oktaTokenUtils = oktaTokenUtils;
this.apiTokenUtils = apiTokenUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
final String header = request.getHeader(HEADER);
if (header != null && !header.isBlank()) {
final String token = header.substring(7);
log.debug("{} TOKEN: {}", HEADER, token);
String uri = request.getRequestURI();
if (uri.equals("/token/exchange")) {
SecurityContextHolder.getContext().setAuthentication(oktaTokenUtils.authenticate(token));
} else {
SecurityContextHolder.getContext().setAuthentication(apiTokenUtils.authenticate(token));
}
}
filterChain.doFilter(request, response);
}
}
我的OktaUtils和ApiUtils类负责验证令牌并返回Authentication对象,以便过滤器可以将其添加到安全上下文持有者中
@Slf4j
@Component
public class ApiTokenUtils {
private final JwtParser parser;
public ApiTokenUtils(ApiTokenConfig config){
parser = Jwts.parserBuilder()
.requireIssuer(config.getIssuer())
.setSigningKey(config.getSecret())
.build();
}
public Authentication authenticate(String token) {
try {
Jws<Claims> claims = parser.parseClaimsJws(token);
String username = claims.getBody().get("user_name", String.class);
log.debug("valid API token for username {}", username);
List<String> authorities = claims.getBody().get("authorities", List.class);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username,
null,
authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList())
);
authentication.setDetails(MyUserDetails.builder()
.token(token)
.authorities(authorities)
.setUserId(claims.getBody().get("userId", String.class))
.email(claims.getBody().get("email", String.class))
.username(username)
.build());
return authentication;
} catch (Exception e) {
if( log.isDebugEnabled() ) {
log.error("API token verification failed", e);
} else {
log.error("API token verification failed");
}
return null;
}
}
}
@Slf4j
@Component
public class OktaTokenUtils {
private final AccessTokenVerifier verifier;
public OktaTokenUtils(OktaConfig oktaConfig) {
verifier = JwtVerifiers.accessTokenVerifierBuilder()
.setIssuer(oktaConfig.getIssuer())
.setAudience(oktaConfig.getAudience()) // defaults to 'api://default'
.setConnectionTimeout(Duration.ofSeconds(3)) // defaults to 1s
.setReadTimeout(Duration.ofSeconds(3)) // defaults to 1s
.build();
}
public Authentication authenticate(String token) {
try {
Jwt jwt = verifier.decode(token);
String subject = jwt.getClaims().get("sub").toString();
log.debug("valid Okta token for SUB {}", subject);
return new UsernamePasswordAuthenticationToken(
subject,
token,
Collections.emptyList());
} catch (Exception e) {
if( log.isDebugEnabled() ) {
log.error("Okta token verification failed", e);
} else {
log.error("Okta token verification failed");
}
return null;
}
}
}
一些对我有帮助的链接: