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


459

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

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

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

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

답변:


1387

토큰 기반 인증 작동 방식

토큰 기반 인증에서 클라이언트는 하드 자격 증명 (예 : 사용자 이름 및 비밀번호)을 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 에서이 질문 을 살펴보십시오 .
  • 이 기사에서는 토큰 기반 인증에 대한 유용한 정보를 제공합니다.

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 토큰 기반 인증은 서버가 발행 한 토큰을 서버가 기억하는 방식으로 작동합니다. 상태 비 저장 인증에 JWT 토큰을 사용할 수 있습니다.
cassiomolin

평범한 대신 해시 암호를 보내는 것은 어떻습니까 (서버 생성 nonce로 해시 됨)? 보안 수준이 증가합니까 (예 : https를 사용하지 않는 경우)? 중간에 사람의 경우-그는 한 세션을 납치 할 수 있지만 최소한 그는 암호를 얻지 못할 것입니다
Denis 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(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를 사용하는 경우 고유 한 필터를 작성할 필요가 없으며 기존 구현 만 사용하십시오.


이 우아한 솔루션을 사용할 수있는 github 저장소가 있습니까?
Daniel Ferreira Castro

6
@DanielFerreiraCastro 물론입니다. 봐 가지고 여기를 .
cassiomolin

요청이 권한이있는 사용자로부터 왔으며 사용자가 데이터를 "소유"하기 때문에 데이터를 변경할 수 있는지 확인하는 좋은 방법이 있습니까 (예 : 해커가 다른 사용자의 이름을 변경하기 위해 자신의 토큰을 사용할 수 없음)? user_id== token.userId또는 이와 비슷한 경우 모든 엔드 포인트에서 확인할 수 있지만 매우 반복적입니다.
mFeinstein 2016 년

@mFeinstein 이에 대한 답변은 여기에 의견에 입력 할 수있는 것보다 많은 문자가 필요합니다. 방향을 알려주기 위해 행 수준 보안을 찾을 수 있습니다.
cassiomolin

행 수준 보안을 검색 할 때 데이터베이스에 대한 많은 주제를 볼 수 있습니다. 그러면 새로운 질문으로 열 것입니다.
mFeinstein
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.