Xác thực RESTful qua Spring


262

Vấn đề:
Chúng tôi có API RESTful dựa trên Spring MVC chứa thông tin nhạy cảm. API phải được bảo mật, tuy nhiên việc gửi thông tin đăng nhập của người dùng (kết hợp người dùng / vượt qua) với mỗi yêu cầu là không mong muốn. Theo hướng dẫn REST (và yêu cầu kinh doanh nội bộ), máy chủ phải không trạng thái. API sẽ được sử dụng bởi một máy chủ khác theo cách tiếp cận kiểu mashup.

Yêu cầu:

  • Khách hàng đưa ra yêu cầu .../authenticate(URL không được bảo vệ) với thông tin đăng nhập; máy chủ trả về mã thông báo an toàn chứa đủ thông tin để máy chủ xác thực các yêu cầu trong tương lai và vẫn không trạng thái. Điều này có thể sẽ bao gồm cùng thông tin với Mã thông báo Ghi nhớ của Spring Security .

  • Khách hàng thực hiện các yêu cầu tiếp theo cho các URL khác nhau (được bảo vệ), nối thêm mã thông báo thu được trước đó làm tham số truy vấn (hoặc, ít mong muốn hơn, tiêu đề yêu cầu HTTP).

  • Khách hàng không thể dự kiến ​​lưu trữ cookie.

  • Vì chúng ta đã sử dụng Spring rồi, nên giải pháp nên sử dụng Spring Security.

Chúng tôi đã đập đầu vào tường cố gắng để làm cho công việc này, vì vậy hy vọng ai đó ngoài kia đã giải quyết vấn đề này.

Với kịch bản trên, làm thế nào bạn có thể giải quyết nhu cầu đặc biệt này?


49
Xin chào Chris, tôi không chắc chắn việc chuyển mã thông báo đó trong tham số truy vấn là ý tưởng tốt nhất. Điều đó sẽ hiển thị trong nhật ký, bất kể HTTPS hay HTTP. Các tiêu đề có lẽ an toàn hơn. Chỉ cần FYI. Câu hỏi tuyệt vời mặc dù. +1
jmort253

1
Sự hiểu biết của bạn về không quốc tịch là gì? Yêu cầu mã thông báo của bạn va chạm với sự hiểu biết của tôi về trạng thái không trạng thái. Câu trả lời xác thực http dường như đối với tôi là triển khai không trạng thái duy nhất.
Markus Malkusch

9
@MarkusMalkusch không trạng thái đề cập đến kiến ​​thức của máy chủ về các giao tiếp trước đó với một khách hàng cụ thể. HTTP không được định nghĩa theo định nghĩa và cookie phiên làm cho nó có trạng thái. Thời gian tồn tại (và nguồn, đối với vấn đề đó) của mã thông báo là không liên quan; máy chủ chỉ quan tâm rằng nó hợp lệ và có thể được liên kết lại với người dùng (KHÔNG phải là phiên). Do đó, việc chuyển mã thông báo nhận dạng không can thiệp vào trạng thái.
Chris Cashwell

1
@ChrisCashwell Làm thế nào để bạn đảm bảo rằng mã thông báo không bị giả mạo / tạo bởi khách hàng? Bạn có sử dụng khóa riêng ở phía máy chủ để mã hóa mã thông báo, cung cấp cho khách hàng và sau đó sử dụng cùng khóa để giải mã nó trong các yêu cầu trong tương lai không? Rõ ràng Base64 hoặc một số obfuscation khác sẽ không đủ. Bạn có thể giải thích các kỹ thuật để "xác thực" các mã thông báo này không?
Craig Otis

6
Mặc dù đây là ngày và tôi đã không chạm hoặc cập nhật mã trong hơn 2 năm, tôi đã tạo ra một Gist để mở rộng hơn nữa về các khái niệm này. gist.github.com/ccashwell/dfc05dd8bd1a75d189d1
Chris Cashwell

Câu trả lời:


190

Chúng tôi quản lý để làm cho điều này hoạt động chính xác như được mô tả trong OP và hy vọng ai đó khác có thể sử dụng giải pháp. Đây là những gì chúng tôi đã làm:

Thiết lập bối cảnh bảo mật như vậy:

<security:http realm="Protected API" use-expressions="true" auto-config="false" create-session="stateless" entry-point-ref="CustomAuthenticationEntryPoint">
    <security:custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" />
    <security:intercept-url pattern="/authenticate" access="permitAll"/>
    <security:intercept-url pattern="/**" access="isAuthenticated()" />
</security:http>

<bean id="CustomAuthenticationEntryPoint"
    class="com.demo.api.support.spring.CustomAuthenticationEntryPoint" />

<bean id="authenticationTokenProcessingFilter"
    class="com.demo.api.support.spring.AuthenticationTokenProcessingFilter" >
    <constructor-arg ref="authenticationManager" />
</bean>

Như bạn có thể thấy, chúng tôi đã tạo một tùy chỉnh AuthenticationEntryPoint, về cơ bản chỉ trả về 401 Unauthorizednếu yêu cầu không được xác thực trong chuỗi bộ lọc của chúng tôi AuthenticationTokenProcessingFilter.

CustomAuthenticationEntryPoint :

public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {
        response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was either missing or invalid." );
    }
}

Trình xác thựcTokenProcessingFilter :

public class AuthenticationTokenProcessingFilter extends GenericFilterBean {

    @Autowired UserService userService;
    @Autowired TokenUtils tokenUtils;
    AuthenticationManager authManager;

    public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Map<String, String[]> parms = request.getParameterMap();

        if(parms.containsKey("token")) {
            String token = parms.get("token")[0]; // grab the first "token" parameter

            // validate the token
            if (tokenUtils.validate(token)) {
                // determine the user based on the (already validated) token
                UserDetails userDetails = tokenUtils.getUserFromToken(token);
                // build an Authentication object with the user's info
                UsernamePasswordAuthenticationToken authentication = 
                        new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) request));
                // set the authentication into the SecurityContext
                SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(authentication));         
            }
        }
        // continue thru the filter chain
        chain.doFilter(request, response);
    }
}

Rõ ràng, TokenUtilscó chứa một số mã riêng tư (và rất cụ thể cho từng trường hợp) và không thể chia sẻ dễ dàng. Đây là giao diện của nó:

public interface TokenUtils {
    String getToken(UserDetails userDetails);
    String getToken(UserDetails userDetails, Long expiration);
    boolean validate(String token);
    UserDetails getUserFromToken(String token);
}

Điều đó nên giúp bạn có một khởi đầu tốt. Chúc mừng mã hóa. :)


Có cần thiết để xác thực mã thông báo khi mã thông báo được gửi cùng với yêu cầu. Làm thế nào về việc lấy thông tin tên người dùng trực tiếp và đặt trong bối cảnh / yêu cầu hiện tại?
Fisher

1
@Spring Tôi không lưu trữ chúng ở bất cứ đâu ... toàn bộ ý tưởng của mã thông báo là nó cần phải được thông qua với mọi yêu cầu và nó có thể được giải mã (một phần) để xác định tính hợp lệ của nó (do đó là validate(...)phương thức). Điều này rất quan trọng vì tôi muốn máy chủ không trạng thái. Tôi sẽ tưởng tượng bạn có thể sử dụng phương pháp này mà không cần sử dụng Spring.
Chris Cashwell

1
Nếu khách hàng là một trình duyệt, làm thế nào mã thông báo có thể được lưu trữ? hoặc bạn phải làm lại xác thực cho mỗi yêu cầu?
người mới bắt

2
lời khuyên tuyệt vời. @ChrisCashwell - phần mà tôi không thể tìm thấy là nơi bạn xác thực thông tin đăng nhập của người dùng và gửi lại mã thông báo? Tôi đoán nó sẽ ở đâu đó trong ý nghĩa của điểm cuối / xác thực. tôi có đúng không Nếu không mục tiêu của / xác thực là gì?
Yonatan Maman

3
Có gì bên trong Trình xác thực?
MoienGK

25

Bạn có thể xem xét Xác thực truy cập Digest . Về cơ bản giao thức như sau:

  1. Yêu cầu được thực hiện từ khách hàng
  2. Máy chủ trả lời bằng một chuỗi nonce duy nhất
  3. Khách hàng cung cấp tên người dùng và mật khẩu (và một số giá trị khác) md5 được băm với nonce; hàm băm này được gọi là HA1
  4. Sau đó, máy chủ có thể xác minh danh tính của khách hàng và cung cấp các tài liệu được yêu cầu
  5. Giao tiếp với nonce có thể tiếp tục cho đến khi máy chủ cung cấp một nonce mới (bộ đếm được sử dụng để loại bỏ các cuộc tấn công phát lại)

Tất cả các giao tiếp này được thực hiện thông qua các tiêu đề, như jmort253 chỉ ra, thường an toàn hơn so với giao tiếp tài liệu nhạy cảm trong các tham số url.

Xác thực truy cập Digest được hỗ trợ bởi Spring Security . Lưu ý rằng, mặc dù các tài liệu nói rằng bạn phải có quyền truy cập vào mật khẩu văn bản đơn giản của khách hàng, bạn có thể xác thực thành công nếu bạn có hàm băm HA1 cho máy khách của mình.


1
Mặc dù đây là một cách tiếp cận khả thi, một số chuyến đi khứ hồi phải được thực hiện để lấy mã thông báo làm cho nó trở nên không mong muốn.
Chris Cashwell

Nếu khách hàng của bạn tuân theo thông số Xác thực HTTP, các chuyến đi khứ hồi đó chỉ xảy ra khi có cuộc gọi đầu tiên và khi 5. xảy ra.
Markus Malkusch

5

Về mã thông báo mang thông tin, Mã thông báo web JSON ( http://jwt.io ) là một công nghệ tuyệt vời. Khái niệm chính là nhúng các yếu tố thông tin (khiếu nại) vào mã thông báo và sau đó ký toàn bộ mã thông báo để kết thúc xác thực có thể xác minh rằng các khiếu nại thực sự đáng tin cậy.

Tôi sử dụng triển khai Java này: https://bitbucket.org/b_c/jose4j/wiki/Home

Ngoài ra còn có một mô-đun Spring (spring-security-jwt), nhưng tôi chưa xem xét những gì nó hỗ trợ.


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.