토큰 기반 인증 작동 방식
토큰 기반 인증에서 클라이언트는 하드 자격 증명 (예 : 사용자 이름 및 비밀번호)을 token 이라는 데이터 조각으로 교환 합니다 . 각 요청에 대해 하드 자격 증명을 보내는 대신 클라이언트는 서버로 토큰을 보내 인증 및 권한 부여를 수행합니다.
간단히 말해서, 토큰 기반 인증 체계는 다음 단계를 따릅니다.
- 클라이언트는 자격 증명 (사용자 이름 및 비밀번호)을 서버로 보냅니다.
- 서버는 자격 증명을 인증하고 유효한 경우 사용자에 대한 토큰을 생성합니다.
- 서버는 이전에 생성 된 토큰을 사용자 식별자 및 만료 날짜와 함께 일부 저장소에 저장합니다.
- 서버는 생성 된 토큰을 클라이언트로 보냅니다.
- 클라이언트는 각 요청에서 서버로 토큰을 보냅니다.
- 각 요청에서 서버는 들어오는 요청에서 토큰을 추출합니다. 토큰으로 서버는 인증을 수행하기 위해 사용자 세부 사항을 찾습니다.
- 토큰이 유효하면 서버는 요청을 수락합니다.
- 토큰이 유효하지 않으면 서버는 요청을 거부합니다.
- 인증이 수행되면 서버는 인증을 수행합니다.
- 서버는 엔드 포인트를 제공하여 토큰을 새로 고칠 수 있습니다.
참고 : 서버가 서명 된 토큰 (예 : JWT와 같이 상태 비 저장 인증 을 수행 할 수있는)을 발행 한 경우 3 단계는 필요하지 않습니다 .
JAX-RS 2.0으로 수행 할 수있는 작업 (Jersey, RESTEasy 및 Apache CXF)
이 솔루션은 공급 업체별 솔루션을 피하면서 JAX-RS 2.0 API 만 사용합니다 . 따라서 Jersey , RESTEasy 및 Apache 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
하여 요청이 자원 메소드에 의해 처리되기 전에 인터셉트 할 수 있도록합니다. 는 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 (컨텍스트 및 종속성 주입)를 사용할 수 있습니다.
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
필드는 User
JAX-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 자원 클래스에서 읽을 수 있습니다.
SecurityContext
CDI 접근 방식은 을 대체하는 접근 방식과 비교하여 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입니까?