Spring-security - httponlycookie 集成到现有的 jwt 中?

ABp*_*ive 2 spring httponly spring-security cookie-httponly

有人告诉我,在使用单独的前端服务时,仅使用 JWT 而不使用 HttpOnly cookie 是不安全的。

正如这里建议的:

http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/

HttpOnly Cookie:https://www.ictshore.com/ict-basics/httponly-cookie/

我目前有一个可用的 JWT 系统,因此我正在尝试升级它以支持 cookie 实现。

我首先将我的 SecurityConfiguration 更改为以下内容:

    private final UserDetailsService uds;
    private final PasswordEncoder bcpe;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(uds).passwordEncoder(bcpe);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable();
        http.addFilter(new CustomAuthenticationFilter(authenticationManagerBean()));
        http.addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().logout().deleteCookies(CustomAuthorizationFilter.COOKIE_NAME)
            .and().authorizeRequests().antMatchers("/login/**", "/User/refreshToken", "/User/add").permitAll()
            .and().authorizeRequests().antMatchers(GET, "/**").hasAnyAuthority("STUDENT")
            .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception{ // NO FUCKING IDEA WHAT THIS DOES
        return super.authenticationManagerBean();
    }
Run Code Online (Sandbox Code Playgroud)

从这里我尝试将实际的 cookie 实现插入到我的CustomAuthorizationFilter

public class CustomAuthorizationFilter extends OncePerRequestFilter { // INTERCEPTS EVERY REQUEST

    public static final String COOKIE_NAME = "auth_by_cookie";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        if(request.getServletPath().equals("/login") || request.getServletPath().equals("/User/refreshToken/**")){ // DO NOTHING IF LOGGING IN OR REFRESHING TOKEN
            filterChain.doFilter(request,response);
        }
        else{
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")){
                try {
                    String token = authorizationHeader.substring("Bearer ".length());
                    //NEEDS SECURE AND ENCRYPTED vvvvvvv
                    Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());

                    JWTVerifier verifier = JWT.require(algorithm).build(); // USING AUTH0
                    DecodedJWT decodedJWT = verifier.verify(token);
                    String email = decodedJWT.getSubject(); // GETS EMAIL
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    stream(roles).forEach(role -> {  authorities.add(new SimpleGrantedAuthority(role)); });
                    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, null, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                    filterChain.doFilter(request, response);
                }

                catch (Exception e){
                    response.setHeader("error" , e.getMessage() );
                    response.setStatus(FORBIDDEN.value());
                    Map<String, String> error = new HashMap<>();
                    error.put("error_message", e.getMessage());
                    response.setContentType(APPLICATION_JSON_VALUE);
                    new ObjectMapper().writeValue(response.getOutputStream(), error);
                }
            }
            else{ filterChain.doFilter(request, response); }
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

我不知道在哪里插入 cookie 读数以及在哪里包装它。它会环绕 JWT 吗?

我确实看到了这个实现:

public class CookieAuthenticationFilter extends OncePerRequestFilter {

    public static final String COOKIE_NAME = "auth_by_cookie";

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest,
                                    HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        Optional<Cookie> cookieAuth = Stream.of(Optional.ofNullable(httpServletRequest.getCookies()).orElse(new Cookie[0]))
                .filter(cookie -> COOKIE_NAME.equals(cookie.getName()))
                .findFirst();

        if (cookieAuth.isPresent()) {
            SecurityContextHolder.getContext().setAuthentication(
                    new PreAuthenticatedAuthenticationToken(cookieAuth.get().getValue(), null));
        }

        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}
Run Code Online (Sandbox Code Playgroud)

虽然这提到了它的“authenticationFilter”,但我确实有一个身份验证过滤器,尽管它与此CookieAuthenticationFilter相比不太具有可比性CustomAuthorizationFilter

public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final AuthenticationManager authManager;
    public CustomAuthenticationFilter authManagerFilter;
    private UserService userService;

    @Override // THIS OVERRIDES THE DEFAULT SPRING SECURITY IMPLEMENTATION
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String email = request.getParameter("email");
        String password = request.getParameter("password");
        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password);
        return authManager.authenticate(authToken);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
        // SPRING SECURITY BUILT IN USER
        User springUserDetails = (User) authentication.getPrincipal();

        // NEEDS SECURE AND ENCRYPTED vvvvvvv
        Algorithm algorithm = Algorithm.HMAC256("secret".getBytes()); // THIS IS USING AUTH0 DEPENDENCY
        String access_token = JWT.create()
                .withSubject(springUserDetails.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + 120 * 60 * 1000)) // this should be 2 hours
                .withIssuer(request.getRequestURI().toString())
                .withClaim("roles", springUserDetails.getAuthorities()
                        .stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.toList()))
                .sign(algorithm);

        String refresh_token = JWT.create()
                .withSubject(springUserDetails.getUsername())
                .withExpiresAt(new Date(System.currentTimeMillis() + 120 * 60 * 1000)) // this should be 2 hours
                .withIssuer(request.getRequestURI().toString())
                .withClaim("roles", springUserDetails.getAuthorities()
                        .stream()
                        .map(GrantedAuthority::getAuthority)
                        .collect(Collectors.toList()))
                .sign(algorithm);

        Map<String, String> tokens = new HashMap<>();
        tokens.put("access_token", access_token);
        tokens.put("refresh_token", refresh_token);
        response.setContentType(APPLICATION_JSON_VALUE);
        new ObjectMapper().writeValue(response.getOutputStream(), tokens);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
            ...
            new ObjectMapper().writeValue(response.getOutputStream(), error);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

欢迎任何建议!

Tho*_*olf 5

通过查看所有自定义代码,我强烈建议您实际阅读可用的不同身份验证类型的 Spring Security 文档,并查找其优点和缺点。

并了解如何构建登录有安全标准,而您构建的内容是不安全的、不可扩展的定制,这是非常糟糕的做法。

但这里有一个简短的回顾:

表单登录

用户通过提供用户名和密码来验证自己的身份。作为回报,他们将获得一个会话 cookie,其中包含随机字符串,但映射到服务器端的键值存储。

cookie 设置为 httpOnly 和 httpSecure,这意味着它们更难被窃取,并且不易受到浏览器中的 XSS 攻击。

我只是想强调 cookie 包含一个随机字符串,因此如果您想要用户信息,您可以在登录后返回 cookie 并在正文中返回 userinfo,或者对用户端点进行额外调用并获取用户信息。

缺点是,如果您想要 5 个后端服务器,则此解决方案无法扩展,您需要 Spring Session 之类的东西并设置一个存储,用于存储会话,以便在后端服务器之间共享。

好的一面是,我们可以随时在服务器端使 cookie 失效。我们拥有完全的控制权。

oauth2

嗯,这是大多数人都知道的,您想要登录,然后您被重定向到发行者(另一台服务器)。您通过该服务器进行身份验证,服务器会为您提供一个临时令牌,您可以用它来交换不透明令牌。

不透明令牌中的内容只是发行者跟踪的随机文本字符串。

现在,当您想要调用后端时,您可以将后端设置为资源服务器,并在标头中提供令牌。资源服务器从标头中提取令牌,询问颁发者令牌是否有效,然后回答是或否。

在这里,您可以撤销令牌,方法是向发行者询问“此令牌不再有效”,下次提供令牌时,它将与发行者检查它是否被阻止,我们没事。

oauth2 + JWT

与上面类似,但我们没有使用不透明的令牌,而是向客户端发送 JWT。这样,当 JWT 呈现资源服务器时,不必询问颁发者令牌是否有效。我们可以使用 JWK 检查签名。通过这种方法,我们可以减少对发行者的一次调用来检查令牌的有效性。

JWT 只是一种令牌格式。Opague token = 随机字符串,JWT = JSON 格式的签名数据并用作 token。

JWT 从来都不是要取代 cookie,只是人们开始使用它们来代替 cookie。

但我们失去的是知道撤销令牌的能力。由于我们不跟踪发行者中的 JWT,并且我们不会在每次调用时询问发行者。

我们可以通过使用短期代币来降低风险。也许 5 分钟。但请记住,这仍然是一个风险,恶意行为者可能会在 5 分钟内造成损害。

您的解决方案

如果我们看看您的自定义解决方案,互联网上有很多人正在构建该解决方案,该解决方案有很多缺陷,那就是您构建了一个 FormLogin 解决方案,该解决方案给出了 JWT,因此带来了 JWT 的所有问题。

因此,您的令牌可能会在浏览器中被盗,因为它不具备 Cookie 附带的安全性。如果代币被盗,我们无法撤销它。它不可扩展,并且是自定义编写的,这意味着一个错误就会危及整个应用程序的数据。

因此,基本上上述解决方案中的所有不好的事情都在这里组合成一个超级糟糕的解决方案。

我的建议

您删除所有自定义代码并查看您拥有的应用程序类型。

如果是单服务器小型应用程序,请使用 FormLogin,根本不要使用 JWT。Cookie 已经发挥了 20 年的功效,而且仍然没问题。不要仅仅因为想使用 JWT 就使用 JWT。

如果您正在编写较大的应用程序,请使用专用的授权服务器,例如 okta、curity、spring 授权服务器、keycloak。

然后使用 Spring Security 附带的内置资源服务器功能将您的服务器设置为资源服务器,并在文档中的 JWT 章节中进行了记录。

JWT 从一开始就不打算暴露给客户端,因为您可以读取其中的所有内容,因此它们应该在服务器之间使用,以最大限度地减少对发行者的调用,因为数据经过签名,因此每个服务器都可以自行检查签名。

然后,整个 javascript 社区和懒惰的开发人员开始编写自定义的不安全解决方案,以向客户端提供 JWT。

现在,每个人都只是在谷歌上搜索 Spring Security 使用 JWT 的教程,并构建一些自定义且不安全的东西,然后在“有人指出他们的解决方案不安全”时询问堆栈溢出。

如果您认真考虑构建安全登录,请阅读以下内容:

  • Spring Security官方文档,章节formlogin、oauth2、JWT
  • oauth2 规范
  • JWT 规范

Curity 有关于 oauth2 的良好文档https://curity.io/resources/learn/code-flow/

FormLogin 弹簧 https://docs.spring.io/spring-security/reference/servlet/authentication/passwords/form.html

oauth2 spring https://docs.spring.io/spring-security/reference/servlet/oauth2/index.html

配置您的应用程序以处理 JWT https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html

关于您的代码的一些提示

这是完全没有必要的。您重写了一个函数,然后调用默认实现,还要在注释中考虑您的语言。

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{ // NO FUCKING IDEA WHAT THIS DOES
    return super.authenticationManagerBean();
}
Run Code Online (Sandbox Code Playgroud)

另外,整个类都可以被删除

public class CustomAuthorizationFilter extends OncePerRequestFilter
Run Code Online (Sandbox Code Playgroud)

如果您想处理 JWT 并使您的服务器成为资源服务器,您所做的就是按照文档所述https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver -jwt-sansboot

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        // This line sets up your server to use the built in filter
        // and accept JWT tokens in headers
        .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
    return http.build();
}
Run Code Online (Sandbox Code Playgroud)

Nimbus然后,您可以使用Spring Security 附带的内置库设置 JWTDecoder

@Bean
JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri)
            .jwsAlgorithm(RS512).jwsAlgorithm(ES512).build();
}
Run Code Online (Sandbox Code Playgroud)

因为它Bean会自动注入,所以我们不需要手动设置任何东西。

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
Run Code Online (Sandbox Code Playgroud)

在这里,您已经声明您希望服务器是无状态的,这意味着您已禁用 cookie,因为服务器使用 cookie 来保留客户端的状态。然后您尝试实现自定义 cookie 过滤器。

再次,您必须决定是要使用带有 cookie 的 FormLogin 还是 oauth2 + JWT,因为现在您正在两者之间进行混合。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(uds).passwordEncoder(bcpe);
}
Run Code Online (Sandbox Code Playgroud)

很可能不需要,因为我假设 uds 和 bcpe 都是 bean、组件等,并且会自动注入。无需将某些东西制作为 bean 然后手动设置它。你把一些东西变成了bean,这样你就不必手动设置它。但你两者都在做。