Hi everybody 😀
Vào đề luôn. Tại sao người ta lại dùng API ?
Application Programming Interface (API) – Nói đến API chắc hẳn nhiều bạn cũng đã từng nghe đến và từng tạo một API trong công việc hoặc cho riêng mình rồi đúng không 🙂 API giúp chúng ta xây dựng được một hệ thống phần mềm mà không phụ thuộc vào nền tảng, bạn có thể xây dựng 1 webapp, 1 mobile app, desktop app,.. mà tất cả vẫn chỉ sử dụng chung 1 API xử lý nghiệp vụ, không tốn công viết nghiệp vụ nhiều lần. Hay hơn nữa API còn tạo ra một hệ thống quản lý tập trung mà không cần đồng bộ dữ liệu. Trong trường hợp hệ thống quá tải, bạn hoàn toàn có thể auto-scale hệ thống thống theo mô hình của microservice, hoặc đơn giản là scale bằng cách deploy thêm nhiều node API và chạy Load Balancer.
Ngoài ra, API còn giúp các hệ thống khác nhau giao tiếp được với nhau mà không quan trọng về nền tảng xây dựng, một hệ thống xây dựng bằng C# có thể giao tiếp trao đổi thông tin với hệ thống khác viết bằng Java một cách dễ dàng.
Qua từng đó lợi ích cũng đủ để thấy sự hữu dụng của API, đó cũng là lý do mà API được sử dụng rộng khắp mọi nơi ngõ ngách của phát triển phần mềm. Cách đây tầm chục năm, phần lớn các API được tạo ra theo kiểu Soap XML, các bản tin trao đổi giữa các hệ thống bằng định dạng XML, khá rắc rối vì có quá nhiều chuẩn định dạng và version XML, dung lượng bản tin lớn, nên API thường được sử dụng để giao tiếp giữa các hệ thống khác nhau. Còn nội tại back-end với front-end thường xây dựng chung project và gọi hàm trực tiếp. Sau đó, khi xu hướng service hóa phát triển mạnh lên, người ta chuyển dần sang sử dụng JSON thay cho XML với quá nhiều ưu điểm, JSON không phụ thuộc vào version, chuẩn generate,… dung lượng nhẹ hơn XML. Các hệ thống bắt đầu xây dựng theo hướng tách biệt back-end với front-end, giao tiếp với nhau qua API, từ đó việc các hệ thống hiện tại muốn xây dựng thêm nền tảng mobile cũng thuận tiện hơn rất nhiều.
Tại sao lại cần đến JWT ?
Chính vì sự bùng nổ của API, quá nhiều hệ thống khác, front-end khác,… cùng truy cập vào 1 API nên đi kèm đó các nhà phát triển lại phải bỏ công sức tìm cách bảo vệ API của mình khỏi bị những tấn công không mong muốn vừa phải scale hệ thống của mình lên để không bị quá tải. Và cách thông dụng nhất là sizing hệ thống theo chiều ngang (nghĩa là phân tán API, triển khai API trên nhiều node server để phân tải). Như các bạn đã biết, đối với các ứng dụng web thông thường ta vẫn sử dụng session để lưu phiên đăng nhập người dùng, session được lưu trữ cả trên client và server giúp xác thực người dùng trong suốt phiên làm việc. Nhưng đối với ứng dụng mobile, hay các API phân tán trên nhiều node thì việc sử dụng session không còn đáp ứng được nữa vì các node server không thể chia sẻ session với nhau, vậy nên ta cần một khóa định danh người dùng trong mọi request đến API, API sẽ dùng khóa này để kiểm tra client đã xác thực hay chưa. Đó chính là JSON Web Token (JWT).
Dễ hiểu thì JWT là một chuỗi mã hóa thông tin người dùng sau khi đăng nhập, với mọi request từ client đến API đều phải gửi kèm theo JWT, bất kỳ API chạy trên node server nào của hệ thống đó cũng đều có thuật toán giải mã được chuỗi JWT đó và xác định được user đã đăng nhập hay chưa. Vì sự tiện lợi này nên JWT cũng phải mang theo trong nó 1 cái expired date tránh việc xác thực vô thời hạn 😀 Các bạn có thể xem qua hình dưới để hình dung rõ hơn về cách xác thực bằng JWT
Làm thế nào để tích hợp JWT vào API?
Cũng không quá phức tạp vì đây là chuẩn chung của thế giới rồi, các bạn có thể tìm thêm trên internet để hiểu chi tiết, phần dưới đây mình chỉ trình bày các bước cơ bản để tích hợp thôi chứ không giải thích sâu nữa. Phần này mình hướng dẫn dùng maven nhé, bạn nào không dùng maven thì hoàn toàn có thể download gói jar tương ứng về add vào project và sử dụng bình thường.
Bước 1: Add dependency JWT vào project
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
Bước 2: Tạo file TokenJWTUtils.java để xử lý việc sinh chuỗi JWT mới theo username và validate chuỗi JWT có sẵn.
package net.tunghuynh.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.Authentication; import javax.servlet.http.HttpServletRequest; import java.util.Date; import static java.util.Collections.emptyList; /** * net.tunghuynh.utils.TokenJwtUtil * TungHuynh * Date 12/10/2019 9:47 AM */ public class TokenJwtUtil { static final long EXPIRATIONTIME = 86_400_000; // 1 day static final String SECRET = "SecretKeyTungHuynh"; static final String TOKEN_PREFIX = "Bearer"; static final String HEADER_STRING = "Authorization"; public static String generateJwt(String userId) { long expirationTime = EXPIRATIONTIME; return Jwts.builder() .setId(userId) .setExpiration(new Date(System.currentTimeMillis() + expirationTime)) .signWith(SignatureAlgorithm.HS512, SECRET) .compact(); } public static Authentication getAuthentication(HttpServletRequest request) { String token = request.getHeader(HEADER_STRING); if (token != null) { // parse the token Claims claims = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token.replace(TOKEN_PREFIX, "")) .getBody(); String userId = claims.getId(); return userId != null ? new UsernamePasswordAuthenticationToken(userId, emptyList()) : null; } return null; } }
Bước 3: Tạo file JWTFilter.java để bắt giá trị JWT trong header của 1 request để xác thực
package net.tunghuynh.filter; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.filter.GenericFilterBean; import net.tunghuynh.utils.TokenJwtUtil; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * net.tunghuynh.filter.JWTFilter.java * TungHuynh * Date 12/10/2019 9:47 AM */ public class JWTFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { Authentication authentication = TokenJwtUtil.getAuthentication((HttpServletRequest) servletRequest); SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(servletRequest, servletResponse); }catch(Exception e){ String mess = e.toString(); if (mess.matches("(?i)(.*)jwt(.*)")){//Bắn ra HTTP status 401 khi xác thực JWT gặp lỗi ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_UNAUTHORIZED); }else { throw e; } } } }
Bước 4: Cấu hình JWTFilter vào file config của ứng dụng
package net.tunghuynh.config; import net.tunghuynh.filter.JWTFilter; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.access.channel.ChannelProcessingFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * net.tunghuynh.config.WebSecurityConfig.java * TungHuynh * Date 12/10/2019 9:47 AM */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity .cors().and().csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(HttpMethod.GET, "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() .antMatchers(HttpMethod.POST, "/login").permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JWTFilter(), UsernamePasswordAuthenticationFilter.class) // disable page caching .headers().cacheControl(); } }
Ở đây mình có cấu hình antMatchers(HttpMethod.POST, "/login").permitAll()
để cho phép api /login không cần xác thực vẫn có thể sử dụng. Đương nhiên rồi, khi gọi api /login thì đã có JWT đâu mà xác thực 😀
Bước 5: Chốt hạ. Tạo chuỗi JWT khi đăng nhập thành công. Method mô phỏng đăng nhập.
public String login(String username, String password) { //Kiểm tra user/pass trong CSDL //Nếu hợp lệ thì sinh JWT theo userId hoặc username và trả về cho client { String token = TokenJwtUtil.generateJwt(userId); return jwt; } //User/pass không hợp lệ thì đăng nhập không thành công return null; }
Done! Ứng dụng của bạn đã có khả năng sinh JWT sau khi đăng nhập thành công, và xác thực JWT khi có request từ client. Ngoài việc truyền userId vào để sinh JWT thì các bạn có thể sửa lại để truyền thêm cả role của user hoặc gì đó tương tự vào JWT.
Giờ để client gửi được JWT cho API thì ta phải add JWT vào header của bản tin, mình ví dụ khi dùng Postman như sau, các bạn có thể tự tìm hiểu cách add header tương ứng với từng ngôn ngữ JS, Java, C#, Python, CURL,….
Header key: Authorization
Header value: Bearer <Chuỗi JWT>
Lúc đó bản tin request của bạn sẽ có dạng như này
POST /api/get-data HTTP/1.1 Host: localhost:7888 Content-Type: application/json Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImp0aSI6IjEiLCJhdWQiOiIzMTUiLCJleHAiOjE1NjY5NzYxNDR9.xQGRX4wR2i4IAcJ2ABxu479MgXwbllx8GAzcPwPSaVnN2y_JylCsO7Z_zHo9XwWT32FPq2ns7GtDcuZahE2yUw Cache-Control: no-cache Postman-Token: ff98556d-e310-f2a1-e3db-981a70240ecf
Nếu bạn không truyền JWT hoặc quá trình verify JWT gặp lỗi thì bạn sẽ nhận được response HTTP status 401 😀
Nhưng JWT không giải quyết hết được bài toán xác thực và phân quyền, một hệ thống API được public ra cho phép rất nhiều thiết bị hay hệ thống khác truy cập vào, mỗi hệ thống khác đó lại có nhiều role người dùng khác nhau (VD ông Admin page vừa có thể xem bài viết, vừa có quyền sửa xóa bài viết, còn khách ghé thăm chỉ có có thể xem và bình luận bài viết). JWT cũng hỗ trợ việc lưu role người dùng, nhưng khi hệ thống lớn dần, số lượng role và chức năng tăng dần thì chuỗi JWT cũng sẽ phình ra rất nhanh, chưa nói đến việc xác thực quyền chỉ dựa vào thông tin từ client là việc rất nguy hiểm, những kẻ phá hoại hoàn toàn có thể giả mạo được chuỗi JWT full quyền và vào phá hệ thống.
Spring Security làm được gì?
Để giải quyết vấn đề phân quyền cho từng API như trên, kiểm tra quyền chủ động trên server không phụ thuộc vào client thì có rất nhiều cách xử lý từ thủ công đến tự động. Một trong những cách gọn nhẹ đó là sử dụng Spring Security. Việc kết hợp JWT với Spring Security là một sự kết hợp hoàn chỉnh (chưa đến hoàn hảo nhé :v) giúp ta giải quyết các vấn đề trên.
Mời các bạn theo dõi tiếp phần 2 của bài viết này để hiểu rõ hơn về Spring Security nhé 🙂
[Java] Phần 18 – Xác thực và phân quyền API bằng JWT và Spring Security [2/2]
Cảm ơn các bạn đã theo dõi!