Xin chào các bạn, ở bài trước mình đã hướng dẫn các bạn cách để tạo một REST API đơn giản rồi ( xem lại tại đây ). Hôm nay mình sẽ hướng dẫn các bạn cách để xác thực một API bằng Spring security và JWT ( JSON Web Token ).
Để đơn giản bài viết mình sẽ chia thành hai phần cho các bạn tiện theo dõi :
- Phần 1: Xây dựng một REST API đơn giản trả về dòng
"Hello".
- Phần 2: Bắt buộc phải xác thực mới sử dụng được API ở trên.
Xây dựng REST API đơn giản với Spring boot
Đầu tiên chúng ta sẽ thêm một số thư viện như bên dưới (ở đây mình dùng maven):
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.learnspring</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringSecurityJWT</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Nếu các bạn chưa biết cách để khởi tại một project Spring nhanh thì có thể tham khảo bài viết cách cài plugin Spring Assistant của mình ở đây.
Sau đó chúng ta thêm lớp SpringSecurityJwtApplication với annotation @SpringBootApplication
package com.learnspring.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SpringSecurityJwtApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityJwtApplication.class, args); } }
Tiếp theo chúng ta sẽ tạo một RestController trả về "Hello"
package com.learnspring.demo.controller; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class HelloController { @RequestMapping(value = "/hello", method = RequestMethod.GET) public ResponseEntity<String> getContent(){ return new ResponseEntity<>("Hello", HttpStatus.OK); } }
Đến đây chúng ta chạy Application lên và dùng Postman để kiểm tra
Xác thực REST API với Spring security
Sau khi đã chạy được API thì tiếp theo chúng ta sẽ xác thực API bằng Spring security và JWT. Bây giờ file pom.xml sẽ trong như sau:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.learnspring</groupId> <artifactId>demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringSecurityJWT</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Tiếp theo chúng ta sẽ tạo ra hai model để nhận request và trả response cho client. Khi request yêu cầu xác thực thì phía người dùng phải gửi lên thông tin username và password. Còn khi trả response thì sẽ trả ra một token, người dùng sử dụng token đó để truy cập vào trang “/hello” ở trên.
package com.learnspring.demo.model; public class JwtRequest { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
package com.learnspring.demo.model; public class JwtResponse { private final String jwttoken; public JwtResponse(String jwttoken) { this.jwttoken = jwttoken; } public String getJwttoken() { return jwttoken; } }
Tiếp theo chúng ta tạo một class JwtUserDetailsService triển khai UserDetailService để Spring Security cung cấp đối tượng UserDetails phục vụ cho việc xác thực. Có một điểm lưu ý ở đây là mình sẽ cung cấp cho chương trình một user (username: tamvo, password: 123). Và password ở đây đã được mã hóa bằng thuật toán Bcrypt.
package com.learnspring.demo.service; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; @Service public class JwtUserDetailsService implements UserDetailsService { private static final String USER_NAME = "tamvo"; private static final String PASSWORD = "$2a$10$slYQmyNdGzTn7ZLBXBChFOC9f6kFjAqPhccnP6DxlWXx2lPk1C3G6"; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { if (USER_NAME.equals(s)){ return new User(USER_NAME, PASSWORD, new ArrayList<>()); } throw new UsernameNotFoundException(s); } }
Tiếp theo chúng ta tạo lớp JwtAuthenticationEntryPoint để gửi mã lỗi về cho người dùng nếu truy cập vào api “/hello” mà chưa được xác thực.
package com.learnspring.demo.config; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"); } }
Tiếp theo sẽ tạo lớp
package com.learnspring.demo.config; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; /** * @author tamvo * @created 13/02/2020 - 3:10 PM */ @Component public class JwtTokenUtil { // Thời gian hiệu lực của token public static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; // Khóa bí mật, token sẽ được mã hóa với khóa này private static final String secret = "12345abcde"; // Lấy tên của user từ token public String getUsernameFromToken(String token) { final Claims claims = getAllClaimsFromToken(token); return claims.getSubject(); } // Lấy ngày hết hiệu lực của token public Date getExpirationDateFromToken(String token) { final Claims claims = getAllClaimsFromToken(token); return claims.getExpiration(); } // Sử dụng khóa ở trên để giải mã token private Claims getAllClaimsFromToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } // Kiểm tra xem token có còn trong thời gian hiệu lực private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(new Date()); } // Tạo một token để trả về cho người dùng public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } // Tạo ra môt token chứa các thông tin: username, ngày khởi tạo, ngày hết hiệu lực, khóa private String doGenerateToken(Map<String, Object> claims, String subject) { return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000)) .signWith(SignatureAlgorithm.HS512, secret).compact(); } // Kiểm tra token có hiệu lực public Boolean validateToken(String token, UserDetails userDetails) { final String username = getUsernameFromToken(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } }
package com.learnspring.demo.config; import com.learnspring.demo.service.JwtUserDetailsService; import io.jsonwebtoken.ExpiredJwtException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @author tamvo * @created 13/02/2020 - 3:42 PM */ @Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private JwtUserDetailsService jwtUserDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); String username = null; String jwtToken = null; // JWT Token is in the form "Bearer token". Remove Bearer word and get // only the Token if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { jwtToken = requestTokenHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { System.out.println("JWT Token has expired"); } } else { logger.warn("JWT Token does not begin with Bearer String"); } // Once we get the token validate it. if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username); // if token is valid configure Spring Security to manually set // authentication if (jwtTokenUtil.validateToken(jwtToken, userDetails)) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); // After setting the Authentication in the context, we specify // that the current user is authenticated. So it passes the // Spring Security Configurations successfully. SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); } }
package com.learnspring.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 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.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * @author tamvo * @created 13/02/2020 - 3:01 PM */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired private UserDetailsService jwtUserDetailsService; @Autowired private JwtRequestFilter jwtRequestFilter; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { // configure AuthenticationManager so that it knows from where to load // user for matching credentials // Use BCryptPasswordEncoder auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/authenticate").permitAll() .anyRequest().authenticated() .and() .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); } }
Các bạn có thể tham khảo project tại đây: https://github.com/tamvo-dev/spring-security-JWT.git
Bài viết của mình đến đây là kết thúc cám ơn các bạn đã theo dõi !
Để lại một bình luận