แนวปฏิบัติที่เหมาะสมที่สุดสำหรับการพิสูจน์ตัวตนโทเค็น REST ด้วย JAX-RS และ Jersey


459

ฉันกำลังมองหาวิธีการเปิดใช้งานการรับรองความถูกต้องใช้โทเค็นในเจอร์ซีย์ ฉันพยายามไม่ใช้เฟรมเวิร์กเฉพาะใด ๆ เป็นไปได้ไหม

แผนของฉันคือ: ผู้ใช้สมัครใช้บริการเว็บของฉันบริการเว็บของฉันสร้างโทเค็นส่งไปยังลูกค้าและลูกค้าจะเก็บไว้ จากนั้นลูกค้าสำหรับแต่ละคำขอจะส่งโทเค็นแทนชื่อผู้ใช้และรหัสผ่าน

ฉันคิดว่าจะใช้ตัวกรองที่กำหนดเองสำหรับแต่ละคำขอ@PreAuthorize("hasRole('ROLE')") แต่ฉันคิดว่านี่เป็นสาเหตุทำให้มีคำขอจำนวนมากไปยังฐานข้อมูลเพื่อตรวจสอบว่าโทเค็นนั้นถูกต้องหรือไม่

หรือไม่สร้างตัวกรองและในแต่ละคำขอจะใส่โทเค็นพารามิเตอร์หรือไม่ เพื่อให้แต่ละ API ตรวจสอบโทเค็นก่อนและหลังจากดำเนินการบางอย่างเพื่อดึงทรัพยากร

คำตอบ:


1388

การรับรองความถูกต้องตามโทเค็นทำงานอย่างไร

ในการตรวจสอบ token-based, การแลกเปลี่ยนลูกค้าข้อมูลประจำตัวยาก (เช่นชื่อผู้ใช้และรหัสผ่าน) สำหรับชิ้นส่วนของข้อมูลที่เรียกว่าโทเค็น สำหรับแต่ละคำขอแทนการส่งข้อมูลประจำตัวที่ยากลูกค้าจะส่งโทเค็นไปยังเซิร์ฟเวอร์เพื่อดำเนินการตรวจสอบและจากนั้นการอนุญาต

กล่าวสองสามคำว่ารูปแบบการรับรองความถูกต้องซึ่งอิงตามโทเค็นทำตามขั้นตอนเหล่านี้:

  1. ไคลเอนต์ส่งข้อมูลประจำตัวของพวกเขา (ชื่อผู้ใช้และรหัสผ่าน) ไปยังเซิร์ฟเวอร์
  2. เซิร์ฟเวอร์ตรวจสอบข้อมูลประจำตัวและหากถูกต้องให้สร้างโทเค็นสำหรับผู้ใช้
  3. เซิร์ฟเวอร์จัดเก็บโทเค็นที่สร้างไว้ก่อนหน้านี้ในที่เก็บข้อมูลบางส่วนพร้อมกับตัวระบุผู้ใช้และวันที่หมดอายุ
  4. เซิร์ฟเวอร์ส่งโทเค็นที่สร้างขึ้นไปยังลูกค้า
  5. ไคลเอ็นต์ส่งโทเค็นไปยังเซิร์ฟเวอร์ในแต่ละคำขอ
  6. เซิร์ฟเวอร์ในแต่ละคำขอแยกโทเค็นออกจากคำขอที่เข้ามา ด้วยโทเค็นเซิร์ฟเวอร์จะค้นหารายละเอียดผู้ใช้เพื่อดำเนินการตรวจสอบสิทธิ์
    • หากโทเค็นนั้นถูกต้องเซิร์ฟเวอร์จะยอมรับการร้องขอ
    • หากโทเค็นไม่ถูกต้องเซิร์ฟเวอร์ปฏิเสธคำขอ
  7. เมื่อทำการตรวจสอบแล้วเซิร์ฟเวอร์จะทำการตรวจสอบความถูกต้อง
  8. เซิร์ฟเวอร์สามารถให้ปลายทางเพื่อรีเฟรชโทเค็น

หมายเหตุ:ไม่จำเป็นต้องใช้ขั้นตอนที่ 3 หากเซิร์ฟเวอร์ออกโทเค็นที่ลงชื่อแล้ว (เช่น JWT ซึ่งอนุญาตให้คุณทำสถานะไร้สัญชาติ)พิสูจน์ตัวตนแบบ )

คุณสามารถทำอะไรกับ JAX-RS 2.0 (Jersey, RESTEasy และ Apache CXF)

การแก้ปัญหานี้ใช้เฉพาะ JAX-RS 2.0 API, หลีกเลี่ยงการแก้ปัญหาผู้ขายที่เฉพาะเจาะจงใดดังนั้นจึงควรจะทำงานกับ JAX-RS 2.0 การใช้งานเช่นย์ , RESTEasyและApache CXF

มีประโยชน์ที่จะกล่าวถึงว่าถ้าคุณใช้การรับรองความถูกต้องด้วยโทเค็นคุณจะไม่ต้องพึ่งพากลไกความปลอดภัยเว็บแอปพลิเคชัน Java EE มาตรฐานที่นำเสนอโดยคอนเทนเนอร์ servlet และกำหนดค่าผ่านแอปพลิเคชัน web.xmlบ่ง มันเป็นการตรวจสอบที่กำหนดเอง

ตรวจสอบสิทธิ์ผู้ใช้ด้วยชื่อผู้ใช้และรหัสผ่านและออกโทเค็น

สร้างวิธีการทรัพยากร JAX-RS ซึ่งรับและตรวจสอบข้อมูลรับรอง (ชื่อผู้ใช้และรหัสผ่าน) และออกโทเค็นสำหรับผู้ใช้:

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

หากมีข้อผิดพลาดเกิดขึ้นเมื่อตรวจสอบข้อมูลรับรอง403จะมีการส่งคืนการตอบกลับด้วยสถานะ(ต้องห้าม)

หากการตรวจสอบความถูกต้องของข้อมูลรับรองสำเร็จการตอบกลับด้วยสถานะ200(OK) จะถูกส่งคืนและโทเค็นที่ออกจะถูกส่งไปยังลูกค้าในส่วนของข้อมูลการตอบสนอง ลูกค้าจะต้องส่งโทเค็นไปยังเซิร์ฟเวอร์ในทุกคำขอ

เมื่อบริโภคapplication/x-www-form-urlencodedลูกค้าต้องส่งข้อมูลประจำตัวในรูปแบบต่อไปนี้ในส่วนของคำขอ:

username=admin&password=123456

แทนที่จะเป็นแบบฟอร์ม params คุณสามารถใส่ชื่อผู้ใช้และรหัสผ่านในคลาสได้:

public class Credentials implements Serializable {

    private String username;
    private String password;

    // Getters and setters omitted
}

จากนั้นใช้เป็น 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
}

ใช้วิธีการนี้ลูกค้าจะต้องส่งข้อมูลประจำตัวในรูปแบบต่อไปนี้ในส่วนของคำขอ:

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

แยกโทเค็นออกจากคำขอและตรวจสอบความถูกต้อง

ลูกค้าควรส่งโทเค็นในAuthorizationส่วนหัวHTTP มาตรฐานของคำขอ ตัวอย่างเช่น:

Authorization: Bearer <token-goes-here>

ชื่อของหัว HTTP มาตรฐานคือโชคร้ายเพราะจะดำเนินการตรวจสอบข้อมูลไม่ได้อนุมัติ อย่างไรก็ตามเป็นส่วนหัว HTTP มาตรฐานสำหรับการส่งข้อมูลรับรองไปยังเซิร์ฟเวอร์

JAX-RS จัดเตรียม@NameBindingคำอธิบายประกอบที่ใช้ในการสร้างคำอธิบายประกอบอื่น ๆ เพื่อเชื่อมโยงตัวกรองและตัวดักกับคลาสทรัพยากรและวิธีการ กำหนด@Securedคำอธิบายประกอบดังต่อไปนี้:

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

คำอธิบายประกอบการผูกชื่อที่กำหนดไว้ข้างต้นจะใช้ในการตกแต่งคลาสตัวกรองซึ่งดำเนินการContainerRequestFilterช่วยให้คุณสามารถดักจับการร้องขอก่อนที่จะได้รับการจัดการโดยวิธีการทรัพยากร ContainerRequestContextสามารถนำมาใช้ในการเข้าถึงส่วนหัวคำขอ HTTP แล้วแยกโทเค็น:

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

หากมีปัญหาใด ๆ เกิดขึ้นระหว่างการตรวจสอบความถูกต้องของโทเค็นการตอบกลับด้วยสถานะ401(ไม่ได้รับอนุญาต) จะถูกส่งคืน มิฉะนั้นคำขอจะดำเนินการตามวิธีการทรัพยากร

รักษาจุดปลาย REST ของคุณให้ปลอดภัย

ในการผูกตัวกรองการพิสูจน์ตัวตนเข้ากับวิธีการของทรัพยากรหรือคลาสทรัพยากรให้ทำหมายเหตุประกอบด้วย@Securedคำอธิบายประกอบที่สร้างขึ้นด้านบน สำหรับเมธอดและ / หรือคลาสที่มีหมายเหตุประกอบตัวกรองจะถูกเรียกใช้งาน ก็หมายความว่าปลายทางดังกล่าวจะเพียง แต่จะมาถึงถ้าขอจะดำเนินการกับโทเค็นที่ถูกต้อง

หากวิธีการบางอย่างหรือคลาสไม่ต้องการการรับรองความถูกต้องก็ไม่ต้องใส่คำอธิบายประกอบ:

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

ในตัวอย่างที่แสดงข้างต้นตัวกรองจะถูกดำเนินการเฉพาะสำหรับวิธีการเพราะข้อเขียนด้วยmySecuredMethod(Long)@Secured

การระบุผู้ใช้ปัจจุบัน

มีโอกาสมากที่คุณจะต้องรู้ว่าผู้ใช้ที่ดำเนินการตามคำขอเป็นไปตาม REST API ของคุณหรือไม่ วิธีการต่อไปนี้สามารถใช้เพื่อให้บรรลุ:

การแทนที่บริบทความปลอดภัยของคำขอปัจจุบัน

ภายในContainerRequestFilter.filter(ContainerRequestContext)วิธีการของคุณSecurityContextสามารถตั้งค่าอินสแตนซ์ใหม่สำหรับคำขอปัจจุบันได้ จากนั้นแทนที่SecurityContext.getUserPrincipal(), ส่งคืนPrincipalอินสแตนซ์:

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

ใช้โทเค็นเพื่อค้นหาตัวระบุผู้ใช้ (ชื่อผู้ใช้) ซึ่งจะเป็นPrincipalชื่อของ

ฉีดเข้าไปSecurityContextในคลาสรีซอร์ส JAX-RS:

@Context
SecurityContext securityContext;

สิ่งเดียวกันสามารถทำได้ในวิธีการทรัพยากร JAX-RS:

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

แล้วรับPrincipal:

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

ใช้ CDI (การฉีดตามบริบทและพึ่งพา)

หากด้วยเหตุผลบางอย่างคุณไม่ต้องการลบล้างSecurityContextคุณสามารถใช้ CDI (Context and Dependency Injection) ซึ่งมีคุณสมบัติที่มีประโยชน์เช่นเหตุการณ์และผู้ผลิต

สร้างตัวระบุ CDI:

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

ในสิ่งที่คุณAuthenticationFilterสร้างไว้ด้านบนให้แทรกEventคำอธิบายประกอบด้วย@AuthenticatedUser:

@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;

หากการรับรองความถูกต้องสำเร็จให้ดำเนินการเหตุการณ์ผ่านชื่อผู้ใช้เป็นพารามิเตอร์ (โปรดจำไว้ว่าโทเค็นจะออกให้กับผู้ใช้และโทเค็นจะถูกใช้เพื่อค้นหาตัวระบุผู้ใช้):

userAuthenticatedEvent.fire(username);

มีโอกาสมากที่จะมีคลาสที่แสดงถึงผู้ใช้ในแอปพลิเคชันของคุณ เรียกคลาสนี้Userกัน

สร้าง CDI bean เพื่อจัดการเหตุการณ์การพิสูจน์ตัวตนค้นหา Userอินสแตนซ์ด้วยชื่อผู้ใช้ที่สอดคล้องกันและกำหนดให้กับauthenticatedUserฟิลด์ผู้สร้าง:

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

authenticatedUserฟิลด์ผลิตUserอินสแตนซ์ที่สามารถฉีดลงในภาชนะที่มีการจัดการถั่วเช่นบริการ JAX-RS, ถั่ว CDI, Servlets และ EJBs ใช้รหัสต่อไปนี้เพื่อฉีดUserอินสแตนซ์ (อันที่จริงเป็นพร็อกซี CDI):

@Inject
@AuthenticatedUser
User authenticatedUser;

โปรดทราบว่า@ProducesคำอธิบายประกอบCDI นั้นแตกต่างจาก JAX-RS@Producesคำอธิบายประกอบ :

ให้แน่ใจว่าคุณใช้ CDI @ProducesคำอธิบายประกอบในAuthenticatedUserProducerถั่วของคุณ

กุญแจสำคัญในที่นี้คือถั่วที่มีคำอธิบายประกอบ @RequestScopedช่วยให้คุณสามารถแบ่งปันข้อมูลระหว่างตัวกรองและถั่วของคุณ หากคุณไม่ต้องการใช้เหตุการณ์คุณสามารถแก้ไขตัวกรองเพื่อจัดเก็บผู้ใช้ที่ได้รับการรับรองความถูกต้องในขอบเขตการร้องขอจากนั้นอ่านจากคลาสทรัพยากร JAX-RS ของคุณ

เมื่อเปรียบเทียบกับวิธีการที่แทนที่ SecurityContext CDI จะช่วยให้คุณได้รับผู้ใช้ที่ผ่านการตรวจสอบสิทธิ์จากถั่วนอกเหนือจากทรัพยากร JAX-RS และผู้ให้บริการ

สนับสนุนการอนุญาตตามบทบาท

โปรดอ้างอิงคำตอบอื่น ๆ ของฉันสำหรับรายละเอียดเกี่ยวกับวิธีการสนับสนุนการอนุญาตตามบทบาท

การออกโทเค็น

โทเค็นสามารถ:

  • ทึบแสง:ไม่เปิดเผยรายละเอียดใด ๆ นอกจากค่าตัวเอง (เช่นสตริงสุ่ม)
  • อยู่ในตัวเอง: มีรายละเอียดเกี่ยวกับโทเค็นตัวเอง (เช่น JWT)

ดูรายละเอียดด้านล่าง:

สตริงสุ่มเป็นโทเค็น

โทเค็นสามารถออกได้โดยการสร้างสตริงแบบสุ่มและเก็บไว้ในฐานข้อมูลพร้อมกับตัวระบุผู้ใช้และวันที่หมดอายุ เป็นตัวอย่างที่ดีของวิธีการสร้างสตริงแบบสุ่มในชวาสามารถมองเห็นได้ที่นี่ คุณสามารถใช้:

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

JWT (โทเค็นเว็บ JSON)

JWT (JSON Web Token) เป็นวิธีมาตรฐานสำหรับการอ้างสิทธิ์อย่างปลอดภัยระหว่างสองฝ่ายและกำหนดโดยRFC 75197519

เป็นโทเค็นในตัวและช่วยให้คุณสามารถเก็บรายละเอียดในการอ้างสิทธิ์ได้ การเรียกร้องเหล่านี้จะถูกเก็บไว้ในส่วนของข้อมูลโทเค็นซึ่งเป็น JSON เข้ารหัสเป็นBase64 นี่คือการอ้างสิทธิ์บางส่วนที่ลงทะเบียนในRFC 7519และสิ่งที่พวกเขาหมายถึง (อ่าน RFC เต็มสำหรับรายละเอียดเพิ่มเติม):

  • iss: อาจารย์ใหญ่ที่ออกโทเค็น
  • sub: อาจารย์ใหญ่ที่เป็นเรื่องของ JWT
  • exp: วันที่หมดอายุสำหรับโทเค็น
  • nbf: เวลาที่โทเค็นจะเริ่มได้รับการยอมรับสำหรับการประมวลผล
  • iat: เวลาที่ออกโทเค็น
  • jti: ตัวระบุที่ไม่ซ้ำกันสำหรับโทเค็น

ระวังว่าคุณจะต้องไม่เก็บข้อมูลที่สำคัญเช่นรหัสผ่านไว้ในโทเค็น

เพย์โหลดสามารถอ่านได้โดยไคลเอนต์และสามารถตรวจสอบความสมบูรณ์ของโทเค็นได้อย่างง่ายดายโดยการตรวจสอบลายเซ็นบนเซิร์ฟเวอร์ ลายเซ็นคือสิ่งที่ป้องกันไม่ให้โทเค็นถูกดัดแปลง

คุณไม่จำเป็นต้องยืนยันโทเค็น JWT หากคุณไม่ต้องการติดตามพวกเขา แม้ว่าจะมีการใช้โทเค็นอย่างต่อเนื่องคุณจะมีโอกาสที่จะทำให้โมฆะและเพิกถอนการเข้าถึงเหล่านั้นได้ ในการติดตามโทเค็น JWT แทนที่จะเก็บโทเค็นทั้งหมดไว้บนเซิร์ฟเวอร์คุณสามารถคงรหัสโทเค็นไว้ได้ (jtiอ้างสิทธิ์) ไว้พร้อมกับรายละเอียดอื่น ๆ เช่นผู้ใช้ที่คุณออกโทเค็นวันหมดอายุเป็นต้น

เมื่อยังคงมีโทเค็นอยู่ให้พิจารณาลบอันเก่าออกเพื่อป้องกันฐานข้อมูลของคุณเพิ่มขึ้นเรื่อย ๆ

ใช้ JWT

มีไลบรารี Java จำนวนหนึ่งที่จะออกและตรวจสอบโทเค็น JWT เช่น:

เพื่อหาแหล่งข้อมูลที่ดีบางส่วนอื่น ๆ ที่จะทำงานร่วมกับ JWT, มีลักษณะที่http://jwt.io

การจัดการการเพิกถอนโทเค็นด้วย JWT

หากคุณต้องการเพิกถอนโทเค็นคุณต้องติดตามพวกเขา คุณไม่จำเป็นต้องเก็บโทเค็นทั้งหมดที่ฝั่งเซิร์ฟเวอร์เก็บเฉพาะตัวระบุโทเค็น (ที่ต้องไม่ซ้ำกัน) และเมตาดาต้าบางส่วนหากคุณต้องการ สำหรับตัวระบุโทเค็นคุณสามารถใช้UUID UUID

jtiควรใช้การอ้างสิทธิ์เพื่อจัดเก็บตัวระบุโทเค็นบนโทเค็น เมื่อตรวจสอบโทเค็นตรวจสอบให้แน่ใจว่าไม่ได้ถูกเพิกถอนโดยตรวจสอบค่าของการjtiอ้างสิทธิ์กับตัวบ่งชี้โทเค็นที่คุณมีในฝั่งเซิร์ฟเวอร์

เพื่อความปลอดภัยเพิกถอนโทเค็นทั้งหมดสำหรับผู้ใช้เมื่อพวกเขาเปลี่ยนรหัสผ่าน

ข้อมูลเพิ่มเติม

  • ไม่สำคัญว่าคุณจะใช้การตรวจสอบความถูกต้องประเภทใด มักจะทำมันอยู่ด้านบนของการเชื่อมต่อ HTTPS เพื่อป้องกันไม่ให้มนุษย์ในกลางโจมตีมนุษย์ในกลางโจมตี
  • ดูคำถามนี้จากความปลอดภัยของข้อมูลสำหรับข้อมูลเพิ่มเติมเกี่ยวกับโทเค็น
  • ในบทความนี้คุณจะพบข้อมูลที่เป็นประโยชน์เกี่ยวกับการรับรองความถูกต้องตามโทเค็น

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. RESTful นี้เป็นอย่างไร
scottysseus

3
@scottyseus Token รับรองความถูกต้องทำงานโดยวิธีการที่เซิร์ฟเวอร์จำโทเค็นที่ได้รับการออก คุณสามารถใช้โทเค็น JWT สำหรับการตรวจสอบสิทธิ์แบบไม่มีรัฐ
cassiomolin

สิ่งที่เกี่ยวกับการส่งรหัสผ่านแฮชแทนธรรมดา (แฮชกับ nonce สร้างเซิร์ฟเวอร์) มันเพิ่มระดับความปลอดภัย (เช่นเมื่อไม่ใช้ https) หรือไม่ ในกรณีของคนที่อยู่ตรงกลาง - เขาจะสามารถจี้หนึ่งเซสชั่น แต่อย่างน้อยเขาจะไม่ได้รับรหัสผ่าน
เดนิส Itskovich

15
ฉันไม่อยากจะเชื่อเลยว่านี่ไม่ใช่เอกสารทางการ
Daniel M.

2
@grep ใน REST ไม่มีสิ่งใดเป็นเซสชันในฝั่งเซิร์ฟเวอร์ ดังนั้นสถานะเซสชันถูกจัดการบนฝั่งไคลเอ็นต์
cassiomolin

98

คำตอบนี้เกี่ยวกับการให้สิทธิ์และเป็นคำตอบที่สมบูรณ์ของคำตอบก่อนหน้าเกี่ยวกับการรับรองความถูกต้อง

ทำไมต้องมีคำตอบอื่นอีก ? ฉันพยายามขยายคำตอบก่อนหน้าโดยเพิ่มรายละเอียดเกี่ยวกับวิธีการสนับสนุนคำอธิบายประกอบ JSR-250 แต่คำตอบเดิมกลายเป็นทางยาวเกินไปและเกินความยาวสูงสุด 30,000 ตัวอักษร ดังนั้นฉันจึงย้ายรายละเอียดการอนุญาตทั้งหมดไปที่คำตอบนี้ทำให้คำตอบอื่น ๆ มุ่งเน้นไปที่การตรวจสอบความถูกต้องและการออกโทเค็น


สนับสนุนการอนุมัติตามบทบาทที่มี@Securedคำอธิบายประกอบ

นอกเหนือจากโฟลว์การพิสูจน์ตัวตนที่แสดงในคำตอบอื่น ๆแล้วการอนุญาตตามบทบาทสามารถได้รับการสนับสนุนในจุดปลาย REST

สร้างการแจงนับและกำหนดบทบาทตามความต้องการของคุณ:

public enum Role {
    ROLE_1,
    ROLE_2,
    ROLE_3
}

เปลี่ยน@Securedคำอธิบายประกอบการผูกชื่อที่สร้างขึ้นก่อนหน้านี้เพื่อสนับสนุนบทบาท:

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

จากนั้นใส่คำอธิบายประกอบคลาสทรัพยากรและวิธีการด้วย@Securedเพื่อดำเนินการอนุญาต คำอธิบายประกอบวิธีการจะแทนที่คำอธิบายประกอบชั้นเรียน:

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

สร้างตัวกรองด้วย AUTHORIZATIONลำดับความสำคัญซึ่งดำเนินการหลังจากAUTHENTICATIONตัวกรองลำดับความสำคัญที่กำหนดไว้ก่อนหน้า

ResourceInfoสามารถนำมาใช้เพื่อให้ได้ทรัพยากรMethodและทรัพยากรClassที่จะจัดการการร้องขอแล้วดึง@Securedคำอธิบายประกอบจากพวกเขา:

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

หากผู้ใช้ไม่ได้รับอนุญาตให้ดำเนินการตามคำขอจะถูกยกเลิกด้วย403(ต้องห้าม)

หากต้องการทราบว่าผู้ใช้ที่มีประสิทธิภาพการร้องขอให้ดูคำตอบก่อนหน้าของฉัน คุณสามารถรับได้จากSecurityContext (ซึ่งควรตั้งไว้แล้วในContainerRequestContext) หรือฉีดโดยใช้ CDI ขึ้นอยู่กับวิธีการที่คุณไป

หาก@Securedคำอธิบายประกอบไม่มีบทบาทที่ประกาศไว้คุณสามารถสันนิษฐานได้ว่าผู้ใช้ที่ได้รับการรับรองความถูกต้องทุกคนสามารถเข้าถึงจุดปลายทางนั้นโดยไม่คำนึงถึงบทบาทที่ผู้ใช้มี

สนับสนุนการอนุญาตตามบทบาทด้วยหมายเหตุประกอบ JSR-250

อีกทางเลือกหนึ่งที่จะกำหนดบทบาทสำคัญในการที่@Securedคำอธิบายประกอบที่แสดงข้างต้นคุณสามารถพิจารณา JSR-250 คำอธิบายประกอบเช่น@RolesAllowed, @PermitAllและ@DenyAllและ

JAX-RS ไม่สนับสนุนหมายเหตุประกอบนอกกรอบ แต่สามารถทำได้ด้วยตัวกรอง ต่อไปนี้เป็นข้อควรพิจารณาบางประการที่คุณควรคำนึงถึงหากคุณต้องการสนับสนุนพวกเขาทั้งหมด:

  • @DenyAllในวิธีการที่จะมีความสำคัญมากกว่า@RolesAllowedและ@PermitAllในชั้นเรียน
  • @RolesAllowedในวิธีการที่จะมีความสำคัญมากกว่า@PermitAllในชั้นเรียน
  • @PermitAllในวิธีการที่จะมีความสำคัญมากกว่า@RolesAllowedในชั้นเรียน
  • @DenyAll ไม่สามารถแนบกับคลาสได้
  • @RolesAllowedในชั้นเรียนจะมีความสำคัญมากกว่า@PermitAllในชั้นเรียน

ดังนั้นตัวกรองการอนุญาตที่ตรวจสอบคำอธิบายประกอบ JSR-250 อาจเป็น:

@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.");
    }
}

หมายเหตุ:RolesAllowedDynamicFeatureการดำเนินการดังกล่าวข้างต้นจะขึ้นอยู่กับนิวเจอร์ซีย์ หากคุณใช้ Jersey คุณไม่จำเป็นต้องเขียนตัวกรองของคุณเองเพียงใช้การดำเนินการที่มีอยู่


มีพื้นที่เก็บข้อมูล github ใด ๆ ด้วยโซลูชั่นที่สง่างามนี้สามารถใช้ได้?
Daniel Ferreira Castro

6
@DanielFerreiraCastro แน่นอน ได้ดูที่นี่
cassiomolin

มีวิธีที่ดีหรือไม่ในการตรวจสอบว่าคำขอมาจากผู้ใช้ที่ได้รับอนุญาตและผู้ใช้นั้นสามารถเปลี่ยนข้อมูลได้เพราะเขา "เป็นเจ้าของข้อมูล" (เช่นแฮกเกอร์ไม่สามารถใช้โทเค็นของเขาเพื่อเปลี่ยนชื่อของผู้ใช้รายอื่น) ฉันรู้ว่าฉันสามารถตรวจสอบที่จุดสิ้นสุดทุกจุดว่าuser_id== token.userIdหรืออะไรทำนองนั้น แต่นี่เป็นการทำซ้ำ ๆ
mFeinstein

@mFeinstein คำตอบสำหรับเรื่องนี้ต้องใช้ตัวอักษรมากกว่าที่ฉันสามารถพิมพ์ได้ที่นี่ในความคิดเห็น เพียงเพื่อให้คุณทิศทางบางอย่างที่คุณสามารถมองหาการรักษาความปลอดภัยระดับแถว
cassiomolin

ฉันสามารถดูหัวข้อต่าง ๆ มากมายในฐานข้อมูลเมื่อฉันค้นหาความปลอดภัยระดับแถวฉันจะเปิดคำถามนี้เป็นคำถามใหม่แล้ว
mFeinstein
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.