[java] JAX-RS 및 Jersey를 사용한 REST 토큰 기반 인증 모범 사례

Jersey에서 토큰 기반 인증을 활성화하는 방법을 찾고 있습니다. 특정 프레임 워크를 사용하지 않으려 고합니다. 가능합니까?

내 계획은 다음과 같습니다. 사용자가 내 웹 서비스에 가입하면 내 웹 서비스가 토큰을 생성하여 클라이언트로 보내고 클라이언트는이를 유지합니다. 그런 다음 클라이언트는 각 요청에 대해 사용자 이름과 비밀번호 대신 토큰을 보냅니다.

각 요청에 대해 사용자 정의 필터를 사용하려고 생각 @PreAuthorize("hasRole('ROLE')")
했지만 데이터베이스에 대한 많은 요청이 토큰이 유효한지 확인한다고 생각했습니다.

아니면 필터를 만들지 않고 각 요청에 매개 변수 토큰을 넣습니까? 따라서 각 API는 먼저 토큰을 확인한 후 리소스를 검색하기 위해 무언가를 실행합니다.



답변

토큰 기반 인증 작동 방식

토큰 기반 인증에서 클라이언트는 하드 자격 증명 (예 : 사용자 이름 및 비밀번호)을 token 이라는 데이터 조각으로 교환 합니다 . 각 요청에 대해 하드 자격 증명을 보내는 대신 클라이언트는 서버로 토큰을 보내 인증 및 권한 부여를 수행합니다.

간단히 말해서, 토큰 기반 인증 체계는 다음 단계를 따릅니다.

  1. 클라이언트는 자격 증명 (사용자 이름 및 비밀번호)을 서버로 보냅니다.
  2. 서버는 자격 증명을 인증하고 유효한 경우 사용자에 대한 토큰을 생성합니다.
  3. 서버는 이전에 생성 된 토큰을 사용자 식별자 및 만료 날짜와 함께 일부 저장소에 저장합니다.
  4. 서버는 생성 된 토큰을 클라이언트로 보냅니다.
  5. 클라이언트는 각 요청에서 서버로 토큰을 보냅니다.
  6. 각 요청에서 서버는 들어오는 요청에서 토큰을 추출합니다. 토큰으로 서버는 인증을 수행하기 위해 사용자 세부 사항을 찾습니다.
    • 토큰이 유효하면 서버는 요청을 수락합니다.
    • 토큰이 유효하지 않으면 서버는 요청을 거부합니다.
  7. 인증이 수행되면 서버는 인증을 수행합니다.
  8. 서버는 엔드 포인트를 제공하여 토큰을 새로 고칠 수 있습니다.

참고 : 서버가 서명 된 토큰 (예 : JWT와 같이 상태 비 저장 인증 을 수행 할 수있는)을 발행 한 경우 3 단계는 필요하지 않습니다 .

JAX-RS 2.0으로 수행 할 수있는 작업 (Jersey, RESTEasy 및 Apache CXF)

이 솔루션은 공급 업체별 솔루션을 피하면서 JAX-RS 2.0 API 만 사용합니다 . 따라서 Jersey , RESTEasyApache CXF 와 같은 JAX-RS 2.0 구현에서 작동해야합니다 .

토큰 기반 인증을 사용하는 경우 서블릿 컨테이너가 제공하고 응용 프로그램 web.xml설명자를 통해 구성 할 수있는 표준 Java EE 웹 응용 프로그램 보안 메커니즘에 의존하지 않는 것이 좋습니다 . 맞춤 인증입니다.

사용자 이름과 비밀번호로 사용자 인증 및 토큰 발행

신임 정보 (사용자 이름 및 비밀번호)를 수신하고 유효성을 검증하고 사용자에 대한 토큰을 발행하는 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

폼 매개 변수 대신 사용자 이름과 암호를 클래스로 래핑 할 수 있습니다.

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

요청에서 토큰 추출 및 유효성 검증

클라이언트는 요청의 표준 HTTP Authorization헤더로 토큰을 보내야합니다 . 예를 들면 다음과 같습니다.

Authorization: Bearer <token-goes-here>

표준 HTTP 헤더의 이름은 권한 부여가 아닌 인증 정보를 전달하기 때문에 불행합니다 . 그러나 자격 증명을 서버로 보내기위한 표준 HTTP 헤더입니다.

JAX-RS는 @NameBinding필터 및 인터셉터를 자원 클래스 및 메소드에 바인드하기 위해 다른 주석을 작성하는 데 사용되는 메타 주석 인을 제공 합니다. @Secured다음과 같이 주석을 정의하십시오 .

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

위에서 정의한 이름 바인딩 어노테이션은 필터 클래스를 장식하는 데 사용됩니다.이 클래스는을 구현 ContainerRequestFilter하여 요청이 자원 메소드에 의해 처리되기 전에 인터셉트 할 수 있도록합니다. 는 ContainerRequestContextHTTP 요청 헤더에 액세스 한 후 토큰을 추출하는 데 사용할 수 있습니다 :

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

SecurityContextJAX-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 (컨텍스트 및 종속성 주입)를 사용할 수 있습니다.

CDI 규정자를 작성하십시오.

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

당신이에서 AuthenticationFilter주입, 위에서 만든 Event주석 @AuthenticatedUser:

@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;

인증이 성공하면 username을 매개 변수로 전달하여 이벤트를 발생시킵니다 (사용자에게 토큰이 발급되고 토큰이 사용자 식별자를 찾는 데 사용됨).

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필드는 UserJAX-RS 서비스, CDI Bean, 서블릿 및 EJB와 같은 컨테이너 관리 Bean에 주입 될 수 있는 인스턴스를 생성합니다 . 다음 코드를 사용하여 User인스턴스 를 주입하십시오 (실제로 CDI 프록시 임).

@Inject
@AuthenticatedUser
User authenticatedUser;

CDI @Produces주석은 JAX-RS 주석과 다릅니다@Produces .

Bean @Produces에서 CDI 주석 을 사용해야합니다 AuthenticatedUserProducer.

여기서 핵심은로 주석이 달린 Bean으로 @RequestScoped, 필터와 Bean간에 데이터를 공유 할 수 있습니다. 이벤트를 사용하지 않으려면 인증 된 사용자를 요청 범위 Bean에 저장하도록 필터를 수정 한 후 JAX-RS 자원 클래스에서 읽을 수 있습니다.

SecurityContextCDI 접근 방식은 을 대체하는 접근 방식과 비교하여 JAX-RS 자원 및 제공자 이외의 Bean에서 인증 된 사용자를 가져올 수 있습니다.

역할 기반 인증 지원

역할 기반 인증을 지원하는 방법에 대한 자세한 내용은 다른 답변 을 참조하십시오 .

발행 토큰

토큰은 다음과 같습니다.

  • 불투명 : 임의의 문자열과 같이 값 자체 이외의 세부 정보를 표시하지 않습니다 .
  • 자체 포함 : JWT와 같은 토큰 자체에 대한 세부 사항을 포함합니다.

아래 세부 사항을 참조하십시오.

토큰으로 임의의 문자열

임의의 문자열을 생성하고 사용자 식별자 및 만료 날짜와 함께 토큰을 데이터베이스에 유지함으로써 토큰을 발행 할 수 있습니다. Java에서 임의의 문자열을 생성하는 방법에 대한 좋은 예는 여기에서 볼 수 있습니다 . 당신은 또한 사용할 수 있습니다 :

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

JWT (JSON 웹 토큰)

JWT (JSON Web Token)는 두 당사자 간의 클레임을 안전하게 표현하는 표준 방법이며 RFC 7519에 의해 정의됩니다 .

자체 포함 토큰이며 클레임에 세부 정보를 저장할 수 있습니다 . 이러한 클레임은 토큰 페이로드에 저장되며 이는 토큰이 Base64로 인코딩 된 JSON 입니다. 다음은 RFC 7519에 등록 된 클레임 과 그 의미에 대한 것입니다 (자세한 내용은 전체 RFC를 읽으십시오).

  • iss: 토큰을 발행 한 교장.
  • sub: JWT의 주제 인 교장.
  • exp: 토큰의 만료 날짜입니다.
  • nbf: 토큰 처리가 시작되는 시간입니다.
  • iat: 토큰이 발행 된 시간입니다.
  • jti: 토큰의 고유 식별자입니다.

비밀번호와 같은 민감한 데이터를 토큰에 저장해서는 안됩니다.

클라이언트가 페이로드를 읽을 수 있으며 서버에서 서명을 확인하여 토큰의 무결성을 쉽게 확인할 수 있습니다. 서명은 토큰이 변조되는 것을 방지합니다.

JWT 토큰을 추적 할 필요가없는 경우 JWT 토큰을 유지하지 않아도됩니다. 그러나 토큰을 유지하면 토큰 액세스를 무효화하고 취소 할 수 있습니다. JWT 토큰을 추적하려면 서버에서 전체 토큰을 유지하는 대신 토큰 jti을 발급 한 사용자, 만료 날짜 등과 같은 다른 세부 정보와 함께 토큰 식별자 ( 클레임)를 유지할 수 있습니다 .

토큰을 유지하는 경우 데이터베이스가 무한정 커지지 않도록 항상 이전 토큰을 제거하십시오.

JWT 사용

다음과 같은 JWT 토큰을 발행하고 유효성을 검증하는 몇 가지 Java 라이브러리가 있습니다.

JWT와 함께 작동하는 다른 훌륭한 자료를 찾으려면 http://jwt.io를 참조하십시오 .

JWT로 토큰 해지 처리

토큰을 취소하려면 추적해야합니다. 전체 토큰을 서버 측에 저장할 필요는 없으며 토큰 식별자 (고유해야 함)와 필요한 경우 일부 메타 데이터 만 저장하십시오. 토큰 식별자로는 UUID를 사용할 수 있습니다 .

jti제는 토큰의 토큰 식별자를 저장하는 데 사용되어야한다. 토큰의 유효성을 검사 할 때 jti서버 측에있는 토큰 식별자와 비교하여 클레임 값을 확인하여 토큰이 취소되지 않았는지 확인하십시오 .

보안을 위해 사용자가 비밀번호를 변경할 때 모든 토큰을 취소하십시오.

추가 정보

  • 사용하려는 인증 유형은 중요하지 않습니다. 중간자 공격 을 방지하려면 항상 HTTPS 연결 맨 위에서 수행하십시오 .
  • 토큰에 대한 자세한 내용은 Information Security 에서이 질문 을 살펴보십시오 .
  • 이 기사에서는 토큰 기반 인증에 대한 유용한 정보를 제공합니다.

답변

이 답변은 인증 에 관한 것이며 인증 에 대한 이전 답변을 보완 합니다.

또 다른 대답? 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(Forbidden)으로 요청이 중단됩니다 .

요청을 수행하는 사용자를 알려면 이전 답변을 참조하십시오 . 당신은 그것을 얻을 수있는 SecurityContext(이미 설정되어야하는 ContainerRequestContext당신이 갈 접근 방식에 따라 CDI를 사용하거나 주입).

경우 @Secured주석이 어떤 역할을 선언 없습니다, 당신은 사용자가 가지고있는 역할을 무시하고, 인증 된 모든 사용자가 해당 엔드 포인트에 액세스 할 수 있습니다 가정 할 수있다.

JSR-250 주석으로 역할 기반 권한 부여 지원

대안의 역할을 정의하는 @Secured위 그림과 같이 주석을, 당신은 같은 JSR-250 주석을 고려할 수 @RolesAllowed, @PermitAll@DenyAll.

JAX-RS는 기본적으로 이러한 주석을 지원하지 않지만 필터를 사용하여 달성 할 수 있습니다. 다음 사항을 모두 지원하려면 명심해야 할 몇 가지 사항이 있습니다.

따라서 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.");
    }
}

참고 : 위 구현은 Jersey 기반입니다 RolesAllowedDynamicFeature. Jersey를 사용하는 경우 고유 한 필터를 작성할 필요가 없으며 기존 구현 만 사용하십시오.


답변