การรับรองความถูกต้องตามโทเค็นทำงานอย่างไร
ในการตรวจสอบ token-based, การแลกเปลี่ยนลูกค้าข้อมูลประจำตัวยาก (เช่นชื่อผู้ใช้และรหัสผ่าน) สำหรับชิ้นส่วนของข้อมูลที่เรียกว่าโทเค็น สำหรับแต่ละคำขอแทนการส่งข้อมูลประจำตัวที่ยากลูกค้าจะส่งโทเค็นไปยังเซิร์ฟเวอร์เพื่อดำเนินการตรวจสอบและจากนั้นการอนุญาต
กล่าวสองสามคำว่ารูปแบบการรับรองความถูกต้องซึ่งอิงตามโทเค็นทำตามขั้นตอนเหล่านี้:
- ไคลเอนต์ส่งข้อมูลประจำตัวของพวกเขา (ชื่อผู้ใช้และรหัสผ่าน) ไปยังเซิร์ฟเวอร์
- เซิร์ฟเวอร์ตรวจสอบข้อมูลประจำตัวและหากถูกต้องให้สร้างโทเค็นสำหรับผู้ใช้
- เซิร์ฟเวอร์จัดเก็บโทเค็นที่สร้างไว้ก่อนหน้านี้ในที่เก็บข้อมูลบางส่วนพร้อมกับตัวระบุผู้ใช้และวันที่หมดอายุ
- เซิร์ฟเวอร์ส่งโทเค็นที่สร้างขึ้นไปยังลูกค้า
- ไคลเอ็นต์ส่งโทเค็นไปยังเซิร์ฟเวอร์ในแต่ละคำขอ
- เซิร์ฟเวอร์ในแต่ละคำขอแยกโทเค็นออกจากคำขอที่เข้ามา ด้วยโทเค็นเซิร์ฟเวอร์จะค้นหารายละเอียดผู้ใช้เพื่อดำเนินการตรวจสอบสิทธิ์
- หากโทเค็นนั้นถูกต้องเซิร์ฟเวอร์จะยอมรับการร้องขอ
- หากโทเค็นไม่ถูกต้องเซิร์ฟเวอร์ปฏิเสธคำขอ
- เมื่อทำการตรวจสอบแล้วเซิร์ฟเวอร์จะทำการตรวจสอบความถูกต้อง
- เซิร์ฟเวอร์สามารถให้ปลายทางเพื่อรีเฟรชโทเค็น
หมายเหตุ:ไม่จำเป็นต้องใช้ขั้นตอนที่ 3 หากเซิร์ฟเวอร์ออกโทเค็นที่ลงชื่อแล้ว (เช่น JWT ซึ่งอนุญาตให้คุณทำสถานะไร้สัญชาติ)พิสูจน์ตัวตนแบบ )
คุณสามารถทำอะไรกับ JAX-RS 2.0 (Jersey, RESTEasy และ Apache CXF)
การแก้ปัญหานี้ใช้เฉพาะ JAX-RS 2.0 API, หลีกเลี่ยงการแก้ปัญหาผู้ขายที่เฉพาะเจาะจงใด ๆ ดังนั้นจึงควรจะทำงานกับ JAX-RS 2.0 การใช้งานเช่นย์ , RESTEasyและApache CXF
มีประโยชน์ที่จะกล่าวถึงว่าถ้าคุณใช้การรับรองความถูกต้องด้วยโทเค็นคุณจะไม่ต้องพึ่งพากลไกความปลอดภัยเว็บแอปพลิเคชัน Java EE มาตรฐานที่นำเสนอโดยคอนเทนเนอร์ servlet และกำหนดค่าผ่านแอปพลิเคชัน web.xml
บ่ง มันเป็นการตรวจสอบที่กำหนดเอง
ตรวจสอบสิทธิ์ผู้ใช้ด้วยชื่อผู้ใช้และรหัสผ่านและออกโทเค็น
สร้างวิธีการทรัพยากร 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
แทนที่จะเป็นแบบฟอร์ม params คุณสามารถใส่ชื่อผู้ใช้และรหัสผ่านในคลาสได้:
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"
}
แยกโทเค็นออกจากคำขอและตรวจสอบความถูกต้อง
ลูกค้าควรส่งโทเค็นในAuthorization
ส่วนหัวHTTP มาตรฐานของคำขอ ตัวอย่างเช่น:
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 (Context and Dependency Injection) ซึ่งมีคุณสมบัติที่มีประโยชน์เช่นเหตุการณ์และผู้ผลิต
สร้างตัวระบุ CDI:
@Qualifier
@Retention(RUNTIME)
@Target({ METHOD, FIELD, PARAMETER })
public @interface AuthenticatedUser { }
ในสิ่งที่คุณAuthenticationFilter
สร้างไว้ด้านบนให้แทรกEvent
คำอธิบายประกอบด้วย@AuthenticatedUser
:
@Inject
@AuthenticatedUser
Event<String> userAuthenticatedEvent;
หากการรับรองความถูกต้องสำเร็จให้ดำเนินการเหตุการณ์ผ่านชื่อผู้ใช้เป็นพารามิเตอร์ (โปรดจำไว้ว่าโทเค็นจะออกให้กับผู้ใช้และโทเค็นจะถูกใช้เพื่อค้นหาตัวระบุผู้ใช้):
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, Servlets และ EJBs ใช้รหัสต่อไปนี้เพื่อฉีดUser
อินสแตนซ์ (อันที่จริงเป็นพร็อกซี CDI):
@Inject
@AuthenticatedUser
User authenticatedUser;
โปรดทราบว่า@Produces
คำอธิบายประกอบCDI นั้นแตกต่างจาก JAX-RS@Produces
คำอธิบายประกอบ :
ให้แน่ใจว่าคุณใช้ CDI @Produces
คำอธิบายประกอบในAuthenticatedUserProducer
ถั่วของคุณ
กุญแจสำคัญในที่นี้คือถั่วที่มีคำอธิบายประกอบ @RequestScoped
ช่วยให้คุณสามารถแบ่งปันข้อมูลระหว่างตัวกรองและถั่วของคุณ หากคุณไม่ต้องการใช้เหตุการณ์คุณสามารถแก้ไขตัวกรองเพื่อจัดเก็บผู้ใช้ที่ได้รับการรับรองความถูกต้องในขอบเขตการร้องขอจากนั้นอ่านจากคลาสทรัพยากร JAX-RS ของคุณ
เมื่อเปรียบเทียบกับวิธีการที่แทนที่ SecurityContext
CDI จะช่วยให้คุณได้รับผู้ใช้ที่ผ่านการตรวจสอบสิทธิ์จากถั่วนอกเหนือจากทรัพยากร JAX-RS และผู้ให้บริการ
สนับสนุนการอนุญาตตามบทบาท
โปรดอ้างอิงคำตอบอื่น ๆ ของฉันสำหรับรายละเอียดเกี่ยวกับวิธีการสนับสนุนการอนุญาตตามบทบาท
การออกโทเค็น
โทเค็นสามารถ:
- ทึบแสง:ไม่เปิดเผยรายละเอียดใด ๆ นอกจากค่าตัวเอง (เช่นสตริงสุ่ม)
- อยู่ในตัวเอง: มีรายละเอียดเกี่ยวกับโทเค็นตัวเอง (เช่น JWT)
ดูรายละเอียดด้านล่าง:
สตริงสุ่มเป็นโทเค็น
โทเค็นสามารถออกได้โดยการสร้างสตริงแบบสุ่มและเก็บไว้ในฐานข้อมูลพร้อมกับตัวระบุผู้ใช้และวันที่หมดอายุ เป็นตัวอย่างที่ดีของวิธีการสร้างสตริงแบบสุ่มในชวาสามารถมองเห็นได้ที่นี่ คุณสามารถใช้:
Random random = new SecureRandom();
String token = new BigInteger(130, random).toString(32);
JWT (โทเค็นเว็บ JSON)
JWT (JSON Web Token) เป็นวิธีมาตรฐานสำหรับการอ้างสิทธิ์อย่างปลอดภัยระหว่างสองฝ่ายและกำหนดโดยRFC 75197519
เป็นโทเค็นในตัวและช่วยให้คุณสามารถเก็บรายละเอียดในการอ้างสิทธิ์ได้ การเรียกร้องเหล่านี้จะถูกเก็บไว้ในส่วนของข้อมูลโทเค็นซึ่งเป็น JSON เข้ารหัสเป็นBase64 นี่คือการอ้างสิทธิ์บางส่วนที่ลงทะเบียนในRFC 7519และสิ่งที่พวกเขาหมายถึง (อ่าน RFC เต็มสำหรับรายละเอียดเพิ่มเติม):
iss
: อาจารย์ใหญ่ที่ออกโทเค็น
sub
: อาจารย์ใหญ่ที่เป็นเรื่องของ JWT
exp
: วันที่หมดอายุสำหรับโทเค็น
nbf
: เวลาที่โทเค็นจะเริ่มได้รับการยอมรับสำหรับการประมวลผล
iat
: เวลาที่ออกโทเค็น
jti
: ตัวระบุที่ไม่ซ้ำกันสำหรับโทเค็น
ระวังว่าคุณจะต้องไม่เก็บข้อมูลที่สำคัญเช่นรหัสผ่านไว้ในโทเค็น
เพย์โหลดสามารถอ่านได้โดยไคลเอนต์และสามารถตรวจสอบความสมบูรณ์ของโทเค็นได้อย่างง่ายดายโดยการตรวจสอบลายเซ็นบนเซิร์ฟเวอร์ ลายเซ็นคือสิ่งที่ป้องกันไม่ให้โทเค็นถูกดัดแปลง
คุณไม่จำเป็นต้องยืนยันโทเค็น JWT หากคุณไม่ต้องการติดตามพวกเขา แม้ว่าจะมีการใช้โทเค็นอย่างต่อเนื่องคุณจะมีโอกาสที่จะทำให้โมฆะและเพิกถอนการเข้าถึงเหล่านั้นได้ ในการติดตามโทเค็น JWT แทนที่จะเก็บโทเค็นทั้งหมดไว้บนเซิร์ฟเวอร์คุณสามารถคงรหัสโทเค็นไว้ได้ (jti
อ้างสิทธิ์) ไว้พร้อมกับรายละเอียดอื่น ๆ เช่นผู้ใช้ที่คุณออกโทเค็นวันหมดอายุเป็นต้น
เมื่อยังคงมีโทเค็นอยู่ให้พิจารณาลบอันเก่าออกเพื่อป้องกันฐานข้อมูลของคุณเพิ่มขึ้นเรื่อย ๆ
ใช้ JWT
มีไลบรารี Java จำนวนหนึ่งที่จะออกและตรวจสอบโทเค็น JWT เช่น:
เพื่อหาแหล่งข้อมูลที่ดีบางส่วนอื่น ๆ ที่จะทำงานร่วมกับ JWT, มีลักษณะที่http://jwt.io
การจัดการการเพิกถอนโทเค็นด้วย JWT
หากคุณต้องการเพิกถอนโทเค็นคุณต้องติดตามพวกเขา คุณไม่จำเป็นต้องเก็บโทเค็นทั้งหมดที่ฝั่งเซิร์ฟเวอร์เก็บเฉพาะตัวระบุโทเค็น (ที่ต้องไม่ซ้ำกัน) และเมตาดาต้าบางส่วนหากคุณต้องการ สำหรับตัวระบุโทเค็นคุณสามารถใช้UUID UUID
jti
ควรใช้การอ้างสิทธิ์เพื่อจัดเก็บตัวระบุโทเค็นบนโทเค็น เมื่อตรวจสอบโทเค็นตรวจสอบให้แน่ใจว่าไม่ได้ถูกเพิกถอนโดยตรวจสอบค่าของการjti
อ้างสิทธิ์กับตัวบ่งชี้โทเค็นที่คุณมีในฝั่งเซิร์ฟเวอร์
เพื่อความปลอดภัยเพิกถอนโทเค็นทั้งหมดสำหรับผู้ใช้เมื่อพวกเขาเปลี่ยนรหัสผ่าน
ข้อมูลเพิ่มเติม
- ไม่สำคัญว่าคุณจะใช้การตรวจสอบความถูกต้องประเภทใด มักจะทำมันอยู่ด้านบนของการเชื่อมต่อ HTTPS เพื่อป้องกันไม่ให้มนุษย์ในกลางโจมตีมนุษย์ในกลางโจมตี
- ดูคำถามนี้จากความปลอดภัยของข้อมูลสำหรับข้อมูลเพิ่มเติมเกี่ยวกับโทเค็น
- ในบทความนี้คุณจะพบข้อมูลที่เป็นประโยชน์เกี่ยวกับการรับรองความถูกต้องตามโทเค็น
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 นี้เป็นอย่างไร