在 Javalin 中创建安全的 REST API

依赖项

首先,我们需要创建一个带有一些依赖项的 Maven 项目:(→ 教程)

<dependencies>
    <dependency>
        <groupId>io.javalin</groupId>
        <artifactId>javalin-bundle</artifactId>
        <version>6.6.0</version>
    </dependency>
</dependencies>

创建控制器

我们需要一些值得保护的东西。假设我们有一个非常重要的 API,用于操作用户数据库。我们创建一个控制器对象,其中包含一些虚拟数据和 CRUD 操作:

import io.javalin.http.Context;
import java.util.*;

public class UserController {
    public record User(String name, String email) {}

    private static final Map<String, User> users;

    static {
        var tempMap = Map.of(
            randomId(), new User("Alice", "alice@alice.kt"),
            randomId(), new User("Bob", "bob@bob.kt"),
            randomId(), new User("Carol", "carol@carol.kt"),
            randomId(), new User("Dave", "dave@dave.kt")
        );
        users = new HashMap<>(tempMap);
    }

    public static void getAllUserIds(Context ctx) {
        ctx.json(users.keySet());
    }

    public static void createUser(Context ctx) {
        users.put(randomId(), ctx.bodyAsClass(User.class));
    }

    public static void getUser(Context ctx) {
        ctx.json(users.get(ctx.pathParam("userId")));
    }

    public static void updateUser(Context ctx) {
        users.put(ctx.pathParam("userId"), ctx.bodyAsClass(User.class));
    }

    public static void deleteUser(Context ctx) {
        users.remove(ctx.pathParam("userId"));
    }

    private static String randomId() {
        return UUID.randomUUID().toString();
    }
}

创建角色

现在我们已经有了功能,我们需要为系统定义一组角色。这可以通过实现RouteRole接口来实现io.javalin.security.RouteRole。我们将定义三个角色,一个代表“任何人”,一个代表读取用户数据的权限,一个代表写入用户数据的权限。


import io.javalin.security.RouteRole;

enum Role implements RouteRole { ANYONE, USER_READ, USER_WRITE }

设置 API

现在我们有了角色,我们可以实现我们的端点:

import io.javalin.Javalin;
import static io.javalin.apibuilder.ApiBuilder.*;

public class Main {

    public static void main(String[] args) {

        Javalin app = Javalin.create(config -> {
            config.router.mount(router -> {
                router.beforeMatched(Auth::handleAccess);
            }).apiBuilder(() -> {
                get("/", ctx -> ctx.redirect("/users"), Role.ANYONE);
                path("users", () -> {
                    get(UserController::getAllUserIds, Role.ANYONE);
                    post(UserController::createUser, Role.USER_WRITE);
                    path("{userId}", () -> {
                        get(UserController::getUser, Role.USER_READ);
                        patch(UserController::updateUser, Role.USER_WRITE);
                        delete(UserController::deleteUser, Role.USER_WRITE);
                    });
                });
            });
        }).start(7070);

    }
}

现在每个端点都被赋予了一个角色:

  • ANYONE能getAllUserIds
  • USER_READ能getUser
  • USER_WRITE可以createUser,updateUser并且deleteUser

现在,剩下的就是实现访问管理(Auth::handleAccess)。

实现授权

我们的访问管理器的规则很简单:

  • 当端点有时ApiRole.ANYONE,所有请求将被处理
  • 当端点有另一个角色集并且请求具有匹配的凭据时,该请求将被处理
  • 否则,我们将停止请求并401 Unauthorized返回给客户端

这很好地转化为代码:

public static void handleAccess(Context ctx) {
    var permittedRoles = ctx.routeRoles();
    if (permittedRoles.contains(Role.ANYONE)) {
        return; // anyone can access
    }
    if (userRoles(ctx).stream().anyMatch(permittedRoles::contains)) {
        return; // user has role required to access
    }
    ctx.header(Header.WWW_AUTHENTICATE, "Basic");
    throw new UnauthorizedResponse();
}

从上下文中提取用户角色

Javalin 中没有内置的ctx.userRolesuserRoles(ctx),所以我们需要实现一些东西。首先,我们需要一个用户表。我们将创建一个表,map(Pair<String, String>, Set<Role>)其中键是明文形式的用户名+密码(请不要在实际服务中这样做),值是用户角色:

record Pair(String a, String b) {}
private static final Map<Pair, List<Role>> userRolesMap = Map.of(
    new Pair("alice", "weak-1234"), List.of(Role.USER_READ),
    new Pair("bob", "weak-123456"), List.of(Role.USER_READ, Role.USER_WRITE)
);

现在我们有了用户表,我们需要对请求进行身份验证。我们从Basic-auth-header中获取用户名+密码 ,并将它们用作以下键userRoleMap:

public static List<Role> getUserRoles(Context ctx) {
    return Optional.ofNullable(ctx.basicAuthCredentials())
        .map(credentials -> userRolesMap.getOrDefault(new Pair(credentials.getUsername(), credentials.getPassword()), List.of()))
        .orElse(List.of());
}

使用基本身份验证时,凭证将以纯文本形式传输(尽管经过 base64 编码)。 如果您在实际服务中使用基本身份验证,请务必启用 SSL。

结论

就这样!现在您拥有一个包含三个角色的安全 REST API。

作者:Jeebiz  创建时间:2025-05-04 00:18
最后编辑:Jeebiz  更新时间:2025-05-04 00:55