Cách xác thực dựa trên mã thông báo
Trong xác thực dựa trên mã thông báo, khách hàng trao đổi thông tin xác thực cứng (như tên người dùng và mật khẩu) cho một phần dữ liệu được gọi là mã thông báo . Đối với mỗi yêu cầu, thay vì gửi thông tin xác thực cứng, khách hàng sẽ gửi mã thông báo đến máy chủ để thực hiện xác thực và sau đó ủy quyền.
Trong một vài từ, một sơ đồ xác thực dựa trên các mã thông báo theo các bước sau:
- Máy khách sẽ gửi thông tin đăng nhập của họ (tên người dùng và mật khẩu) đến máy chủ.
- Máy chủ xác thực thông tin đăng nhập và nếu chúng hợp lệ, sẽ tạo mã thông báo cho người dùng.
- Máy chủ lưu trữ mã thông báo được tạo trước đó trong một số lưu trữ cùng với số nhận dạng người dùng và ngày hết hạn.
- Máy chủ gửi mã thông báo được tạo cho khách hàng.
- Máy khách gửi mã thông báo đến máy chủ trong mỗi yêu cầu.
- Máy chủ, trong mỗi yêu cầu, trích xuất mã thông báo từ yêu cầu đến. Với mã thông báo, máy chủ tra cứu chi tiết người dùng để thực hiện xác thực.
- Nếu mã thông báo hợp lệ, máy chủ chấp nhận yêu cầu.
- Nếu mã thông báo không hợp lệ, máy chủ sẽ từ chối yêu cầu.
- Khi xác thực đã được thực hiện, máy chủ thực hiện ủy quyền.
- Máy chủ có thể cung cấp điểm cuối để làm mới mã thông báo.
Lưu ý: Bước 3 không bắt buộc nếu máy chủ đã phát hành mã thông báo đã ký (chẳng hạn như JWT, cho phép bạn thực hiện xác thực không trạng thái ).
Bạn có thể làm gì với JAX-RS 2.0 (Jersey, REST EAS và Apache CXF)
Giải pháp này chỉ sử dụng API JAX-RS 2.0, tránh mọi giải pháp cụ thể của nhà cung cấp . Vì vậy, nó nên hoạt động với các triển khai JAX-RS 2.0, chẳng hạn như Jersey , REST EAS và Apache CXF .
Điều đáng nói là nếu bạn đang sử dụng xác thực dựa trên mã thông báo, bạn không dựa vào các cơ chế bảo mật ứng dụng web Java EE tiêu chuẩn được cung cấp bởi bộ chứa servlet và có thể định cấu hình thông qua bộ web.xml
mô tả của ứng dụng . Đó là một xác thực tùy chỉnh.
Xác thực người dùng bằng tên người dùng và mật khẩu của họ và phát hành mã thông báo
Tạo phương thức tài nguyên JAX-RS để nhận và xác thực thông tin đăng nhập (tên người dùng và mật khẩu) và cấp mã thông báo cho người dùng:
@Path("/authentication")
public class AuthenticationEndpoint {
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateUser(@FormParam("username") String username,
@FormParam("password") String password) {
try {
// Authenticate the user using the credentials provided
authenticate(username, password);
// Issue a token for the user
String token = issueToken(username);
// Return the token on the response
return Response.ok(token).build();
} catch (Exception e) {
return Response.status(Response.Status.FORBIDDEN).build();
}
}
private void authenticate(String username, String password) throws Exception {
// Authenticate against a database, LDAP, file or whatever
// Throw an Exception if the credentials are invalid
}
private String issueToken(String username) {
// Issue a token (can be a random String persisted to a database or a JWT token)
// The issued token must be associated to a user
// Return the issued token
}
}
Nếu bất kỳ trường hợp ngoại lệ nào được đưa ra khi xác thực thông tin đăng nhập, phản hồi với trạng thái 403
(Bị cấm) sẽ được trả về.
Nếu thông tin đăng nhập được xác thực thành công, phản hồi với trạng thái 200
(OK) sẽ được trả về và mã thông báo đã phát hành sẽ được gửi cho khách hàng trong tải trọng phản hồi. Máy khách phải gửi mã thông báo đến máy chủ trong mọi yêu cầu.
Khi tiêu thụ application/x-www-form-urlencoded
, khách hàng phải gửi thông tin đăng nhập theo định dạng sau trong tải trọng yêu cầu:
username=admin&password=123456
Thay vì tham số mẫu, có thể bọc tên người dùng và mật khẩu vào một lớp:
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
Và sau đó sử dụng nó dưới dạng JSON:
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response authenticateUser(Credentials credentials) {
String username = credentials.getUsername();
String password = credentials.getPassword();
// Authenticate the user, issue a token and return a response
}
Sử dụng phương pháp này, khách hàng phải gửi thông tin đăng nhập theo định dạng sau trong tải trọng của yêu cầu:
{
"username": "admin",
"password": "123456"
}
Trích xuất mã thông báo từ yêu cầu và xác thực nó
Khách hàng nên gửi mã thông báo trong tiêu Authorization
đề HTTP tiêu chuẩn của yêu cầu. Ví dụ:
Authorization: Bearer <token-goes-here>
Tên của tiêu đề HTTP tiêu chuẩn là không may vì nó mang thông tin xác thực , không ủy quyền . Tuy nhiên, đó là tiêu đề HTTP tiêu chuẩn để gửi thông tin đăng nhập đến máy chủ.
JAX-RS cung cấp @NameBinding
, một chú thích meta được sử dụng để tạo các chú thích khác để liên kết các bộ lọc và bộ chặn với các lớp và phương thức tài nguyên. Xác định một @Secured
chú thích như sau:
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
Chú thích liên kết tên được xác định ở trên sẽ được sử dụng để trang trí một lớp bộ lọc, thực hiện ContainerRequestFilter
, cho phép bạn chặn yêu cầu trước khi nó được xử lý bằng phương thức tài nguyên. Có ContainerRequestContext
thể được sử dụng để truy cập các tiêu đề yêu cầu HTTP và sau đó trích xuất mã thông báo:
@Secured
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
private static final String REALM = "example";
private static final String AUTHENTICATION_SCHEME = "Bearer";
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Get the Authorization header from the request
String authorizationHeader =
requestContext.getHeaderString(HttpHeaders.AUTHORIZATION);
// Validate the Authorization header
if (!isTokenBasedAuthentication(authorizationHeader)) {
abortWithUnauthorized(requestContext);
return;
}
// Extract the token from the Authorization header
String token = authorizationHeader
.substring(AUTHENTICATION_SCHEME.length()).trim();
try {
// Validate the token
validateToken(token);
} catch (Exception e) {
abortWithUnauthorized(requestContext);
}
}
private boolean isTokenBasedAuthentication(String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
// The authentication scheme comparison must be case-insensitive
return authorizationHeader != null && authorizationHeader.toLowerCase()
.startsWith(AUTHENTICATION_SCHEME.toLowerCase() + " ");
}
private void abortWithUnauthorized(ContainerRequestContext requestContext) {
// Abort the filter chain with a 401 status code response
// The WWW-Authenticate header is sent along with the response
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE,
AUTHENTICATION_SCHEME + " realm=\"" + REALM + "\"")
.build());
}
private void validateToken(String token) throws Exception {
// Check if the token was issued by the server and if it's not expired
// Throw an Exception if the token is invalid
}
}
Nếu có bất kỳ vấn đề nào xảy ra trong quá trình xác thực mã thông báo, phản hồi với trạng thái 401
(Không được phép) sẽ được trả lại. Nếu không, yêu cầu sẽ tiến hành một phương thức tài nguyên.
Đảm bảo các điểm cuối REST của bạn
Để liên kết bộ lọc xác thực với các phương thức tài nguyên hoặc các lớp tài nguyên, chú thích chúng với @Secured
chú thích được tạo ở trên. Đối với các phương thức và / hoặc các lớp được chú thích, bộ lọc sẽ được thực thi. Điều đó có nghĩa là các điểm cuối như vậy sẽ chỉ đạt được nếu yêu cầu được thực hiện với mã thông báo hợp lệ.
Nếu một số phương thức hoặc lớp không cần xác thực, chỉ cần không chú thích chúng:
@Path("/example")
public class ExampleResource {
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myUnsecuredMethod(@PathParam("id") Long id) {
// This method is not annotated with @Secured
// The authentication filter won't be executed before invoking this method
...
}
@DELETE
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response mySecuredMethod(@PathParam("id") Long id) {
// This method is annotated with @Secured
// The authentication filter will be executed before invoking this method
// The HTTP request must be performed with a valid token
...
}
}
Trong ví dụ trình bày ở trên, bộ lọc sẽ được thực hiện chỉ cho mySecuredMethod(Long)
phương pháp vì nó chú thích với @Secured
.
Xác định người dùng hiện tại
Rất có khả năng bạn sẽ cần biết người dùng đang thực hiện yêu cầu lại API REST của bạn. Các cách tiếp cận sau đây có thể được sử dụng để đạt được nó:
Ghi đè bối cảnh bảo mật của yêu cầu hiện tại
Trong ContainerRequestFilter.filter(ContainerRequestContext)
phương thức của bạn , một thể hiện mới SecurityContext
có thể được đặt cho yêu cầu hiện tại. Sau đó ghi đè lên SecurityContext.getUserPrincipal()
, trả về một Principal
thể hiện:
final SecurityContext currentSecurityContext = requestContext.getSecurityContext();
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return () -> username;
}
@Override
public boolean isUserInRole(String role) {
return true;
}
@Override
public boolean isSecure() {
return currentSecurityContext.isSecure();
}
@Override
public String getAuthenticationScheme() {
return AUTHENTICATION_SCHEME;
}
});
Sử dụng mã thông báo để tra cứu định danh người dùng (tên người dùng), đó sẽ là Principal
tên của.
Tiêm SecurityContext
vào bất kỳ lớp tài nguyên JAX-RS nào:
@Context
SecurityContext securityContext;
Điều tương tự có thể được thực hiện trong phương thức tài nguyên JAX-RS:
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
Và sau đó nhận được Principal
:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
Sử dụng CDI (Bối cảnh và tiêm phụ thuộc)
Nếu, vì một số lý do, bạn không muốn ghi đè lên SecurityContext
, bạn có thể sử dụng CDI (Bối cảnh và phụ thuộc tiêm), cung cấp các tính năng hữu ích như sự kiện và nhà sản xuất.
Tạo vòng loại CDI:
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
Trong phần AuthenticationFilter
đã tạo của bạn ở trên, hãy thêm một Event
chú thích với@AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
Nếu xác thực thành công, hãy kích hoạt sự kiện chuyển tên người dùng làm tham số (hãy nhớ, mã thông báo được cấp cho người dùng và mã thông báo sẽ được sử dụng để tra cứu định danh người dùng):
userAuthenticatedEvent.fire(username);
Rất có khả năng có một lớp đại diện cho người dùng trong ứng dụng của bạn. Hãy gọi lớp này User
.
Tạo một hạt CDI để xử lý sự kiện xác thực, tìm một User
thể hiện với tên người dùng tương ứng và gán nó cho trường authenticatedUser
nhà sản xuất:
@RequestScoped
public class AuthenticatedUserProducer {
@Produces
@RequestScoped
@AuthenticatedUser
private User authenticatedUser;
public void handleAuthenticationEvent(@Observes @AuthenticatedUser String username) {
this.authenticatedUser = findUser(username);
}
private User findUser(String username) {
// Hit the the database or a service to find a user by its username and return it
// Return the User instance
}
}
Các authenticatedUser
lĩnh vực sản xuất ra một User
ví dụ có thể được tiêm vào container quản lý đậu, chẳng hạn như các dịch vụ JAX-RS, đậu CDI, servlets và EJB. Sử dụng đoạn mã sau để thêm một User
thể hiện (trên thực tế, đó là proxy CDI):
@Inject
@AuthenticatedUser
User authenticatedUser;
Lưu ý rằng @Produces
chú thích CDI khác với @Produces
chú thích JAX-RS :
Hãy chắc chắn rằng bạn sử dụng @Produces
chú thích CDI trong AuthenticatedUserProducer
bean của bạn .
Chìa khóa ở đây là bean được chú thích @RequestScoped
, cho phép bạn chia sẻ dữ liệu giữa các bộ lọc và các bean của bạn. Nếu bạn không sử dụng các sự kiện, bạn có thể sửa đổi bộ lọc để lưu trữ người dùng được xác thực trong một bean có phạm vi yêu cầu và sau đó đọc nó từ các lớp tài nguyên JAX-RS của bạn.
So với cách tiếp cận ghi đè SecurityContext
, phương pháp CDI cho phép bạn có được người dùng được xác thực từ các loại đậu khác ngoài các nhà cung cấp và tài nguyên của JAX-RS.
Hỗ trợ ủy quyền dựa trên vai trò
Vui lòng tham khảo câu trả lời khác của tôi để biết chi tiết về cách hỗ trợ ủy quyền dựa trên vai trò.
Phát hành mã thông báo
Mã thông báo có thể là:
- Opaque: Hiển thị không có chi tiết nào ngoài giá trị (như một chuỗi ngẫu nhiên)
- Tự chứa: Chứa các chi tiết về chính mã thông báo (như JWT).
Xem chi tiết bên dưới:
Chuỗi ngẫu nhiên dưới dạng mã thông báo
Mã thông báo có thể được phát hành bằng cách tạo một chuỗi ngẫu nhiên và lưu nó vào cơ sở dữ liệu cùng với mã định danh người dùng và ngày hết hạn. Một ví dụ điển hình về cách tạo một chuỗi ngẫu nhiên trong Java có thể được nhìn thấy ở đây . Bạn cũng có thể sử dụng:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (Mã thông báo web JSON)
JWT (JSON Web Token) là một phương pháp tiêu chuẩn để thể hiện các khiếu nại một cách an toàn giữa hai bên và được xác định bởi RFC 7519 .
Đó là mã thông báo độc lập và nó cho phép bạn lưu trữ thông tin chi tiết trong khiếu nại . Các khiếu nại này được lưu trữ trong tải trọng mã thông báo được mã hóa JSON là Base64 . Dưới đây là một số khiếu nại được đăng ký trong RFC 7519 và ý nghĩa của chúng (đọc RFC đầy đủ để biết thêm chi tiết):
iss
: Hiệu trưởng đã phát hành mã thông báo.
sub
: Hiệu trưởng là chủ đề của JWT.
exp
: Ngày hết hạn cho mã thông báo.
nbf
: Thời gian mã thông báo sẽ bắt đầu được chấp nhận để xử lý.
iat
: Thời gian mã thông báo được phát hành.
jti
: Mã định danh duy nhất cho mã thông báo.
Xin lưu ý rằng bạn không được lưu trữ dữ liệu nhạy cảm, như mật khẩu, trong mã thông báo.
Tải trọng có thể được đọc bởi khách hàng và có thể dễ dàng kiểm tra tính toàn vẹn của mã thông báo bằng cách xác minh chữ ký của nó trên máy chủ. Chữ ký là thứ ngăn chặn mã thông báo bị giả mạo.
Bạn sẽ không cần phải duy trì mã thông báo JWT nếu bạn không cần theo dõi chúng. Mặc dù, bằng cách duy trì mã thông báo, bạn sẽ có khả năng vô hiệu hóa và thu hồi quyền truy cập của chúng. Để theo dõi mã thông báo JWT, thay vì duy trì toàn bộ mã thông báo trên máy chủ, bạn có thể duy trì mã định danh mã thông báo (jti
xác nhận) cùng với một số chi tiết khác như người dùng bạn đã cấp mã thông báo, ngày hết hạn, v.v.
Khi các mã thông báo vẫn tồn tại, luôn luôn xem xét loại bỏ các mã cũ để ngăn cơ sở dữ liệu của bạn phát triển vô thời hạn.
Sử dụng JWT
Có một vài thư viện Java để phát hành và xác thực các mã thông báo JWT, chẳng hạn như:
Để tìm một số tài nguyên tuyệt vời khác để làm việc với JWT, hãy xem http://jwt.io .
Xử lý thu hồi mã thông báo với JWT
Nếu bạn muốn thu hồi mã thông báo, bạn phải theo dõi chúng. Bạn không cần lưu trữ toàn bộ mã thông báo ở phía máy chủ, chỉ lưu trữ mã định danh mã thông báo (phải là duy nhất) và một số siêu dữ liệu nếu bạn cần. Đối với mã định danh mã thông báo, bạn có thể sử dụng UUID .
Yêu jti
cầu nên được sử dụng để lưu trữ mã định danh mã thông báo trên mã thông báo. Khi xác thực mã thông báo, đảm bảo rằng nó chưa bị thu hồi bằng cách kiểm tra giá trị của jti
khiếu nại đối với các mã định danh mã thông báo bạn có ở phía máy chủ.
Vì mục đích bảo mật, hãy thu hồi tất cả các mã thông báo cho người dùng khi họ thay đổi mật khẩu.
Thông tin thêm
- Việc bạn quyết định sử dụng loại xác thực nào không quan trọng. Luôn luôn làm điều đó trên đầu kết nối HTTPS để ngăn chặn cuộc tấn công giữa chừng .
- Hãy xem câu hỏi này từ Bảo mật thông tin để biết thêm thông tin về mã thông báo.
- Trong bài viết này, bạn sẽ tìm thấy một số thông tin hữu ích về xác thực dựa trên mã thông báo.
The server stores the previously generated token in some storage along with the user identifier and an expiration date. The server sends the generated token to the client.
Làm thế nào là RESTful này?