REST Web service: Basic Authentication trong Jersey 2.x

Trong bài trước, chúng ta đã cùng tìm hiểu về xác thực và phân quyền trong ứng dụng. Trong bài này, chúng ta sẽ cùng tìm hiểu về xác thực và phân quyền ứng dụng sử dụng cơ chế Basic Authentication trong Jersey 2.x.

1. Giới thiệu Basic Authentication trong Jersey REST API

Basic Authentication là cơ chế xác thực mà ứng dụng client sẽ gửi username + password của người dùng theo mỗi request lên server.

Trong bài “Filter và Interceptor với Jersey 2.x“, chúng ta đã biết Filter có thể thực hiện một số hành động trước khi resource method được thực thi. Phía server có thể dễ dàng chứng thực tất cả request của client trước khi REST API method thực sự được gọi thông qua tính năng Filter này.

Ý tưởng như sau:

  • Tạo một Filter implements từ ContainerRequestFilter : để verify access của một user.
  • Đánh dấu Annotation: @RolesAllowed hoặc @PermitAll hoặc @DenyAll ở mức class hoặc method để xác thực quyền truy cập resource.
    • @RolesAllowed : chỉ định một số role được phép gọi resource method.
    • @PermitAll : bất kỳ user nào được phép gọi resource method.
    • @DenyAll : bất kỳ user nào cũng không được phép gọi resource method.

2. Basic Authentication trong Jersey Server

2.1. Tạo Jersey project

Tương tự như các bài viết trước, chúng ta sẽ tạo Jersey project với cấu trúc như sau:

2.2. Tạo các data model class

Order.java

package com.maixuanviet.model;
 
import javax.xml.bind.annotation.XmlRootElement;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
@Data
@AllArgsConstructor
@NoArgsConstructor
@XmlRootElement
public class Order {
 
    private Integer id;
    private String name;
}

User.java

package com.maixuanviet.model;
 
import java.util.List;
 
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
 
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
 
    private String username;
    private String password;
    private List<String> roles;
}

Role.java

package com.maixuanviet.model;
 
public interface Role {
 
    String ROLE_ADMIN = "Admin";
    String ROLE_CUSTOMER = "Customer";
}

2.3. Tạo SecurityContext class

SecurityContext : chứa thông tin chứng thực của một request. Sau khi chứng thực thành công, chúng ta cần cung cấp thông tin chứng thực user, role cho context class này. Các phương thức của SecurityContext sẽ được triệu gọi trước khi các resource method đã đánh dấu với các Annotation @RolesAllowed hoặc @PermitAll hoặc @DenyAll được thực thi.

BasicSecurityConext.java

package com.maixuanviet.model;
 
import java.security.Principal;
 
import javax.ws.rs.core.SecurityContext;
 
/**
 * The SecurityContext interface provides access to security related
 * information. An instance of SecurityContext can be injected into a JAX-RS
 * resource class field or method parameter using the @Context annotation.
 * 
 * @see https://jersey.github.io/documentation/latest/security.html
 * @see https://docs.oracle.com/javaee/7/api/javax/ws/rs/core/SecurityContext.html
 */
public class BasicSecurityConext implements SecurityContext {
 
    private User user;
    private boolean secure;
 
    public BasicSecurityConext(User user, boolean secure) {
        this.user = user;
        this.secure = secure;
    }
 
    @Override
    public Principal getUserPrincipal() {
        return () -> user.getUsername();
    }
 
    @Override
    public boolean isUserInRole(String role) {
        return user.getRoles().contains(role);
    }
 
    @Override
    public boolean isSecure() {
        return secure;
    }
 
    @Override
    public String getAuthenticationScheme() {
        return SecurityContext.BASIC_AUTH;
    }
}

2.4. Tạo REST API

Chúng ta cung cấp các REST API như sau:

OrderService.java

package com.maixuanviet.api;
 
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
 
import com.maixuanviet.model.Order;
import com.maixuanviet.model.Role;
 
// URI:
// http(s)://<domain>:(port)/<YourApplicationName>/<UrlPattern in web.xml>/<path>
// http://localhost:8080/RestfulWebServiceExample/rest/orders
@Path("/orders")
@PermitAll
public class OrderService {
 
    @GET
    @Path("/{id}")
    public Response get(@PathParam("id") int id) {
        System.out.println("OrderService#get()");
        return Response.ok("OrderService#get()").build();
    }
 
    @RolesAllowed(Role.ROLE_CUSTOMER)
    @POST
    public Response insert(Order order, @Context SecurityContext securityContext) {
        System.out.println("User: " + securityContext.getUserPrincipal().getName());
        System.out.println("OrderService#insert()");
        return Response.ok("OrderService#insert()").build();
    }
 
    @RolesAllowed({ Role.ROLE_ADMIN, Role.ROLE_CUSTOMER })
    @PUT
    public Response update(Order order) {
        System.out.println("OrderService#update()");
        return Response.ok("OrderService#update()").build();
    }
 
    @RolesAllowed(Role.ROLE_ADMIN)
    @DELETE
    @Path("/{id}")
    public Response delete(@PathParam("id") int id) {
        System.out.println("OrderService#delete()");
        return Response.ok("OrderService#delete()").build();
    }
}

2.5. Tạo ContainerRequestFilter để chứng thực user

Chúng ta sử dụng ContainerRequestFilter để thực hiện chứng thực user trước khi truy cập resource method.

  • Lấy thông tin Authorization từ header ở phía client gửi lên.
  • Tách lấy username và password từ Authentication header.
  • Truy xuất thông tin user từ database dựa trên username client gửi lên.
  • Thực hiện chứng thực user.
  • Lưu thông tin chứng thực lại để các request sau có thể kiểm tra chứng thực mà không cần truy xuất database, chẳng hạn @RolesAllowed.
package com.maixuanviet.filter;
 
import java.io.UnsupportedEncodingException;
import java.util.Base64;
import java.util.StringTokenizer;
 
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
 
import com.maixuanviet.model.BasicSecurityConext;
import com.maixuanviet.model.User;
import com.maixuanviet.service.UserService;
 
/**
 * This filter verify the access permissions for a user based on username and
 * password provided in request
 */
@Provider
@Priority(Priorities.AUTHENTICATION) // needs to happen before authorization
public class AuthFilter implements ContainerRequestFilter {
 
    @Override
    public void filter(ContainerRequestContext containerRequest)
            throws WebApplicationException, UnsupportedEncodingException {
 
        // (1) Parsing the Basic Auth Authorization header
        // The structure of authentication header:
        // Authorization: Basic encodedByBase64(username:password)
        String authCredentials = containerRequest.getHeaderString(HttpHeaders.AUTHORIZATION);
        if (null == authCredentials) {
            return;
        }
 
        // (2) Extract user name and password from Authentication header
        final String encodedUserPassword = authCredentials.replaceFirst("Basic" + " ", "");
        byte[] decodedBytes = Base64.getDecoder().decode(encodedUserPassword);
        String usernameAndPassword = new String(decodedBytes, "UTF-8");
 
        final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":");
        final String username = tokenizer.nextToken();
        final String password = tokenizer.nextToken();
 
        // (3) Getting the User with the username
        UserService userService = new UserService();
        User user = userService.getUser(username);
 
        // (4) Doing authentication
        if (user == null || !user.getPassword().equals(password)) {
            Response respone = Response.status(Response.Status.UNAUTHORIZED) // 401 Unauthorized
                    .entity("You cannot access this resource") // the response entity
                    .build();
            containerRequest.abortWith(respone);
        }
 
        // (5) Setting a new SecurityContext
        SecurityContext oldContext = containerRequest.getSecurityContext();
        containerRequest.setSecurityContext(new BasicSecurityConext(user, oldContext.isSecure()));
    }
}

Chi tiết về Filter các bạn xem lại bài viết “Filter và Interceptor với Jersey 2.x“.

2.6. Đăng ký RolesAllowedDynamicFeature

Để có thể áp dụng các Annotation @RolesAllowed, @PermitAll hoặc @DenyAll, chúng ta cần đăng ký sử dụng tính năng này với Jersey.

package com.maixuanviet.config;
 
import java.util.logging.Level;
import java.util.logging.Logger;
 
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.logging.LoggingFeature;
//Deployment of a JAX-RS application using @ApplicationPath with Servlet 3.0
//Descriptor-less deployment
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
 
public class JerseyServletContainerConfig extends ResourceConfig {
    public JerseyServletContainerConfig() {
        // if there are more than two packages then separate them with semicolon
        packages("com.maixuanviet");
        register(new LoggingFeature(Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), Level.INFO,
                LoggingFeature.Verbosity.PAYLOAD_ANY, 10000));
        register(JacksonFeature.class);
         
        // This authorization feature is not automatically turned on.
        // We need to turn it on by ourself.
        register(RolesAllowedDynamicFeature.class);
    }
}

2.7. Tạo custom exception để xử lý kết quả trả về Client khi có ngoại lệ xảy ra

GenericExceptionMapper.java

package com.maixuanviet.exception.mapper;
 
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
 
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Throwable> {
 
    @Override
    public Response toResponse(Throwable ex) {
        return Response.status(getStatusType(ex)) 
                .entity(ex.getMessage())
                .type(MediaType.TEXT_PLAIN) // "text/plain"
                .build();
    }
     
    private Response.StatusType getStatusType(Throwable ex) {
        if (ex instanceof WebApplicationException) {
            return((WebApplicationException)ex).getResponse().getStatusInfo();
        } else {
            // 500, "Internal Server Error"
            return Response.Status.INTERNAL_SERVER_ERROR;
        }
    }
}

Chi tiết về xử lý ngoại lệ với Jersey, các bạn xem lại bài viết “HTTP Status Code và xử lý ngoại lệ RESTful web service với Jersey 2.x“.

2.8. Test ứng dụng với Postman

Test @GET http://localhost:8080/RestfulWebServiceExample/rest/orders/1

Như bạn thấy, chúng ta vẫn truy xuất được resource method với @PermitAll.

Test @DELETE http://localhost:8080/RestfulWebServiceExample/rest/orders/1

Chúng ta nhận được thông báo lỗi 403, do chưa được chứng thực với user có role Admin.

Bây giờ, chúng ta đăng nhập với user Admin để truy cập resource.

Chúng ta đã truy xuất thành công resource với user có role Admin.

Tương tự các bạn test với @POST, @PUT lần lượt với các username là customer, admin, test và password là gpcoder để xem kết quả.

3. Basic Authentication trong Jersey Client

3.1. Giới thiệu HttpAuthenticationFeature

Đối với Jersey Client, chúng ta có thể sử dụng HttpAuthenticationFeature để gửi thông tin chứng thực lên server.

HttpAuthenticationFeature hỗ trợ 4 mode chứng thực sau:

  • BASIC : client luôn gửi thông tin username và password trong mỗi request. Mode này cần phải kết hợp với SSL/TLS do nội dung username và password được mã hóa với Base64.
  • BASIC NON-PREEMPTIVE : thông tin chứng thực chỉ được gửi đi nếu server từ chối request và trả về status 401 và sau đó request được lặp lại với thông tin chứng thực được gửi đi.
  • DIGEST : sử dụng Http digest authentication. Thông tin dùng MD5 nhiều lần để mã hóa.
  • UNIVERSAL : kết hợp cả basic và digest authentication trong non-preemptive mode. Tức là trong trường hợp server response status 401, một authentication mode thích hợp được sử dụng dựa vào thông tin được yêu cầu từ phía server: WWW-Authenticate HTTP header.

Ví dụ tạo một HttpAuthenticationFeature

// Basic authentication mode
HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic("username", "password");
 
// Basic authentication – non-prempitive mode
feature = HttpAuthenticationFeature.basicBuilder()
        .nonPreemptive()
        .credentials("username", "password")
        .build();
 
// Digest mode
feature = HttpAuthenticationFeature.digest("user", "superSecretPassword");
 
// Universal mode
feature = HttpAuthenticationFeature.universal("user", "superSecretPassword");
 
// Building the feature in universal mode with different credentials for basic and digest
feature = HttpAuthenticationFeature.universalBuilder()
        .credentialsForBasic("username1", "password1") // will be used for basic authentication only
        .credentials("username2", "password2") // having different credentials for different schemes
        .build();
 
final Client client = ClientBuilder.newClient();
client.register(feature);

3.2. Tạo Jersey Client sử dụng Basic Authentication

Ví dụ: tạo ứng dụng REST Client truy cập @DELETE resource.

package com.maixuanviet.client;
 
import java.util.logging.Level;
import java.util.logging.Logger;
 
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
 
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature;
import org.glassfish.jersey.logging.LoggingFeature;
 
public class OrderServiceClient {
 
    public static final String API_URL = "http://localhost:8080/RestfulWebServiceExample/rest/orders";
 
    public static void main(String[] args) {
        // (1) Create client config
        ClientConfig clientConfig = new ClientConfig();
 
        // Config logging for client side
        clientConfig.register( //
                new LoggingFeature( //
                        Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), //
                        Level.INFO, //
                        LoggingFeature.Verbosity.PAYLOAD_ANY, //
                        10000));
 
        // (2) Create basic authentication
        HttpAuthenticationFeature authDetails = HttpAuthenticationFeature.basic("admin", "maixuanviet");
 
        // (3) Create jersey client with authentication
        Client client = ClientBuilder.newClient(clientConfig);
        client.register(authDetails);
 
        // (4) Call @DELETE API
        WebTarget target = client.target(API_URL).path("1");
        Invocation.Builder invocationBuilder = target.request(MediaType.APPLICATION_JSON_TYPE);
        final Response response = invocationBuilder.delete();
 
        // (5) Handle result
        System.out.println("Call delete() successful with the result: " + response.readEntity(String.class));
    }
}