Belirteç tabanlı kimlik doğrulaması nasıl çalışır?
Belirteç tabanlı kimlik doğrulamasında istemci, belirteç adı verilen bir veri parçası için sabit kimlik bilgilerini (kullanıcı adı ve parola gibi) değiştirir . Her istek için, sabit kimlik bilgilerini göndermek yerine, istemci kimlik doğrulama ve ardından yetkilendirme yapmak için belirteci sunucuya gönderir.
Birkaç kelimeyle, jetonlara dayalı bir kimlik doğrulama şeması aşağıdaki adımları izler:
- İstemci kimlik bilgilerini (kullanıcı adı ve şifre) sunucuya gönderir.
- Sunucu kimlik bilgilerini doğrular ve geçerliyse kullanıcı için bir belirteç oluşturur.
- Sunucu, önceden oluşturulan belirteci, kullanıcı tanımlayıcısı ve bir son kullanma tarihi ile birlikte bir miktar depolama alanında saklar.
- Sunucu, oluşturulan belirteci istemciye gönderir.
- İstemci, her istekte belirteci sunucuya gönderir.
- Sunucu, her istekte jetonu gelen istekten alır. Belirteçle, sunucu kimlik doğrulaması gerçekleştirmek için kullanıcı ayrıntılarını arar.
- Belirteç geçerliyse, sunucu isteği kabul eder.
- Simge geçersizse, sunucu isteği reddeder.
- Kimlik doğrulama gerçekleştirildikten sonra, sunucu yetkilendirme gerçekleştirir.
- Sunucu, simgeleri yenilemek için bir uç nokta sağlayabilir.
Not: Sunucu imzalı bir belirteç ( durum bilgisi olmayan kimlik doğrulaması gerçekleştirmenizi sağlayan JWT gibi) yayınladıysa 3. adım gerekli değildir .
JAX-RS 2.0 (Jersey, RESTEasy ve Apache CXF) ile yapabilecekleriniz
Bu çözüm yalnızca JAX-RS 2.0 API'sini kullanır ve satıcıya özel çözümlerden kaçınır . Bu nedenle Jersey , RESTEasy ve Apache CXF gibi JAX-RS 2.0 uygulamaları ile çalışmalıdır .
Belirteç tabanlı kimlik doğrulaması kullanıyorsanız, sunucu uygulaması kapsayıcısı tarafından sunulan ve uygulamanın web.xml
tanımlayıcısı aracılığıyla yapılandırılabilen standart Java EE web uygulaması güvenlik mekanizmalarına güvenmediğinizi belirtmek gerekir . Bu özel bir kimlik doğrulamasıdır.
Bir kullanıcının kullanıcı adı ve şifresi ile kimliğini doğrulama ve belirteç verme
Kimlik bilgilerini (kullanıcı adı ve şifre) alan ve doğrulayan bir JAX-RS kaynak yöntemi oluşturun ve kullanıcı için bir belirteç yayınlayın:
@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
}
}
Kimlik bilgileri doğrulanırken herhangi bir istisna atılırsa, durumuyla 403
(Yasak) bir yanıt döndürülür.
Kimlik bilgileri başarıyla doğrulanırsa, durumu 200
(Tamam) olan bir yanıt döndürülür ve verilen belirteç yanıt yükünde istemciye gönderilir. İstemci, her istekte belirteci sunucuya göndermelidir.
Tüketim yaparken application/x-www-form-urlencoded
, istemci kimlik bilgilerini istek yükünde aşağıdaki biçimde göndermelidir:
username=admin&password=123456
Form parametreleri yerine, kullanıcı adını ve parolayı bir sınıfa sarmak mümkündür:
public class Credentials implements Serializable {
private String username;
private String password;
// Getters and setters omitted
}
Ve sonra JSON olarak tüketin:
@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
}
Bu yaklaşımı kullanarak, istemcinin kimlik bilgilerini isteğin yükünde aşağıdaki biçimde göndermesi gerekir:
{
"username": "admin",
"password": "123456"
}
Jetonun istekten çıkarılması ve onaylanması
İstemci, belirteci Authorization
isteğin standart HTTP üstbilgisinde göndermelidir . Örneğin:
Authorization: Bearer <token-goes-here>
Standart HTTP üstbilgisinin adı, yetkilendirme değil kimlik doğrulama bilgilerini taşıdığı için talihsizdir . Ancak, sunucuya kimlik bilgileri göndermek için standart HTTP üstbilgisidir.
JAX-RS, @NameBinding
filtreleri ve önleyicileri kaynak sınıflarına ve yöntemlerine bağlamak için başka ek açıklamalar oluşturmak için kullanılan bir meta ek açıklama sağlar. Bir @Secured
ek açıklamayı aşağıdaki gibi tanımlayın :
@NameBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface Secured { }
Yukarıda tanımlanan ad bağlama ek açıklaması, ContainerRequestFilter
bir kaynak yöntemiyle işlenmeden önce isteği durdurmanıza olanak tanıyan bir filtre sınıfını süslemek için kullanılacaktır. ContainerRequestContext
HTTP istek başlıklarını erişebilir ve daha sonra belirteç ayıklamak için kullanılabilir:
@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
}
}
Belirteç doğrulaması sırasında herhangi bir sorun olursa, durumla ilgili bir yanıt 401
(Yetkisiz) döndürülür. Aksi takdirde, istek bir kaynak yöntemine geçecektir.
REST uç noktalarınızı koruma
Kimlik doğrulama filtresini kaynak yöntemlerine veya kaynak sınıflarına bağlamak için, @Secured
yukarıda oluşturulan ek açıklama ile açıklama ekleyin . Ek açıklamalı yöntemler ve / veya sınıflar için filtre yürütülür. Bu, bu tür uç noktalara yalnızca talebin geçerli bir belirteçle gerçekleştirilmesi durumunda ulaşılacağı anlamına gelir .
Bazı yöntemlerin veya sınıfların kimlik doğrulamasına ihtiyacı yoksa, bunlara açıklama eklemeyin:
@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
...
}
}
Yukarıda gösterilen örnekte, filtre açıklandığı için yalnızcamySecuredMethod(Long)
yöntem için yürütülür @Secured
.
Mevcut kullanıcıyı belirleme
Büyük olasılıkla, REST API'nizle ilgili isteği gerçekleştiren kullanıcıyı tanımanız gerekecektir. Bunu başarmak için aşağıdaki yaklaşımlar kullanılabilir:
Geçerli isteğin güvenlik bağlamını geçersiz kılma
ContainerRequestFilter.filter(ContainerRequestContext)
Yöntemin içinde SecurityContext
, geçerli istek için yeni bir örnek ayarlanabilir. Ardından SecurityContext.getUserPrincipal()
, bir Principal
örneği döndürerek geçersiz kılın :
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
'Adı olan kullanıcı tanımlayıcısını (kullanıcı adı) aramak için jetonu kullanın .
SecurityContext
Herhangi bir JAX-RS kaynak sınıfına enjekte edin :
@Context
SecurityContext securityContext;
Aynı şey bir JAX-RS kaynak yönteminde de yapılabilir:
@GET
@Secured
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Response myMethod(@PathParam("id") Long id,
@Context SecurityContext securityContext) {
...
}
Ve sonra Principal
:
Principal principal = securityContext.getUserPrincipal();
String username = principal.getName();
CDI Kullanımı (Bağlam ve Bağımlılık Enjeksiyonu)
Herhangi bir nedenle, geçersiz kılmak istemiyorsanız SecurityContext
, etkinlikler ve üreticiler gibi yararlı özellikler sağlayan CDI (Bağlam ve Bağımlılık Enjeksiyonu) kullanabilirsiniz.
Bir CDI niteleyicisi oluşturun:
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
AuthenticationFilter
Yukarıda oluşturduğunuzda, Event
açıklamalı bir not ekleyin @AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
Kimlik doğrulama başarılı olursa, kullanıcı adını parametre olarak ileten etkinliği tetikleyin (unutmayın, kullanıcı için simge verilir ve simge kullanıcı tanımlayıcısını aramak için kullanılır):
userAuthenticatedEvent.fire(username);
Uygulamanızda bir kullanıcıyı temsil eden bir sınıf olması muhtemeldir. Bu sınıfa diyelim User
.
Kimlik doğrulama olayını işlemek için bir CDI çekirdeği oluşturun User
, karşılık gelen kullanıcı adına sahip bir örnek bulun ve authenticatedUser
üretici alanına atayın :
@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
Alan bir üretir User
böyle JAX-RS hizmetler, CDI fasulye, servlet ve EJB'ler olarak konteyner yönetilen fasulye, enjekte edilebilir örneği. Bir User
örneği enjekte etmek için aşağıdaki kod parçasını kullanın (aslında, bu bir CDI proxy'sidir):
@Inject
@AuthenticatedUser
User authenticatedUser;
CDI @Produces
ek açıklamasının JAX-RS ek açıklamasından farklı olduğuna dikkat edin @Produces
:
Çekirdeğinizde CDI @Produces
notunu kullandığınızdan emin olun AuthenticatedUserProducer
.
Buradaki anahtar, açıklamalı fasulye olup @RequestScoped
filtreler ve çekirdekleriniz arasında veri paylaşmanıza izin verir. Olayları kullanmak istemiyorsanız, kimliği doğrulanmış kullanıcıyı istek kapsamındaki bir çekirdeğe kaydetmek için filtreyi değiştirebilir ve ardından JAX-RS kaynak sınıflarınızdan okuyabilirsiniz.
SecurityContext
CDI yaklaşımı, geçersiz kılınan yaklaşıma kıyasla , kimliği doğrulanmış kullanıcıyı JAX-RS kaynakları ve sağlayıcıları dışındaki çekirdeklerden almanıza olanak tanır.
Rol tabanlı yetkilendirmeyi destekleme
Rol tabanlı yetkilendirmeyi nasıl destekleyeceğinize ilişkin ayrıntılar için lütfen diğer cevabıma bakın .
Jeton düzenleme
Bir belirteç şunlar olabilir:
- Opak: Değerin kendisinden başka hiçbir ayrıntı göstermez (rastgele bir dize gibi)
- Bağımsız: Simgenin kendisi hakkında ayrıntılar içerir (JWT gibi).
Aşağıdaki ayrıntılara bakın:
Jeton olarak rastgele dize
Bir belirteç rastgele bir dize oluşturularak ve kullanıcı tanımlayıcısı ve bir son kullanma tarihi ile birlikte bir veritabanına devam ettirilerek verilebilir. Java'da rastgele bir dizenin nasıl oluşturulacağına dair iyi bir örnek burada görülebilir . Ayrıca şunları kullanabilirsiniz:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (JSON Web Simgesi)
JWT (JSON Web Jetonu), talepleri iki taraf arasında güvenli bir şekilde temsil etmek için standart bir yöntemdir ve RFC 7519 tarafından tanımlanır .
Bu, bağımsız bir jeton ve ayrıntıları taleplerde saklamanızı sağlar . Bu hak talepleri, Base64 olarak kodlanmış bir JSON olan belirteç bilgi yükünde saklanır . RFC 7519'da kayıtlı bazı iddialar ve anlamları (daha fazla bilgi için tam RFC'yi okuyun):
iss
: Jetonu veren müdür.
sub
: JWT'nin konusu olan müdür.
exp
: Belirtecin son kullanma tarihi.
nbf
: Simgenin işleme alınmaya başlanacağı zaman.
iat
: Simgenin verildiği zaman.
jti
: Belirteç için benzersiz tanımlayıcı.
Parola gibi hassas verileri jetonda saklamamanız gerektiğini unutmayın.
Yük müşteri tarafından okunabilir ve token bütünlüğü sunucudaki imzası doğrulanarak kolayca kontrol edilebilir. İmza, simgenin kurcalanmasını önleyen şeydir.
İzlemeniz gerekmiyorsa JWT jetonlarını saklamanız gerekmez. Bununla birlikte, jetonları devam ettirerek, bunların erişimini geçersiz kılma ve iptal etme olasılığınız olacaktır. JWT jetonlarının kaydını tutmak için, tüm jetonu sunucuda saklamak yerine, jeton tanımlayıcısına ( jti
hak talebine), jetonu verdiğiniz kullanıcı, son kullanma tarihi vb. Gibi diğer bazı ayrıntılarla devam edebilirsiniz .
Belirteçlere devam ederken, veritabanınızın süresiz büyümesini önlemek için her zaman eskilerini kaldırmayı düşünün.
JWT kullanma
JWT belirteçlerini yayınlamak ve doğrulamak için birkaç Java kitaplığı vardır:
JWT ile çalışmak için başka harika kaynaklar bulmak için http://jwt.io adresine göz atın .
Jeton iptalini JWT ile işleme
Jetonları iptal etmek istiyorsanız, onları takip etmelisiniz. Tüm belirteci sunucu tarafında depolamanız, yalnızca belirteç tanımlayıcısını (benzersiz olması gerekir) ve gerekirse bazı meta verileri depolamanız gerekmez. Belirteç tanımlayıcısı için UUID kullanabilirsiniz .
jti
İstem belirteci token tanımlayıcı saklamak için kullanılır. Belirteci doğrularken, jti
sunucu tarafında sahip olduğunuz belirteç tanımlayıcılarına karşı hak talebinin değerini kontrol ederek iptal edilmediğinden emin olun.
Güvenlik nedeniyle, bir kullanıcının şifresini değiştirdiğinde tüm simgeleri iptal edin.
Ek bilgi
- Hangi tür kimlik doğrulamasını kullanmaya karar verdiğiniz önemli değildir. Ortadaki adam saldırısını önlemek için her zaman bir HTTPS bağlantısının üstünde yapın .
- Jetonlar hakkında daha fazla bilgi için Bilgi Güvenliği'nden bu soruya göz atın .
- Bu makalede , belirteç tabanlı kimlik doğrulaması hakkında bazı yararlı bilgiler bulacaksınız.
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.
Bu nasıl RESTful?