Praktik terbaik untuk otentikasi berbasis token REST dengan JAX-RS dan Jersey

459

Saya mencari cara untuk mengaktifkan otentikasi berbasis token di Jersey. Saya mencoba untuk tidak menggunakan kerangka kerja tertentu. Apakah itu mungkin?

Paket saya adalah: Seorang pengguna mendaftar ke layanan web saya, layanan web saya menghasilkan token, mengirimkannya ke klien, dan klien akan menyimpannya. Kemudian klien, untuk setiap permintaan, akan mengirim token alih-alih nama pengguna dan kata sandi.

Saya berpikir untuk menggunakan filter khusus untuk setiap permintaan dan @PreAuthorize("hasRole('ROLE')") tetapi saya hanya berpikir bahwa ini menyebabkan banyak permintaan ke database untuk memeriksa apakah token itu valid.

Atau tidak membuat filter dan dalam setiap permintaan menempatkan token param? Sehingga setiap API pertama-tama memeriksa token dan setelah mengeksekusi sesuatu untuk mengambil sumber.

DevOps85
sumber

Jawaban:

1388

Cara kerja otentikasi berbasis token

Dalam otentikasi berbasis token, klien bertukar kredensial keras (seperti nama pengguna dan kata sandi) untuk sepotong data yang disebut token . Untuk setiap permintaan, alih-alih mengirim kredensial keras, klien akan mengirim token ke server untuk melakukan otentikasi dan kemudian otorisasi.

Dalam beberapa kata, skema otentikasi berdasarkan token ikuti langkah-langkah ini:

  1. Klien mengirimkan kredensial mereka (nama pengguna dan kata sandi) ke server.
  2. Server mengautentikasi kredensial dan, jika valid, buat token untuk pengguna.
  3. Server menyimpan token yang dibuat sebelumnya di beberapa penyimpanan bersama dengan pengenal pengguna dan tanggal kedaluwarsa.
  4. Server mengirimkan token yang dihasilkan ke klien.
  5. Klien mengirim token ke server di setiap permintaan.
  6. Server, dalam setiap permintaan, mengekstrak token dari permintaan yang masuk. Dengan token, server mencari detail pengguna untuk melakukan otentikasi.
    • Jika token itu valid, server menerima permintaan.
    • Jika token tidak valid, server menolak permintaan tersebut.
  7. Setelah otentikasi dilakukan, server melakukan otorisasi.
  8. Server dapat memberikan titik akhir untuk menyegarkan token.

Catatan: Langkah 3 tidak diperlukan jika server telah mengeluarkan token yang ditandatangani (seperti JWT, yang memungkinkan Anda untuk melakukan otentikasi stateless ).

Apa yang dapat Anda lakukan dengan JAX-RS 2.0 (Jersey, RESTEasy dan Apache CXF)

Solusi ini hanya menggunakan JAX-RS 2.0 API, menghindari solusi spesifik vendor . Jadi, itu harus bekerja dengan implementasi JAX-RS 2.0, seperti Jersey , RESTEasy dan Apache CXF .

Penting untuk menyebutkan bahwa jika Anda menggunakan otentikasi berbasis token, Anda tidak bergantung pada mekanisme keamanan aplikasi web Java EE standar yang ditawarkan oleh wadah servlet dan dapat dikonfigurasi melalui web.xmldeskriptor aplikasi . Ini otentikasi khusus.

Otentikasi pengguna dengan nama pengguna dan kata sandi dan mengeluarkan token

Buat metode sumber daya JAX-RS yang menerima dan memvalidasi kredensial (nama pengguna dan kata sandi) dan mengeluarkan token untuk pengguna:

@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
    }
}

Jika ada pengecualian yang dilemparkan saat memvalidasi kredensial, respons dengan status 403(Terlarang) akan dikembalikan.

Jika kredensial berhasil divalidasi, respons dengan status 200(OK) akan dikembalikan dan token yang diterbitkan akan dikirim ke klien dalam muatan respons. Klien harus mengirim token ke server dalam setiap permintaan.

Saat mengkonsumsi application/x-www-form-urlencoded, klien harus mengirim kredensial dalam format berikut dalam payload permintaan:

username=admin&password=123456

Alih-alih bentuk params, dimungkinkan untuk membungkus nama pengguna dan kata sandi ke dalam kelas:

public class Credentials implements Serializable {

    private String username;
    private String password;

    // Getters and setters omitted
}

Dan kemudian mengkonsumsinya sebagai 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
}

Dengan menggunakan pendekatan ini, klien harus mengirim kredensial dalam format berikut dalam payload permintaan:

{
  "username": "admin",
  "password": "123456"
}

Mengekstraksi token dari permintaan dan memvalidasinya

Klien harus mengirim token di Authorizationheader HTTP standar permintaan. Sebagai contoh:

Authorization: Bearer <token-goes-here>

Nama header HTTP standar sangat disayangkan karena membawa informasi otentikasi , bukan otorisasi . Namun, itu adalah header HTTP standar untuk mengirim kredensial ke server.

JAX-RS menyediakan @NameBinding, meta-anotasi yang digunakan untuk membuat anotasi lain untuk mengikat filter dan interseptor ke kelas dan metode sumber daya. Tetapkan @Securedanotasi sebagai berikut:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }

Anotasi pengikat nama yang didefinisikan di atas akan digunakan untuk menghias kelas filter, yang mengimplementasikan ContainerRequestFilter, memungkinkan Anda untuk mencegat permintaan sebelum ditangani oleh metode sumber daya. The ContainerRequestContextdapat digunakan untuk mengakses header permintaan HTTP dan kemudian ekstrak token:

@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
    }
}

Jika ada masalah yang terjadi selama validasi token, respons dengan status 401(Tidak Diotorisasi) akan dikembalikan. Kalau tidak, permintaan akan dilanjutkan ke metode sumber daya.

Mengamankan titik akhir REST Anda

Untuk mengikat filter otentikasi ke metode sumber daya atau kelas sumber daya, beri anotasi dengan @Securedanotasi yang dibuat di atas. Untuk metode dan / atau kelas yang dianotasi, filter akan dieksekusi. Ini berarti bahwa titik akhir tersebut hanya akan tercapai jika permintaan dilakukan dengan token yang valid.

Jika beberapa metode atau kelas tidak memerlukan otentikasi, cukup jangan membubuhi keterangannya:

@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
        ...
    }
}

Dalam contoh yang ditunjukkan di atas, filter akan dieksekusi hanya untuk mySecuredMethod(Long)metode karena itu dijelaskan dengan @Secured.

Mengidentifikasi pengguna saat ini

Sangat mungkin bahwa Anda perlu mengetahui pengguna yang melakukan permintaan terhadap API REST Anda. Pendekatan berikut dapat digunakan untuk mencapainya:

Mengganti konteks keamanan permintaan saat ini

Dalam ContainerRequestFilter.filter(ContainerRequestContext)metode Anda , SecurityContextcontoh baru dapat ditetapkan untuk permintaan saat ini. Kemudian timpa SecurityContext.getUserPrincipal(), mengembalikan sebuah Principalinstance:

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;
    }
});

Gunakan token untuk mencari pengidentifikasi pengguna (nama pengguna), yang akan menjadi Principalnama.

Suntikkan SecurityContextdalam kelas sumber daya JAX-RS:

@Context
SecurityContext securityContext;

Hal yang sama dapat dilakukan dalam metode sumber daya JAX-RS:

@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id, 
                         @Context SecurityContext securityContext) {
    ...
}

Dan kemudian dapatkan Principal:

Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();

Menggunakan CDI (Injeksi Konteks dan Ketergantungan)

Jika, karena alasan tertentu, Anda tidak ingin mengesampingkannya SecurityContext, Anda dapat menggunakan CDI (Context and Dependency Injection), yang menyediakan fitur berguna seperti acara dan produsen.

Buat kualifikasi CDI:

@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }

Di yang Anda AuthenticationFilterbuat di atas, suntikkan Eventanotasi dengan @AuthenticatedUser:

@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;

Jika otentikasi berhasil, jalankan peristiwa yang melewati nama pengguna sebagai parameter (ingat, token dikeluarkan untuk pengguna dan token akan digunakan untuk mencari pengidentifikasi pengguna):

userAuthenticatedEvent.fire(username);

Sangat mungkin ada kelas yang mewakili pengguna di aplikasi Anda. Sebut kelas ini User.

Buat kacang CDI untuk menangani acara otentikasi, cari Usercontoh dengan nama pengguna koresponden dan tetapkan ke authenticatedUserbidang produsen:

@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
    }
}

The authenticatedUserlapangan menghasilkan Usercontoh yang dapat disuntikkan ke dalam wadah yang dikelola kacang-kacangan, seperti layanan JAX-RS, kacang CDI, servlets dan EJBs. Gunakan potongan kode berikut untuk menyuntikkan Userinstance (sebenarnya, ini adalah proxy CDI):

@Inject
@AuthenticatedUser
User authenticatedUser;

Perhatikan bahwa @Producesanotasi CDI berbeda dari @Producesanotasi JAX-RS :

Pastikan Anda menggunakan @Producesanotasi CDI dalam AuthenticatedUserProducerkacang Anda .

Kuncinya di sini adalah kacang yang dianotasi @RequestScoped, memungkinkan Anda untuk berbagi data antara filter dan kacang Anda. Jika Anda tidak ingin menggunakan acara, Anda dapat memodifikasi filter untuk menyimpan pengguna terotentikasi dalam kacang lingkup permintaan dan kemudian membacanya dari kelas sumber daya JAX-RS Anda.

Dibandingkan dengan pendekatan yang mengesampingkan SecurityContext, pendekatan CDI memungkinkan Anda untuk mendapatkan pengguna terotentikasi dari kacang selain sumber daya dan penyedia JAX-RS.

Mendukung otorisasi berbasis peran

Silakan merujuk ke jawaban saya yang lain untuk perincian tentang bagaimana mendukung otorisasi berbasis peran.

Mengeluarkan token

Token dapat berupa:

  • Buram: Tidak mengungkapkan detail selain dari nilai itu sendiri (seperti string acak)
  • Mandiri: Berisi rincian tentang token itu sendiri (seperti JWT).

Lihat detail di bawah ini:

String acak sebagai token

Token dapat dikeluarkan dengan membuat string acak dan menahannya ke database bersama dengan pengenal pengguna dan tanggal kedaluwarsa. Contoh yang baik tentang cara membuat string acak di Jawa dapat dilihat di sini . Anda juga bisa menggunakan:

Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);

JWT (Token Web JSON)

JWT (JSON Web Token) adalah metode standar untuk mewakili klaim secara aman antara dua pihak dan didefinisikan oleh RFC 7519 .

Ini token yang berdiri sendiri dan memungkinkan Anda untuk menyimpan detail dalam klaim . Klaim ini disimpan dalam token payload yang merupakan JSON yang disandikan sebagai Base64 . Berikut adalah beberapa klaim yang terdaftar di RFC 7519 dan apa artinya (baca RFC lengkap untuk detail lebih lanjut):

  • iss: Kepala sekolah yang menerbitkan token.
  • sub: Kepala sekolah yang menjadi subjek JWT.
  • exp: Tanggal kedaluwarsa untuk token.
  • nbf: Waktu di mana token akan mulai diterima untuk diproses.
  • iat: Waktu saat token diterbitkan.
  • jti: Pengidentifikasi unik untuk token.

Ketahuilah bahwa Anda tidak boleh menyimpan data sensitif, seperti kata sandi, di token.

Payload dapat dibaca oleh klien dan integritas token dapat dengan mudah diperiksa dengan memverifikasi tanda tangannya di server. Tanda tangan inilah yang mencegah token untuk dirusak.

Anda tidak perlu mempertahankan token JWT jika Anda tidak perlu melacaknya. Meskipun demikian, dengan mempertahankan token, Anda akan memiliki kemungkinan membatalkan dan mencabut aksesnya. Untuk tetap melacak token JWT, alih-alih mempertahankan seluruh token di server, Anda dapat tetap menggunakan pengenal token ( jtiklaim) bersama dengan beberapa detail lainnya seperti pengguna yang Anda berikan token, tanggal kedaluwarsa, dll.

Saat mempertahankan token, selalu pertimbangkan menghapus yang lama untuk mencegah basis data Anda tumbuh tanpa batas.

Menggunakan JWT

Ada beberapa perpustakaan Java untuk menerbitkan dan memvalidasi token JWT seperti:

Untuk menemukan sumber daya hebat lainnya untuk bekerja dengan JWT, lihat di http://jwt.io .

Menangani pencabutan token dengan JWT

Jika Anda ingin mencabut token, Anda harus melacaknya. Anda tidak perlu menyimpan seluruh token di sisi server, hanya menyimpan pengenal token (yang harus unik) dan beberapa metadata jika diperlukan. Untuk pengenal token Anda dapat menggunakan UUID .

The jtiklaim harus digunakan untuk menyimpan identifier token token. Saat memvalidasi token, pastikan belum dibatalkan dengan memeriksa nilai jtiklaim terhadap pengenal token yang Anda miliki di sisi server.

Untuk tujuan keamanan, cabut semua token untuk pengguna saat mereka mengubah kata sandi mereka.

Informasi tambahan

  • Tidak masalah jenis autentikasi apa yang Anda putuskan untuk digunakan. Selalu lakukan di atas koneksi HTTPS untuk mencegah serangan man-in-the-middle .
  • Lihatlah pertanyaan ini dari Keamanan Informasi untuk informasi lebih lanjut tentang token.
  • Pada artikel ini Anda akan menemukan beberapa informasi berguna tentang otentikasi berbasis token.
cassiomolin
sumber
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. Bagaimana ini tenang?
scottysseus
3
Otentikasi berbasis Token @scottyseus bekerja dengan cara server mengingat token yang telah dikeluarkannya. Anda dapat menggunakan token JWT untuk otentikasi kewarganegaraan.
cassiomolin
Bagaimana dengan mengirim kata sandi hash bukannya polos (hash dengan nonce yang dihasilkan server)? Apakah ini meningkatkan tingkat keamanan (misalnya ketika tidak menggunakan https)? Dalam kasus seorang pria di tengah - ia akan dapat membajak satu sesi, tetapi setidaknya ia tidak akan mendapatkan kata sandi
Denis Itskovich
15
Saya tidak percaya ini tidak ada dalam dokumentasi resmi.
Daniel M.
2
@grep Di REST, tidak ada yang namanya sesi di sisi server. Akibatnya, status sesi dikelola di sisi klien.
cassiomolin
98

Jawaban ini adalah tentang otorisasi dan merupakan pelengkap dari jawaban saya sebelumnya tentang otentikasi

Mengapa ada jawaban lain ? Saya mencoba memperluas jawaban saya sebelumnya dengan menambahkan detail tentang bagaimana mendukung anotasi JSR-250. Namun jawaban aslinya menjadi terlalu panjang dan melampaui panjang maksimum 30.000 karakter . Jadi saya memindahkan seluruh detail otorisasi ke jawaban ini, menjaga jawaban yang lain fokus pada melakukan otentikasi dan menerbitkan token.


Mendukung otorisasi berbasis peran dengan @Securedanotasi

Selain aliran otentikasi yang ditunjukkan dalam jawaban lain , otorisasi berbasis peran dapat didukung di titik akhir REST.

Buat enumerasi dan tentukan peran sesuai dengan kebutuhan Anda:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

Ubah @Securedanotasi pengikat nama yang dibuat sebelumnya untuk mendukung peran:

@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured {
    Role[] value() default {};
}

Dan kemudian anotasi kelas sumber daya dan metode dengan @Secureduntuk melakukan otorisasi. Anotasi metode akan menggantikan anotasi kelas:

@Path("/example")
@Secured({Role.ROLE_1})
public class ExampleResource {

    @GET
    @Path("{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public Response myMethod(@PathParam("id") Long id) {
        // This method is not annotated with @Secured
        // But it's declared within a class annotated with @Secured({Role.ROLE_1})
        // So it only can be executed by the users who have the ROLE_1 role
        ...
    }

    @DELETE
    @Path("{id}")    
    @Produces(MediaType.APPLICATION_JSON)
    @Secured({Role.ROLE_1, Role.ROLE_2})
    public Response myOtherMethod(@PathParam("id") Long id) {
        // This method is annotated with @Secured({Role.ROLE_1, Role.ROLE_2})
        // The method annotation overrides the class annotation
        // So it only can be executed by the users who have the ROLE_1 or ROLE_2 roles
        ...
    }
}

Buat filter dengan AUTHORIZATIONprioritas, yang dijalankan setelah AUTHENTICATIONfilter prioritas ditentukan sebelumnya.

The ResourceInfodapat digunakan untuk mendapatkan sumber daya Methoddan sumber daya Classyang akan menangani permintaan dan kemudian ekstrak @Securedpenjelasan dari mereka:

@Secured
@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        // Get the resource class which matches with the requested URL
        // Extract the roles declared by it
        Class<?> resourceClass = resourceInfo.getResourceClass();
        List<Role> classRoles = extractRoles(resourceClass);

        // Get the resource method which matches with the requested URL
        // Extract the roles declared by it
        Method resourceMethod = resourceInfo.getResourceMethod();
        List<Role> methodRoles = extractRoles(resourceMethod);

        try {

            // Check if the user is allowed to execute the method
            // The method annotations override the class annotations
            if (methodRoles.isEmpty()) {
                checkPermissions(classRoles);
            } else {
                checkPermissions(methodRoles);
            }

        } catch (Exception e) {
            requestContext.abortWith(
                Response.status(Response.Status.FORBIDDEN).build());
        }
    }

    // Extract the roles from the annotated element
    private List<Role> extractRoles(AnnotatedElement annotatedElement) {
        if (annotatedElement == null) {
            return new ArrayList<Role>();
        } else {
            Secured secured = annotatedElement.getAnnotation(Secured.class);
            if (secured == null) {
                return new ArrayList<Role>();
            } else {
                Role[] allowedRoles = secured.value();
                return Arrays.asList(allowedRoles);
            }
        }
    }

    private void checkPermissions(List<Role> allowedRoles) throws Exception {
        // Check if the user contains one of the allowed roles
        // Throw an Exception if the user has not permission to execute the method
    }
}

Jika pengguna tidak memiliki izin untuk menjalankan operasi, permintaan dibatalkan dengan 403(Dilarang).

Untuk mengetahui pengguna yang melakukan permintaan, lihat jawaban saya sebelumnya . Anda bisa mendapatkannya dari SecurityContext(yang seharusnya sudah diatur dalam ContainerRequestContext) atau menyuntikkannya menggunakan CDI, tergantung pada pendekatan yang Anda gunakan.

Jika @Securedanotasi tidak memiliki peran yang dideklarasikan, Anda dapat mengasumsikan semua pengguna terotentikasi dapat mengakses titik akhir tersebut, mengabaikan peran yang dimiliki pengguna.

Mendukung otorisasi berbasis peran dengan anotasi JSR-250

Atau untuk mendefinisikan peran dalam @Securedanotasi seperti yang ditunjukkan di atas, Anda dapat mempertimbangkan anotasi JSR-250 seperti @RolesAllowed, @PermitAlldan @DenyAll.

JAX-RS tidak mendukung penjelasan seperti itu di luar kebiasaan, tetapi dapat dicapai dengan filter. Berikut adalah beberapa pertimbangan yang perlu diingat jika Anda ingin mendukung semuanya:

Jadi filter otorisasi yang memeriksa anotasi JSR-250 bisa seperti:

@Provider
@Priority(Priorities.AUTHORIZATION)
public class AuthorizationFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {

        Method method = resourceInfo.getResourceMethod();

        // @DenyAll on the method takes precedence over @RolesAllowed and @PermitAll
        if (method.isAnnotationPresent(DenyAll.class)) {
            refuseRequest();
        }

        // @RolesAllowed on the method takes precedence over @PermitAll
        RolesAllowed rolesAllowed = method.getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
            return;
        }

        // @PermitAll on the method takes precedence over @RolesAllowed on the class
        if (method.isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // @DenyAll can't be attached to classes

        // @RolesAllowed on the class takes precedence over @PermitAll on the class
        rolesAllowed = 
            resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class);
        if (rolesAllowed != null) {
            performAuthorization(rolesAllowed.value(), requestContext);
        }

        // @PermitAll on the class
        if (resourceInfo.getResourceClass().isAnnotationPresent(PermitAll.class)) {
            // Do nothing
            return;
        }

        // Authentication is required for non-annotated methods
        if (!isAuthenticated(requestContext)) {
            refuseRequest();
        }
    }

    /**
     * Perform authorization based on roles.
     *
     * @param rolesAllowed
     * @param requestContext
     */
    private void performAuthorization(String[] rolesAllowed, 
                                      ContainerRequestContext requestContext) {

        if (rolesAllowed.length > 0 && !isAuthenticated(requestContext)) {
            refuseRequest();
        }

        for (final String role : rolesAllowed) {
            if (requestContext.getSecurityContext().isUserInRole(role)) {
                return;
            }
        }

        refuseRequest();
    }

    /**
     * Check if the user is authenticated.
     *
     * @param requestContext
     * @return
     */
    private boolean isAuthenticated(final ContainerRequestContext requestContext) {
        // Return true if the user is authenticated or false otherwise
        // An implementation could be like:
        // return requestContext.getSecurityContext().getUserPrincipal() != null;
    }

    /**
     * Refuse the request.
     */
    private void refuseRequest() {
        throw new AccessDeniedException(
            "You don't have permissions to perform this action.");
    }
}

Catatan: Implementasi di atas didasarkan pada Jersey RolesAllowedDynamicFeature. Jika Anda menggunakan Jersey, Anda tidak perlu menulis filter sendiri, cukup gunakan implementasi yang ada.

cassiomolin
sumber
Apakah ada repositori github dengan solusi elegan ini?
Daniel Ferreira Castro
6
@DanielFerreiraCastro Tentu saja. Silahkan lihat di sini .
cassiomolin
Apakah ada cara yang baik untuk memvalidasi bahwa permintaan berasal dari pengguna yang berwenang DAN bahwa pengguna BISA mengubah data karena ia "memiliki" data (mis. Sehingga peretas tidak dapat menggunakan tokennya untuk mengubah nama pengguna lain)? Saya tahu saya bisa memeriksa di setiap titik akhir jika user_id== token.userId, atau sesuatu seperti itu, tapi ini sangat berulang.
mFeinstein
@ mFeinstein Jawaban untuk itu pasti akan membutuhkan lebih banyak karakter daripada yang bisa saya ketik di komentar Hanya untuk memberi Anda arahan, Anda bisa mencari keamanan tingkat baris .
cassiomolin
Saya bisa melihat banyak topik di database ketika saya mencari keamanan tingkat baris, saya akan membuka ini sebagai pertanyaan baru
mFeinstein