1. 개요

Jakarta EE 8 Security API는 Java 컨테이너의 Security 문제를 처리하는 새로운 표준이자 이식 가능한 방법입니다.

이 기사에서는 API의 세 가지 핵심 기능을 살펴보겠습니다.

  1. HTTP 인증 메커니즘
  2. 신원 저장소
  3. Security 컨텍스트

먼저 제공된 구현을 구성하는 방법을 이해한 다음 사용자 지정 구현을 구현하는 방법을 이해합니다.

2. 메이븐 의존성

Jakarta EE 8 Security API를 설정하려면 서버 제공 구현 또는 명시적 구현이 필요합니다.

2.1. 서버 구현 사용

Jakarta EE 8 호환 서버는 이미 Jakarta EE 8 Security API에 대한 구현을 제공하므로 Jakarta EE 웹 프로필 API  Maven 아티팩트만 필요합니다.

<dependencies>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>8.0</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2.2. 명시적 구현 사용

먼저 Jakarta EE 8 Security API 에 대한 Maven 아티팩트를 지정합니다 .

<dependencies>
    <dependency>
        <groupId>javax.security.enterprise</groupId>
        <artifactId>javax.security.enterprise-api</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

그런 다음   참조 구현 인 Soteria 와 같은 구현을 추가합니다.

<dependencies>
    <dependency>
        <groupId>org.glassfish.soteria</groupId>
        <artifactId>javax.security.enterprise</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

3. HTTP 인증 메커니즘

Jakarta EE 8 이전에는 web.xml 파일을 통해 선언적으로 인증 메커니즘을 구성했습니다.

이 버전에서 Jakarta EE 8 Security API는 새로운 HttpAuthenticationMechanism 인터페이스를 교체용으로 설계했습니다. 따라서 웹 애플리케이션은 이제 이 인터페이스의 구현을 제공하여 인증 메커니즘을 구성할 수 있습니다.

다행스럽게도 컨테이너는 이미 Servlet 사양에 정의된 세 가지 인증 방법인 기본 HTTP 인증, 양식 기반 인증 및 사용자 지정 양식 기반 인증 각각에 대한 구현을 제공합니다.

또한 각 구현을 트리거하는 어노테이션을 제공합니다.

  1. @BasicAuthenticationMechanismDefinition
  2. @FormAuthenticationMechanismDefinition
  3. @CustomFormAuthenrticationMechanismDefinition

3.1. 기본 HTTP 인증

위에서 언급한 것처럼 웹 애플리케이션은  CDI 빈에서 @BasicAuthenticationMechanismDefinition 어노테이션을 사용하여 기본 HTTP 인증을 구성할 수 있습니다 .

@BasicAuthenticationMechanismDefinition(
  realmName = "userRealm")
@ApplicationScoped
public class AppConfig{}

이 시점에서 서블릿 컨테이너는 제공된 HttpAuthenticationMechanism  인터페이스 구현을 검색하고 인스턴스화합니다.

승인되지 않은 요청을 수신하면 컨테이너는 WWW-Authenticate 응답 헤더를 통해 적절한 인증 정보를 제공하도록 클라이언트에 요청합니다.

WWW-Authenticate: Basic realm="userRealm"

그런 다음 클라이언트는 Authorization 요청 헤더 를 통해 콜론 ":"으로 구분되고 Base64로 인코딩된 사용자 이름과 비밀번호를 보냅니다 .

//user=baeldung, password=baeldung
Authorization: Basic YmFlbGR1bmc6YmFlbGR1bmc=

자격 증명을 제공하기 위해 표시되는 대화 상자는 서버가 아니라 브라우저에서 표시됩니다.

3.2. 양식 기반 HTTP 인증

@FormAuthenticationMechanismDefinition  어노테이션은 Servlet 사양에 정의된 대로 양식  기반 인증을 트리거합니다 .

그런 다음 로그인 및 오류 페이지를 지정하거나 기본 적절한 /login/login-error 를 사용하는 옵션이 있습니다 .

@FormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(
    loginPage = "/login.html",
    errorPage = "/login-error.html"))
@ApplicationScoped
public class AppConfig{}

loginPage 를 호출한 결과 서버는 양식을 클라이언트로 보내야 합니다.

<form action="j_security_check" method="post">
    <input name="j_username" type="text"/>
    <input name="j_password" type="password"/>
    <input type="submit">
</form>

그런 다음 클라이언트는 컨테이너에서 제공하는 미리 정의된 백업 인증 프로세스로 양식을 보내야 합니다.

3.3. 사용자 정의 양식 기반 HTTP 인증

웹 애플리케이션은 @CustomFormAuthenticationMechanismDefinition 어노테이션을 사용하여 사용자 정의 양식 기반 인증 구현을 트리거할 수 있습니다 .

@CustomFormAuthenticationMechanismDefinition(
  loginToContinue = @LoginToContinue(loginPage = "/login.xhtml"))
@ApplicationScoped
public class AppConfig {
}

그러나 기본 양식 기반 인증과 달리 사용자 지정 로그인 페이지를 구성하고  SecurityContext.authenticate() 메서드를 지원 인증 프로세스로 호출합니다.

로그인 논리를 포함하는 백업 LoginBean 도 살펴보겠습니다 .

@Named
@RequestScoped
public class LoginBean {

    @Inject
    private SecurityContext securityContext;

    @NotNull private String username;

    @NotNull private String password;

    public void login() {
        Credential credential = new UsernamePasswordCredential(
          username, new Password(password));
        AuthenticationStatus status = securityContext
          .authenticate(
            getHttpRequestFromFacesContext(),
            getHttpResponseFromFacesContext(),
            withParams().credential(credential));
        // ...
    }
     
    // ...
}

사용자 정의 login.xhtml 페이지 를 호출한 결과 클라이언트는 받은 양식을 LoginBeanlogin() 메소드에 제출합니다.

//...
<input type="submit" value="Login" jsf:action="#{loginBean.login}"/>

3.4. 사용자 지정 인증 메커니즘

HttpAuthenticationMechanism 인터페이스는 세 가지 메서드를 정의합니다 . 가장 중요한 것은   구현을 제공해야 하는 validateRequest() 입니다.

다른 두 메서드  secureResponse()cleanSubject() 기본 동작 은 대부분의 경우 충분합니다.

구현 예를 살펴보겠습니다.

@ApplicationScoped
public class CustomAuthentication 
  implements HttpAuthenticationMechanism {

    @Override
    public AuthenticationStatus validateRequest(
      HttpServletRequest request,
      HttpServletResponse response, 
      HttpMessageContext httpMsgContext) 
      throws AuthenticationException {
 
        String username = request.getParameter("username");
        String password = response.getParameter("password");
        // mocking UserDetail, but in real life, we can obtain it from a database
        UserDetail userDetail = findByUserNameAndPassword(username, password);
        if (userDetail != null) {
            return httpMsgContext.notifyContainerAboutLogin(
              new CustomPrincipal(userDetail),
              new HashSet<>(userDetail.getRoles()));
        }
        return httpMsgContext.responseUnauthorized();
    }
    //...
}

여기서 구현은 유효성 검사 프로세스의 비즈니스 논리를 제공하지만 실제로는 유효성 검사 를 호출 하여 IdentityStoreHandler 를 통해 IdentityStore 에 Delegation하는 것이 좋습니다 .

또한 CDI를 활성화해야 하므로 @ApplicationScoped 어노테이션으로 구현에  어노테이션을 달았습니다.

자격 증명의 유효한 확인 및 사용자 역할의 최종 검색 후 구현은 다음을 컨테이너에 알려야 합니다 .

HttpMessageContext.notifyContainerAboutLogin(Principal principal, Set groups)

3.5. 서블릿 Security 강화

웹 애플리케이션은 서블릿 구현에서 @ ServletSecurity 어노테이션을 사용하여 Security 제약 조건을 적용할 수 있습니다 .

@WebServlet("/secured")
@ServletSecurity(
  value = @HttpConstraint(rolesAllowed = {"admin_role"}),
  httpMethodConstraints = {
    @HttpMethodConstraint(
      value = "GET", 
      rolesAllowed = {"user_role"}),
    @HttpMethodConstraint(     
      value = "POST", 
      rolesAllowed = {"admin_role"})
  })
public class SecuredServlet extends HttpServlet {
}

이 어노테이션에는  httpMethodConstraintsvalue 라는 두 가지 속성이 있습니다 . httpMethodConstraints  는 하나 이상의 제약 조건을 지정하는 데 사용되며 각 제약 조건은 허용된 역할 List으로 HTTP 메서드에 대한 액세스 제어를 나타냅니다.

그러면 컨테이너는 연결된 사용자에게 리소스 액세스에 적합한 역할이 있는지 모든 url-pattern 및 HTTP 메서드에 대해 확인합니다.

4. 신원 저장소

이 기능은 IdentityStore 인터페이스에 의해 추상화되며 자격 증명  의 유효성을 검사하고 최종적으로 그룹 구성원 자격을 검색하는 데 사용됩니다. 즉, 인증, 권한 부여 또는 둘 다를 위한 기능을 제공할 수 있습니다 .

IdentityStore 는 호출된 IdentityStoreHandler  인터페이스 를 통해 HttpAuthenticationMecanism 에서  사용하도록 의도되고 권장됩니다 . IdentityStoreHandler 의 기본 구현은 Servlet 컨테이너에서 제공됩니다. 

애플리케이션은 IdentityStore 의 구현을 제공 하거나 데이터베이스 및 LDAP용 컨테이너에서 제공하는 두 가지 내장 구현 중 하나를 사용할 수 있습니다.

4.1. 내장 ID 저장소

Jakarta EE 호환 서버는 두 개의 ID 저장소인 데이터베이스 및 LDAP 에 대한 구현을 제공해야 합니다 .

데이터베이스 IdentityStore 구현은 구성 데이터를 @DataBaseIdentityStoreDefinition 어노테이션에 전달하여 초기화됩니다.

@DatabaseIdentityStoreDefinition(
  dataSourceLookup = "java:comp/env/jdbc/securityDS",
  callerQuery = "select password from users where username = ?",
  groupsQuery = "select GROUPNAME from groups where username = ?",
  priority=30)
@ApplicationScoped
public class AppConfig {
}

구성 데이터 로 외부 데이터베이스에 대한 JNDI 데이터 소스가 필요하고 호출자와 그의 그룹을 확인하기 위한 두 개의 JDBC 문이 필요하며 마지막으로 다중 저장소인 경우에 사용되는 우선 순위 매개 변수가 구성됩니다.

우선 순위가 높은 IdentityStore 는 나중에 IdentityStoreHandler에 의해 처리됩니다.

데이터베이스와 마찬가지로 LDAP IdentityStore 구현은 구성 데이터를 전달 하여 @LdapIdentityStoreDefinition 을 통해 초기화됩니다.

@LdapIdentityStoreDefinition(
  url = "ldap://localhost:10389",
  callerBaseDn = "ou=caller,dc=baeldung,dc=com",
  groupSearchBase = "ou=group,dc=baeldung,dc=com",
  groupSearchFilter = "(&(member=%s)(objectClass=groupOfNames))")
@ApplicationScoped
public class AppConfig {
}

여기에는 외부 LDAP 서버의 URL, LDAP 디렉토리에서 발신자를 검색하는 방법 및 그의 그룹을 검색하는 방법이 필요합니다.

4.2. 사용자 정의 IdentityStore 구현

IdentityStore 인터페이스는 네 가지 기본 메서드를 정의합니다 .

default CredentialValidationResult validate(
  Credential credential)
default Set<String> getCallerGroups(
  CredentialValidationResult validationResult)
default int priority()
default Set<ValidationType> validationTypes()

priority()  메서드는 이 구현이 IdentityStoreHandler 에 의해 처리되는 반복 순서 값을 반환합니다 .  우선 순위가 낮은 IdentityStore  가  먼저 처리됩니다.

기본적으로  IdentityStore 는 자격 증명 유효성 검사 (ValidationType.VALIDATE) 및 그룹 검색( ValidationType.PROVIDE_GROUPS )을 모두 처리합니다. 하나의 기능만 제공할 수 있도록 이 동작을 재정의할 수 있습니다.

따라서  자격 증명 유효성 검사에만 사용되도록 IdentityStore 를 구성할 수 있습니다.

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.VALIDATE);
}

이 경우에 우리는 validate() 메소드 에 대한 구현을 제공해야 합니다 :

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 70;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.VALIDATE);
    }

    public CredentialValidationResult validate( 
      UsernamePasswordCredential credential) {
 
        UserDetails user = users.get(credential.getCaller());
        if (credential.compareTo(user.getLogin(), user.getPassword())) {
            return new CredentialValidationResult(user.getLogin());
        }
        return INVALID_RESULT;
    }
}

또는 그룹 검색에만 사용할 수 있도록 IdentityStore 를 구성하도록 선택할 수 있습니다.

@Override
public Set<ValidationType> validationTypes() {
    return EnumSet.of(ValidationType.PROVIDE_GROUPS);
}

그런 다음 getCallerGroups() 메서드 에 대한 구현을 제공해야 합니다.

@ApplicationScoped
public class InMemoryIdentityStore implements IdentityStore {
    // init from a file or harcoded
    private Map<String, UserDetails> users = new HashMap<>();

    @Override
    public int priority() {
        return 90;
    }

    @Override
    public Set<ValidationType> validationTypes() {
        return EnumSet.of(ValidationType.PROVIDE_GROUPS);
    }

    @Override
    public Set<String> getCallerGroups(CredentialValidationResult validationResult) {
        UserDetails user = users.get(
          validationResult.getCallerPrincipal().getName());
        return new HashSet<>(user.getRoles());
    }
}

IdentityStoreHandler 는 구현이 CDI bean이 될 것으로 예상 하기 때문에 ApplicationScoped 어노테이션 으로 장식합니다 .

5. Security 컨텍스트 API

Jakarta EE 8 Security API는 SecurityContext 인터페이스 를 통해 프로그램 Security에 대한 액세스 포인트를 제공합니다 . 컨테이너에 의해 시행되는 선언적 Security 모델이 충분하지 않은 경우 대안입니다.

SecurityContext 인터페이스 의 기본 구현은 런타임 시 CDI 빈으로 제공되어야 하므로 이를 주입해야 합니다.

@Inject
SecurityContext securityContext;

이 시점에서 사용자를 인증하고, 인증된 사용자를 검색하고, 역할 구성원을 확인하고, 사용 가능한 다섯 가지 방법을 통해 웹 리소스에 대한 액세스를 허용하거나 거부할 수 있습니다.

5.1. 발신자 데이터 검색

이전 버전의 Jakarta EE에서는 Principal 을 검색 하거나 각 컨테이너에서 역할 구성원 자격을 다르게 확인했습니다.

서블릿 컨테이너에서 HttpServletRequest 의  getUserPrincipal() isUserInRole()  메소드를 사용하는 동안 EJB 컨테이너 에서는 EJBContext 의 유사한 메소드  getCallerPrincipal()  및  isCallerInRole() 메소드  를 사용합니다.

새로운 Jakarta EE 8 Security API는 SecurityContext 인터페이스 를 통해 유사한 방법을 제공하여 이를  표준화했습니다.

Principal getCallerPrincipal();
boolean isCallerInRole(String role);
<T extends Principal> Set<T> getPrincipalsByType(Class<T> type);

getCallerPrincipal() 메서드는 인증된 호출자의 컨테이너별 표현을 반환하는 반면  getPrincipalsByType ()  메서드는 지정된 유형의 모든 Security 주체를 검색합니다.

애플리케이션 특정 호출자가 컨테이너 호출자와 다른 경우에 유용할 수 있습니다.

5.2. 웹 리소스 액세스 테스트

먼저 보호된 리소스를 구성해야 합니다.

@WebServlet("/protectedServlet")
@ServletSecurity(@HttpConstraint(rolesAllowed = "USER_ROLE"))
public class ProtectedServlet extends HttpServlet {
    //...
}

그런 다음 이 보호된 리소스에 대한 액세스를 확인하려면 hasAccessToWebResource() 메서드를 호출해야 합니다.

securityContext.hasAccessToWebResource("/protectedServlet", "GET");

이 경우 사용자가  USER_ROLE 역할에 있으면 메서드는 true를 반환합니다.

5.3. 프로그래밍 방식으로 호출자 인증

애플리케이션은 다음을 호출하여 인증 프로세스를 프로그래밍 방식으로 트리거할 수 있습니다 .

AuthenticationStatus authenticate(
  HttpServletRequest request, 
  HttpServletResponse response,
  AuthenticationParameters parameters);

그런 다음 컨테이너는 알림을 받고 애플리케이션에 대해 구성된 인증 메커니즘을 호출합니다. AuthenticationParameters  매개변수는  HttpAuthenticationMechanism에 자격 증명을 제공합니다.

withParams().credential(credential)

AuthenticationStatusSUCCESSSEND_FAILURE  값은  성공 및 실패 인증을 설계하고  SEND_CONTINUE   는 인증 프로세스의 진행 상태를 나타냅니다.

6. 예제 실행

이러한 예제를 강조하기 위해 Jakarta EE 8을 지원하는  Open Liberty Server의 최신 개발 빌드를 사용했습니다. 이 빌드  는 응용 프로그램을 배포하고 서버를 시작할 수도 있는 liberty-maven-plugin 덕분에 다운로드 및 설치됩니다 .

예제를 실행하려면 해당 모듈에 액세스하고 다음 명령을 호출하기만 하면 됩니다.

mvn clean package liberty:run

결과적으로 Maven은 서버를 다운로드하고 애플리케이션을 빌드, 배포 및 실행합니다.

7. 결론

이 기사에서는 새로운 Jakarta EE 8 Security API의 주요 기능 구성 및 구현에 대해 다루었습니다.

먼저 기본 내장 인증 메커니즘을 구성하는 방법과 사용자 지정 메커니즘을 구현하는 방법을 보여줌으로써 시작했습니다. 나중에 내장 ID 저장소를 구성하는 방법과 사용자 지정 저장소를 구현하는 방법을 살펴보았습니다. 마지막으로 SecurityContext 의 메서드를 호출하는 방법을 살펴보았습니다 .

항상 그렇듯이 이 기사의 코드 예제는 GitHub에서 사용할 수 있습니다 .

Security footer banner