REST API menggunakan Spring Security dan JWT

Cepat atau lambat, setiap pengembang Java akan dihadapkan pada kebutuhan untuk mengimplementasikan aplikasi REST API yang aman. Pada artikel ini saya ingin membagikan implementasi saya dari tugas ini.





1. Apa itu REST?

REST (dari bahasa Inggris. Representational State Transfer) adalah prinsip umum mengatur interaksi aplikasi / situs dengan server menggunakan protokol HTTP.





Diagram di bawah menunjukkan model umum.





Semua interaksi dengan server dikurangi menjadi 4 operasi (4 operasi adalah perlu dan cukup minimum, dalam implementasi tertentu mungkin ada lebih banyak jenis operasi):





  1. ( JSON, XML);





  2. ;





  3. ;









, REST .





2.

REST , . , . .

:





3.

Spring Boot Spring Web, :





  1. Java 8+;





  2. Apache Maven





Spring Security JsonWebToken (JWT).

Lombok.





4.

. Spring Boot REST API .





4.1 Web-

Maven- SpringBootSecurityRest. , Intellij IDEA, Spring Boot DevTools, Lombok Spring Web, pom-.





4.2 pom-xml

pom- :





  1. parent- spring-boot-starter-parent;





  2. spring-boot-starter-web, spring-boot-devtools Lombok.





<?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.3.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com</groupId>
    <artifactId>springbootsecurityrest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springbootsecurityrest</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>15</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-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--Test-->
        <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>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
      
      



4.3 REST

, com.springbootsecurityrest :





  • model – POJO-;





  • repository – , .. , ;





  • service – , , , ( );





  • rest – .





model POJO User.





import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private String login;
    private String password;
    private String firstname;
    private String lastname;
    private Integer age;
}
      
      



repository UserRepository c :





  1. getByLogin – ;





  2. getAll – . Spring , @Repository.





@Repository
public class UserRepository {
  
    private List<User> users;

    public UserRepository() {
        this.users = List.of(
                new User("anton", "1234", "", "", 20),
                new User("ivan", "12345", "", "", 21));
    }

    public User getByLogin(String login) {
        return this.users.stream()
                .filter(user -> login.equals(user.getLogin()))
                .findFirst()
                .orElse(null);
    }

    public List<User> getAll() {
        return this.users;
    }
      
      



service UserService. @Service UserRepository. getAll, getByLogin .





@Service
public class UserService {

    private UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public List<User> getAll() {
        return this.repository.getAll();
    }

    public User getByLogin(String login) {
        return this.repository.getByLogin(login);
    }
}
      
      



UserController rest, UserService getAll. @GetMapping , .





@RestController
public class UserController {

    private UserService service;

    public UserController(UserService service) {
        this.service = service;
    }

    @GetMapping(path = "/users", produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody List<User> getAll() {
        return this.service.getAll();
    }
}
      
      



, , http://localhost:8080/users, , :





5. Spring Security

REST API . , , . Spring Security JWT.





Spring Security Java/JavaEE framework, , , Spring Framework.





JSON Web Token (JWT) — (RFC 7519) , JSON. , - . , , .





5.1

pom-.





<!--Security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>jakarta.xml.bind</groupId>
    <artifactId>jakarta.xml.bind-api</artifactId>
    <version>2.3.3</version>
</dependency>
      
      



5.2

, security JwtTokenRepository CsrfTokenRepository ( org.springframework.security.web.csrf).





:





  1. generateToken;





  2. – saveToken;





  3. – loadToken.





Jwt, .





@Repository
public class JwtTokenRepository implements CsrfTokenRepository {

    @Getter
    private String secret;

    public JwtTokenRepository() {
        this.secret = "springrest";
    }

    @Override
    public CsrfToken generateToken(HttpServletRequest httpServletRequest) {
        String id = UUID.randomUUID().toString().replace("-", "");
        Date now = new Date();
        Date exp = Date.from(LocalDateTime.now().plusMinutes(30)
                .atZone(ZoneId.systemDefault()).toInstant());

        String token = "";
        try {
            token = Jwts.builder()
                    .setId(id)
                    .setIssuedAt(now)
                    .setNotBefore(now)
                    .setExpiration(exp)
                    .signWith(SignatureAlgorithm.HS256, secret)
                    .compact();
        } catch (JwtException e) {
            e.printStackTrace();
            //ignore
        }
        return new DefaultCsrfToken("x-csrf-token", "_csrf", token);
    }

    @Override
    public void saveToken(CsrfToken csrfToken, HttpServletRequest request, HttpServletResponse response) {
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        return null;
    }
}
      
      



secret , , , , ip- . exp , 30 . application.properties.





30 . . , 30 .





    @Override
    public void saveToken(CsrfToken csrfToken, HttpServletRequest request, HttpServletResponse response) {
        if (Objects.nonNull(csrfToken)) {
            if (!response.getHeaderNames().contains(ACCESS_CONTROL_EXPOSE_HEADERS))
                response.addHeader(ACCESS_CONTROL_EXPOSE_HEADERS, csrfToken.getHeaderName());

            if (response.getHeaderNames().contains(csrfToken.getHeaderName()))
                response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
            else
                response.addHeader(csrfToken.getHeaderName(), csrfToken.getToken());
        }
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        return (CsrfToken) request.getAttribute(CsrfToken.class.getName());
    }
      
      



response ( ) headers Access-Control-Expose-Headers.





response, .





    public void clearToken(HttpServletResponse response) {
        if (response.getHeaderNames().contains("x-csrf-token"))
            response.setHeader("x-csrf-token", "");
    }
      
      



5.3 SpringSecurity

JwtCsrfFilter, OncePerRequestFilter ( org.springframework.web.filter). . ( /auth/login), .





public class JwtCsrfFilter extends OncePerRequestFilter {

    private final CsrfTokenRepository tokenRepository;

    private final HandlerExceptionResolver resolver;

    public JwtCsrfFilter(CsrfTokenRepository tokenRepository, HandlerExceptionResolver resolver) {
        this.tokenRepository = tokenRepository;
        this.resolver = resolver;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setAttribute(HttpServletResponse.class.getName(), response);
        CsrfToken csrfToken = this.tokenRepository.loadToken(request);
        boolean missingToken = csrfToken == null;
        if (missingToken) {
            csrfToken = this.tokenRepository.generateToken(request);
            this.tokenRepository.saveToken(csrfToken, request, response);
        }

        request.setAttribute(CsrfToken.class.getName(), csrfToken);
        request.setAttribute(csrfToken.getParameterName(), csrfToken);
        if (request.getServletPath().equals("/auth/login")) {
            try {
                filterChain.doFilter(request, response);
            } catch (Exception e) {
                resolver.resolveException(request, response, null, new MissingCsrfTokenException(csrfToken.getToken()));
            }
        } else {
            String actualToken = request.getHeader(csrfToken.getHeaderName());
            if (actualToken == null) {
                actualToken = request.getParameter(csrfToken.getParameterName());
            }
            try {
                if (!StringUtils.isEmpty(actualToken)) {
                    Jwts.parser()
                            .setSigningKey(((JwtTokenRepository) tokenRepository).getSecret())
                            .parseClaimsJws(actualToken);

                        filterChain.doFilter(request, response);
                } else
                    resolver.resolveException(request, response, null, new InvalidCsrfTokenException(csrfToken, actualToken));
            } catch (JwtException e) {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Invalid CSRF token found for " + UrlUtils.buildFullRequestUrl(request));
                }

                if (missingToken) {
                    resolver.resolveException(request, response, null, new MissingCsrfTokenException(actualToken));
                } else {
                    resolver.resolveException(request, response, null, new InvalidCsrfTokenException(csrfToken, actualToken));
                }
            }
        }
    }
}
      
      



5.4

, . UserService UserDetailsService org.springframework.security.core.userdetails. , .





@Service
public class UserService implements UserDetailsService {

    private UserRepository repository;

    public UserService(UserRepository repository) {
        this.repository = repository;
    }

    public List<User> getAll() {
        return this.repository.getAll();
    }

    public User getByLogin(String login) {
        return this.repository.getByLogin(login);
    }

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        User u = getByLogin(login);
        if (Objects.isNull(u)) {
            throw new UsernameNotFoundException(String.format("User %s is not found", login));
        }
        return new org.springframework.security.core.userdetails.User(u.getLogin(), u.getPassword(), true, true, true, true, new HashSet<>());
    }
}
      
      



UserDetails org.springframework.security.core.userdetails. GrantedAuthority, , , . , UsernameNotFoundException.





5.5

. AuthController getAuthUser. /auth/login, Security , UserService .





@RestController
@RequestMapping("/auth")
public class AuthController {

    private UserService service;

    public AuthController(UserService service) {
        this.service = service;
    }

    @PostMapping(path = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
    public @ResponseBody com.springbootsecurityrest.model.User getAuthUser() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth == null) {
            return null;
        }
        Object principal = auth.getPrincipal();
        User user = (principal instanceof User) ? (User) principal : null;
        return Objects.nonNull(user) ? this.service.getByLogin(user.getUsername()) : null;
    }

}
      
      



5.6

, . GlobalExceptionHandler com.springbootsecurityrest, ResponseEntityExceptionHandler handleAuthenticationException.





401 (UNAUTHORIZED) ErrorInfo.





@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private JwtTokenRepository tokenRepository;

    public GlobalExceptionHandler(JwtTokenRepository tokenRepository) {
        this.tokenRepository = tokenRepository;
    }

    @ExceptionHandler({AuthenticationException.class, MissingCsrfTokenException.class, InvalidCsrfTokenException.class, SessionAuthenticationException.class})
    public ErrorInfo handleAuthenticationException(RuntimeException ex, HttpServletRequest request, HttpServletResponse response){
        this.tokenRepository.clearToken(response);
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        return new ErrorInfo(UrlUtils.buildFullRequestUrl(request), "error.authorization");
    }

    @Getter public class ErrorInfo {
        private final String url;
        private final String info;

        ErrorInfo(String url, String info) {
            this.url = url;
            this.info = info;
        }
    }
}
      
      



5.7 Spring Security.

. com.springbootsecurityrest SpringSecurityConfig, WebSecurityConfigurerAdapter org.springframework.security.config.annotation.web.configuration. : Configuration EnableWebSecurity.





configure(AuthenticationManagerBuilder auth), AuthenticationManagerBuilder UserService, Spring Security .





@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService service;

    @Autowired
    private JwtTokenRepository jwtTokenRepository;

    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    @Bean
    public PasswordEncoder devPasswordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
      @Override
    protected void configure(HttpSecurity http) throws Exception { 
    
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(this.service);
    }

}
      
      



configure(HttpSecurity http):





    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.NEVER)
                .and()
                    .addFilterAt(new JwtCsrfFilter(jwtTokenRepository, resolver), CsrfFilter.class)
                    .csrf().ignoringAntMatchers("/**")
                .and()
                    .authorizeRequests()
                    .antMatchers("/auth/login")
                    .authenticated()
                .and()
                    .httpBasic()
                    .authenticationEntryPoint(((request, response, e) -> resolver.resolveException(request, response, null, e)));
    }
      
      



:





  1. sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER) - ;





  2. addFilterAt(new JwtCsrfFilter(jwtTokenRepository, resolver), CsrfFilter.class).csrf().ignoringAntMatchers("/**") - JwtCsrfFilter , ;





  3. .authorizeRequests().antMatchers("/auth/login").authenticated() /auth/login security. ( ), JwtCsrfFilter;





  4. .httpBasic().authenticationEntryPoint(((request, response, e) -> resolver.resolveException(request, response, null, e))) - GlobalExceptionHandler





6.

Postman. http://localhost:8080/users GET.





Tidak ada token, validasi gagal, kami mendapat pesan dengan status 401.





Kami mencoba masuk dengan data yang salah, menjalankan permintaan http: // localhost: 8080 / auth / login dengan tipe POST, validasi gagal, tidak ada token yang diterima, kesalahan dengan status 401 dikembalikan.





Kami masuk dengan data yang benar, otorisasi selesai, pengguna resmi dan token diterima.





Kami mengulangi permintaan http: // localhost: 8080 / users dengan tipe GET, tetapi dengan token yang diterima di langkah sebelumnya. Kami mendapatkan daftar pengguna dan token yang diperbarui.





Kesimpulan

Artikel ini melihat salah satu contoh penerapan aplikasi REST dengan Spring Security dan JWT. Saya berharap opsi penerapan ini bermanfaat bagi seseorang.





Kode proyek lengkap tersedia di github








All Articles