การรับรองความถูกต้องตามโทเค็นทำงานอย่างไร
ในการตรวจสอบ 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 นี้เป็นอย่างไร