REST-API mit Spring Security und JWT

Früher oder später wird jeder Java-Entwickler mit der Notwendigkeit konfrontiert sein, eine sichere REST-API-Anwendung zu implementieren. In diesem Artikel möchte ich meine Implementierung dieser Aufgabe teilen.





1. Was ist REST?

REST (aus dem Englischen. Representational State Transfer) ist das allgemeine Prinzip für die Organisation der Interaktion einer Anwendung / Site mit einem Server unter Verwendung des HTTP-Protokolls.





Das folgende Diagramm zeigt das allgemeine Modell.





Jede Interaktion mit dem Server wird auf 4 Vorgänge reduziert (4 ist ein notwendiges und ausreichendes Minimum, in einer bestimmten Implementierung kann es mehr Arten von Vorgängen geben):





  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.





Kein Token, Validierung fehlgeschlagen, wir erhalten eine Nachricht mit dem Status 401.





Wir versuchen, uns mit falschen Daten anzumelden, führen die Anforderung http: // localhost: 8080 / auth / login mit dem POST-Typ aus, Validierung fehlgeschlagen, kein Token empfangen, ein Fehler mit dem Status 401 wurde zurückgegeben.





Wir melden uns mit korrekten Daten an, die Autorisierung ist abgeschlossen, ein autorisierter Benutzer und ein Token werden empfangen.





Wir wiederholen die Anfrage http: // localhost: 8080 / users mit dem Typ GET, aber mit dem empfangenen Token im vorherigen Schritt. Wir erhalten eine Benutzerliste und ein aktualisiertes Token.





Fazit

Dieser Artikel befasste sich mit einem der Beispiele für die Implementierung einer REST-Anwendung mit Spring Security und JWT. Ich hoffe, diese Implementierungsoption wird jemandem nützlich sein.





Der vollständige Projektcode ist auf github verfügbar








All Articles