[JAVA] Implementieren Sie eine einfache Rest-API mit Spring Security mit Spring Boot 2.0

Überblick

Wir haben eine einfache Rest-API-Demoanwendung mit Spring Security und Spring Boot implementiert. In der ersten Hälfte des Artikels wird die Implementierung rund um Spring Security beschrieben, und in der zweiten Hälfte werden die Controller-Implementierung und ihr Testcode beschrieben.

Den Quellcode finden Sie unter rubytomato / demo-security-spring2.

Umgebung

Referenz

Anforderungen an die Demo-Anwendung

So authentifizieren Sie sich

Diese Demo-Anwendung authentifiziert sich mit Ihrer E-Mail-Adresse und Ihrem Passwort. Insbesondere wird eine POST-Anforderung für eine E-Mail-Adresse, ein Kennwort und ein CSRF-Token an die Anmelde-API gesendet. Wenn eine Authentifizierung möglich ist, werden der HTTP-Status 200, ein Sitzungscookie und ein neues CSRF-Token-Cookie zurückgegeben. Die anschließende Authentifizierung und Autorisierung erfolgt mithilfe von Sitzungscookies.

Informationen zum HTTP-Statuscode

Anwendungen, die Seiten auf der Serverseite rendern, können zu einer Seite umleiten (oder weiterleiten), die durch ein bestimmtes Verhalten bestimmt wird (z. B. nach der Anmeldung zur ursprünglichen Seite zurückkehren, bei Fehlschlagen einer Anforderung zu einer Fehlerseite wechseln). Diese Demo-Anwendung verhält sich grundsätzlich nicht so, und es wird davon ausgegangen, dass eine Umleitung, die erforderlich ist, der Client-Seite überlassen bleibt.

Verhalten HTTP-Statuscode Bemerkungen
Wenn die Anmeldung erfolgreich ist 200 (Ok) Sitzungsgenerierung
CSRF-Token-Generierung
Wenn die Anmeldung fehlschlägt 401 (Unauthorized) Falsches Passwort
Falsche E-Mail Adresse
Wenn die Abmeldung erfolgreich ist 200 (Ok) Sitzung verwerfen
Cookies löschen
Wenn die Abmeldung fehlschlägt 500 (Internal Server Error)
Wenn die API erfolgreich ist 200 (Ok) Authentifiziert, autorisiert und führt die API aus
Antwort das Ergebnis der normalen Beendigung der API
Wenn die API fehlschlägt 400 (Bad Request)
404 (Not Found)
500 (Internal Server Error)
Authentifiziert, autorisiert und führt die API aus
Antwort auf API-Abbruch
API-Authentifizierungsfehler 401 (Unauthorized) Beim Aufruf der API ohne Authentifizierung
Einschließlich Sitzungsablauf, betrügerischer CSRF-Token usw.
API nicht ausgeführt
API-Autorisierungsfehler 403 (Forbidden) Beim Aufruf einer authentifizierten, aber nicht autorisierten API
API nicht ausgeführt

Aufbewahrung von Benutzerinformationen

Benutzerinformationen werden in der USER-Tabelle mit Elementen wie Benutzername, Hash-Passwort, E-Mail-Adresse (eindeutig) und Administrator-Flag gespeichert. Das Admin-Flag (admin_flag) hat eine Rolle für die Benutzeranwendung, 1 für die ADMIN-Rolle und 0 für die USER-Rolle. Es wird davon ausgegangen, dass einige APIs eine Rollenautorisierung durchführen.

CREATE TABLE IF NOT EXISTS `user` (
  id BIGINT AUTO_INCREMENT,
  `name` VARCHAR(128) NOT NULL,
  password VARCHAR(256) NOT NULL,
  email VARCHAR(256) NOT NULL,
  admin_flag BOOLEAN NOT NULL DEFAULT FALSE,
  PRIMARY KEY (id),
  UNIQUE KEY (email)
)
ENGINE = INNODB,
CHARACTER SET = utf8mb4,
COLLATE utf8mb4_general_ci;

Die in der Demoanwendung verwendeten Benutzerdaten sehen folgendermaßen aus: Das Kennwort wird mit der Codierungsmethode der später beschriebenen BCryptPasswordEncoder-Klasse gehasht.

> select * from user;
+----+----------+--------------------------------------------------------------+-----------------------+------------+
| id | name     | password                                                     | email                 | admin_flag |
+----+----------+--------------------------------------------------------------+-----------------------+------------+
|  1 | kamimura | $2a$10$yiIGwxNPWwJ3CZ0SGAq3i.atLYrQNhzTyep1ALi6dbax1b1R2Y.cG | [email protected] |          1 |
|  2 | sakuma   | $2a$10$9jo/FSVljst5xJjuw9eyoumx2iVCUA.uBkUKeBo748bUIaPjypbte | [email protected]   |          0 |
|  3 | yukinaga | $2a$10$1OXUbgiuuIi3SOO3t.jyZOEY66ELL03dRcGpAKWql8HBXOag4YZ8q | [email protected] |          0 |
+----+----------+--------------------------------------------------------------+-----------------------+------------+
3 rows in set (0.00 sec)

Obwohl in dieser Demoanwendung nicht implementiert, wird davon ausgegangen, dass das bei der Registrierung eines neuen Benutzers eingegebene Kennwort mit BCryptPasswordEncoder.encode gehasht und persistent gemacht wird.

PasswordEncoder encoder = new BCryptPasswordEncoder();

User user = User.of(usernme, encoder.encode(rawPassword), email);
userRepository.save(user);

Sicherheitsimplementierung

Spring Security-Konfiguration

Implementieren Sie die Sicherheit in einer Klasse, die WebSecurityConfigurerAdapter erbt. Die einzelnen Implementierungen werden separat beschrieben.

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // AUTHORIZE
            .authorizeRequests()
                .mvcMatchers("/prelogin", "/hello/**")
                    .permitAll()
                .mvcMatchers("/user/**")
                    .hasRole("USER")
                .mvcMatchers("/admin/**")
                    .hasRole("ADMIN")
                .anyRequest()
                    .authenticated()
            .and()
            // EXCEPTION
            .exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint())
                .accessDeniedHandler(accessDeniedHandler())
            .and()
            // LOGIN
            .formLogin()
                .loginProcessingUrl("/login").permitAll()
                    .usernameParameter("email")
                    .passwordParameter("pass")
                .successHandler(authenticationSuccessHandler())
                .failureHandler(authenticationFailureHandler())
            .and()
            // LOGOUT
            .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .logoutSuccessHandler(logoutSuccessHandler())
                //.addLogoutHandler(new CookieClearingLogoutHandler())
            .and()
             // CSRF
            .csrf()
                //.disable()
                //.ignoringAntMatchers("/login")
                .csrfTokenRepository(new CookieCsrfTokenRepository())
            ;
    }

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth,
                                @Qualifier("simpleUserDetailsService") UserDetailsService userDetailsService,
                                PasswordEncoder passwordEncoder) throws Exception {
        auth.eraseCredentials(true)
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    AuthenticationEntryPoint authenticationEntryPoint() {
        return new SimpleAuthenticationEntryPoint();
    }

    AccessDeniedHandler accessDeniedHandler() {
        return new SimpleAccessDeniedHandler();
    }

    AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new SimpleAuthenticationSuccessHandler();
    }

    AuthenticationFailureHandler authenticationFailureHandler() {
        return new SimpleAuthenticationFailureHandler();
    }

    LogoutSuccessHandler logoutSuccessHandler() {
        return new HttpStatusReturningLogoutSuccessHandler();
    }

}

Autorisierungseinstellungen

configuration


.authorizeRequests()
    .mvcMatchers("/prelogin", "/hello/**")
        .permitAll()
    .mvcMatchers("/user/**")
        .hasRole("USER")
    .mvcMatchers("/admin/**")
        .hasRole("ADMIN")
    .anyRequest()
        .authenticated()

Legen Sie die API-Autorisierung fest.

Behandlung von Authentifizierungs- und Autorisierungsausnahmen

configuration


.exceptionHandling()
    .authenticationEntryPoint(authenticationEntryPoint())
    .accessDeniedHandler(accessDeniedHandler())

authenticationEntryPoint()

15.2.1 AuthenticationEntryPoint

Legen Sie die Verarbeitung fest, wenn ein nicht authentifizierter Benutzer auf eine API zugreift, für die eine Authentifizierung erforderlich ist.

AuthenticationEntryPoint authenticationEntryPoint() {
    return new SimpleAuthenticationEntryPoint();
}

Es werden nicht die bereitgestellten Standard- oder Standardimplementierungsklassen verwendet, sondern der Prozess der Rückgabe des HTTP-Status 401 und der Standardnachricht implementiert.

SimpleAuthenticationEntryPoint


public class SimpleAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException exception) throws IOException, ServletException {
        if (response.isCommitted()) {
            log.info("Response has already been committed.");
            return;
        }
        response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
    }

}

Die Standardnachricht sieht folgendermaßen aus: message ist der Wert, der für den zweiten Parameter der sendError-Methode (HttpStatus.UNAUTHORIZED.getReasonPhrase) angegeben wurde.

HTTP/1.1 401
//Kürzung

{
  "timestamp" : "2018-04-08T21:13:24.918+0000",
  "status" : 401,
  "error" : "Unauthorized",
  "message" : "Unauthorized",
  "path" : "/app/memo/1"
}

standard implementations

AuthenticationException

Möglicherweise können Sie den detaillierteren Grund für die Ausnahme in einer Unterklasse von AuthenticationException herausfinden.

accessDeniedHandler()

15.2.2 AccessDeniedHandler

Legt fest, was passiert, wenn ein Benutzer auf eine authentifizierte, aber nicht lizenzierte Ressource zugreift.

AccessDeniedHandler accessDeniedHandler() {
    return new SimpleAccessDeniedHandler();
}

Implementieren Sie anstelle der bereitgestellten Standard- oder Standardimplementierungsklassen einen Handler, der nur den HTTP-Status 403 und eine Standardnachricht zurückgibt.

SimpleAccessDeniedHandler


public class SimpleAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException exception) throws IOException, ServletException {
        response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
    }

}

standard implementations

AccessDeniedException

Ausführlichere Gründe für die Ausnahme finden Sie in der AccessDeniedException-Unterklasse.

Authentifizierung und Verarbeitung bei Erfolg / Misserfolg

15.4.1 Application Flow on Authentication Success and Failure

configuration


.formLogin()
    .loginProcessingUrl("/login").permitAll()
        .usernameParameter("email")
        .passwordParameter("pass")
    .successHandler(authenticationSuccessHandler())
    .failureHandler(authenticationFailureHandler())

loginProcessingUrl()

Legen Sie die Anmeldeseite und den Parameternamen fest. Für den Zugriff auf diese Seite ist keine Authentifizierung erforderlich. (allowAll)

successHandler()

Legen Sie einen Handler fest, der die Verarbeitung implementiert, wenn die Authentifizierung erfolgreich ist.

AuthenticationSuccessHandler authenticationSuccessHandler() {
    return new SimpleAuthenticationSuccessHandler();
}

Implementieren Sie anstelle der bereitgestellten Standard- oder Standardimplementierungsklassen einen Handler, der nur den HTTP-Status 200 zurückgibt.

SimpleAuthenticationSuccessHandler


@Slf4j
public class SimpleAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication auth) throws IOException, ServletException {
        if (response.isCommitted()) {
            log.info("Response has already been committed.");
            return;
        }

        response.setStatus(HttpStatus.OK.value());
        clearAuthenticationAttributes(request);
    }

    /**
     * Removes temporary authentication-related data which may have been stored in the
     * session during the authentication process.
     */
    private void clearAuthenticationAttributes(HttpServletRequest request) {
        HttpSession session = request.getSession(false);

        if (session == null) {
            return;
        }
        session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
    }
}

standard implementations

failureHandler()

Legen Sie einen Handler fest, der die Verarbeitung implementiert, wenn die Authentifizierung fehlschlägt.

AuthenticationFailureHandler authenticationFailureHandler() {
    return new SimpleAuthenticationFailureHandler();
}

Implementieren Sie anstelle der bereitgestellten Standard- oder Standardimplementierungsklassen einen Handler, der nur den HTTP-Status 403 und eine Standardnachricht zurückgibt.

SimpleAuthenticationFailureHandler


public class SimpleAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        response.sendError(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());
    }

}

standard implementations

Verarbeitung beim Abmelden

configuration


.logout()
    .logoutUrl("/logout")
    .invalidateHttpSession(true)
    .deleteCookies("JSESSIONID")
    .logoutSuccessHandler(logoutSuccessHandler())
    //.addLogoutHandler(new CookieClearingLogoutHandler())

logoutUrl()

Legen Sie die Abmeldeseite fest.

logoutSuccessHandler()

Legen Sie einen Handler fest, der die Verarbeitung implementiert, wenn die Abmeldung normal endet. Ich habe HttpStatusReturningLogoutSuccessHandler verwendet, eine Standardimplementierungsklasse von Spring Security, die nur den HTTP-Status zurückgibt. Die beim Abmelden durchgeführte Sitzungszerstörung und das Löschen von Cookies werden in der Konfiguration durchgeführt, sodass keine Implementierung erforderlich ist.

5.5.2 LogoutSuccessHandler

LogoutSuccessHandler logoutSuccessHandler() {
  return new HttpStatusReturningLogoutSuccessHandler();
}

standard implementations

addLogoutHandler()

Ich habe es in dieser Demo-Anwendung nicht verwendet, aber Sie können einen Handler hinzufügen, der ausgeführt wird, wenn die Abmeldung abgeschlossen ist.

standard implementations

CSRF

Standardmäßig ist der CSRF-Schutz aktiviert und enthält das CSRF-Token in HttpSession. Da die Anmelde-API auch CSRF-Maßnahmen unterliegt, ist beim Anmelden ein CSRF-Token erforderlich. Wenn Sie dies jedoch nicht in der Anmelde-API möchten, geben Sie die URL in ignoringAntMatchers an.

configuration


.csrf()
    //.ignoringAntMatchers("/login")
    .csrfTokenRepository(new CookieCsrfTokenRepository())

Wenn Sie CSRF-Gegenmaßnahmen deaktivieren möchten, fügen Sie Deaktivierung hinzu.

configuration


.csrf()
    .disable()

csrfTokenRepository

Wir haben die Standardimplementierungsklasse CookieCsrfTokenRepository verwendet, die CSRF-Token in Cookies enthält.

standard implementations

HEADER

Standardmäßig ist die Cache-Steuerung deaktiviert und die folgenden Header werden festgelegt. Wir haben die Standardeinstellungen für diese Demo-Anwendung beibehalten, da sie nicht angepasst werden müssen.

X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY

Wenn Sie die Cache-Steuerung deaktivieren möchten

configuration


.headers()
    .cacheControl()
        .disable()

Wenn Sie auch andere Optionen deaktivieren möchten

configuration


.headers()
    .cacheControl()
        .disable()
    .frameOptions()
        .disable()
    .xssProtection()
        .disable()
    .contentTypeOptions()
        .disable()

Wenn Sie einen Header hinzufügen möchten

.headers()
    .addHeaderWriter(new StaticHeadersWriter("X-TEST-STATIC-HEADER", "dummy_value"))

Konfiguration des Authentifizierungsprozesses

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth,
    @Qualifier("simpleUserDetailsService") UserDetailsService userDetailsService,
    PasswordEncoder passwordEncoder) throws Exception {

    auth.eraseCredentials(true)
        .userDetailsService(userDetailsService)
        .passwordEncoder(passwordEncoder);

}

UserDetailsService

9.2.2 The UserDetailsService 10.2 UserDetailsService Implementations

Die UserDetailsService-Schnittstelle definiert nur eine Methode, loadUserByUsername. Die Klasse, die diese Schnittstelle implementiert, muss loadUserByUsername überschreiben und alle Anmeldeinformationsklassen zurückgeben, die die UserDetails-Schnittstelle implementieren.

In dieser Demoanwendung werden Benutzerinformationen in der USER-Tabelle der Datenbank gespeichert. Durchsuchen Sie daher die USER-Tabelle mithilfe des UserRepository, wie im folgenden Beispiel gezeigt, und generieren Sie, wenn ein Benutzer gefunden wird, eine Instanz der Authentifizierungsinformationsklasse SimpleLoginUser, die die UserDetails-Schnittstelle implementiert. Und zurück.

@Service("simpleUserDetailsService")
public class SimpleUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public SimpleUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(final String email) {
        //Durchsuchen Sie Benutzerentitäten aus der Datenbank per E-Mail
        return userRepository.findByEmail(email)
                .map(SimpleLoginUser::new)
                .orElseThrow(() -> new UsernameNotFoundException("user not found"));
    }
}

UserDetails

Einige der Benutzerdetails sind unten aufgeführt, da die Erläuterungen auf der Referenzseite leicht verständlich sind.

9.2.2 The UserDetailsService

UserDetails is a core interface in Spring Security. It represents a principal, but in an extensible and application-specific way. Think of UserDetails as the adapter between your own user database and what Spring Security needs inside the SecurityContextHolder.

Benutzer erben, der UserDetails implementiert, und anwendungsspezifische Anmeldeinformationsklasse SimpleLoginUser implementieren. Wenn Sie über die für Ihre Anwendungsanforderungen erforderlichen Informationen verfügen, definieren Sie diese in den Feldern dieser Klasse. Dieses Beispiel definiert eine Instanz der Benutzerentität.

SimpleLoginUser


public class SimpleLoginUser extends org.springframework.security.core.userdetails.User {

    //Benutzerentität
    private com.example.demo.entity.User user;

    public User getUser() {
        return user;
    }

    public SimpleLoginUser(User user) {
        super(user.getName(), user.getPassword(), determineRoles(user.getAdmin()));
        this.user = user;
    }

    private static final List<GrantedAuthority> USER_ROLES = AuthorityUtils.createAuthorityList("ROLE_USER");
    private static final List<GrantedAuthority> ADMIN_ROLES = AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER");

    private static List<GrantedAuthority> determineRoles(boolean isAdmin) {
        return isAdmin ? ADMIN_ROLES : USER_ROLES;
    }
}

Password Encoder

10.3 Password Encoding

Die Standardimplementierungsklasse BCryptPasswordEncoder wurde für die Kennwortkodierung verwendet. Es gibt mehrere andere Standardimplementierungsklassen. Sie können jedoch auch die PasswordEncoder-Schnittstelle implementieren, um den benötigten Encoder zu erstellen, wenn dieser nicht Ihren Anwendungsanforderungen entspricht.

PasswordEncoder


@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

standard implementations

DelegatingPasswordEncoder

Es ist ein Encoder, der aus Spring Security 5.0 implementiert wurde. In der Vergangenheit gab es nach der Entscheidung für den Codierungsalgorithmus das Problem, dass es schwierig war, den übernommenen Algorithmus später zu ändern.

Dieser Codierer delegiert den Prozess an die vorhandene Codierungsklasse, wie der Klassenname andeutet, hängt jedoch die Algorithmus-ID an den Anfang des codierten Hashwerts an. Standardmäßig wird BCryptPasswordEncoder verwendet, sodass {bcrpt} angibt, dass Bcrypt zum Hashwert hinzugefügt wird, wie im folgenden Beispiel gezeigt.

DelegatingPasswordEncoder sucht anhand dieser ID, welche Codierungsklasse verwendet werden soll.

PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
String password = encoder.encode("dummy_password");
System.out.println(password);
// {bcrypt}$2a$10$7qLNvQ7CZ80.VcZGtfe2QuMk7NlWP8ktJyEZoqToo1L7.zi9dIy76

Controller zu implementieren

39.5 Spring MVC and CSRF Integration

Im Argument des Controllers

@GetMapping
public String greeting(@AuthenticationPrincipal(expression = "user") User user, CsrfToken csrfToken) {
    log.debug("token : {}", csrfToken.getToken());
    log.debug("access user : {}", user.toString());

}

Pre-Login-API

Eine API, die das für die Anmelde-API erforderliche CSRF-Token zurückgibt.

method path body content type request body
GET /prelogin
@RestController
@RequestMapping(path = "prelogin")
public class PreLoginController {

    @GetMapping
    public String preLogin(HttpServletRequest request) {
        DefaultCsrfToken token = (DefaultCsrfToken) request.getAttribute("_csrf");
        if (token == null) {
            throw new RuntimeException("could not get a token.");
        }
        return token.getToken();
    }

}

Anmelde- / Abmelde-API

Es ist keine Implementierung erforderlich, da diese in den Sicherheitskonfigurationseinstellungen aktiviert ist.

method path body content type request body
POST /login application/x-www-form-urlencoded email={email}
pass={password}
_csrf={CSRF-TOKEN}
POST /logout

API, die keine Authentifizierung erfordert

Es ist eine API, auf die jeder ohne Authentifizierung oder Autorisierung zugreifen kann.

method path body content type request body
GET /hello
GET /hello/{message}
POST /hello application/x-www-form-urlencoded message={message}

HelloController


@RestController
@RequestMapping(path = "hello")
@Slf4j
public class HelloController {

    @GetMapping
    public String greeting() {
        return "hello world";
    }

    @GetMapping(path = "{message}")
    public String greeting(@PathVariable(name = "message") String message) {
        return "hello " + message;
    }

    @PostMapping
    public String postGreeting(@RequestParam(name = "message") String message) {
        return "hello " + message;
    }

}

API, die eine Authentifizierung erfordert und keine Autorisierung erfordert

Es ist eine API, auf die jeder authentifizierte Benutzer zugreifen kann.

method path body content type request body
GET /memo/1
GET /memo/list
@RestController
@RequestMapping(path = "memo")
public class MemoController {

    private final MemoService service;

    public MemoController(MemoService service) {
        this.service = service;
    }

    @GetMapping(path = "{id}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity<Memo> id(@PathVariable(value = "id") Long id) {
        Optional<Memo> memo = service.findById(id);
        return memo.map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @GetMapping(path = "list", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public ResponseEntity<List<Memo>> list(Pageable page) {
        Page<Memo> memos = service.findAll(page);
        return ResponseEntity.ok(memos.getContent());
    }

}

APIs, die Authentifizierung und USER-Rollen erfordern

Eine API, auf die zugegriffen werden kann, wenn der authentifizierte Benutzer die Rolle USER hat.

method path body content type request body
GET /user
GET /user/echo/{message}
POST /user/echo application/json {"{key}": "{value}"}
@RestController
@RequestMapping(path = "user")
public class UserController {

    @GetMapping
    public String greeting(@AuthenticationPrincipal(expression = "user") User user) {
        return "hello " + user.getName();
    }

    @GetMapping(path = "echo/{message}")
    public String getEcho(@PathVariable(name = "message") String message) {
        return message.toUpperCase();
    }

    @PostMapping(path = "echo", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public String postEcho(@RequestBody Map<String, String> message) {
        return message.toString();
    }

}

Sie können das Authentifizierungsobjekt des authentifizierten Benutzers als Argument der Handler-Methode verwenden.

public String greeting(@AuthenticationPrincipal SimpleLoginUser loginUser) {
    User user = loginUser.getUser();

    //Kürzung
}

Sie können das Objekt direkt abrufen, indem Sie die getter-Methode des Authentifizierungsobjekts (z. B. user for getUser) im Ausdruck angeben.

public String greeting(@AuthenticationPrincipal(expression = "user") User user) {

    //Kürzung
}

APIs, die eine Authentifizierung und eine ADMIN-Rolle erfordern

Eine API, auf die zugegriffen werden kann, wenn der authentifizierte Benutzer die ADMIN-Rolle hat.

method path body content type request body
GET /admin
GET /admin/{username}
GET /admin/echo/{message}
POST /admin/echo application/json {"{key}": "{value}"}
@RestController
@RequestMapping(path = "admin")
public class AdminController {

    private final UserService userService;

    public AdminController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public String greeting(@AuthenticationPrincipal(expression = "user") User user) {
         return "hello admin " + user.getName();
    }

    @GetMapping(path = "{name}")
    public String greeting(@AuthenticationPrincipal(expression = "user") User user, @PathVariable(name = "name") String name) {
         return userService.findByName(name).map(u -> "hello " + u.getName()).orElse("unknown user");
    }

    @GetMapping(path = "echo/{message}")
    public String getEcho(@PathVariable(name = "message") String message) {
        return message.toUpperCase();
    }

    @PostMapping(path = "echo", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public String postEcho(@RequestBody Map<String, String> message) {
        return message.toString();
    }

}

API-Funktionsprüfung

Ich habe die Funktionsweise der API mit dem Befehl curl überprüft. Das Cookie für das Authentifizierungsergebnis wird mit der Option "-c" in einer Textdatei gespeichert, und die Textdatei wird beim Senden mit der Option "-b" angegeben.

API, die keine Authentifizierung erfordert

Für nicht authentifizierte Benutzer

> curl -i "http://localhost:9000/app/hello/world"

HTTP/1.1 200

Selbst wenn für die API keine Authentifizierung erforderlich ist und CSRF unterliegt, sind beim POST das CSRF-Token-Cookie und das CSRF-Token im x-xsrf-Token-Header erforderlich.

NG


> curl -i -X POST "http://localhost:9000/app/hello" -d "message=WORLD"

HTTP/1.1 401

OK


> curl -i -b cookie.txt -X POST "http://localhost:9000/app/hello" -d "message=WORLD" -d "_csrf={CSRF-TOKEN}"

HTTP/1.1 200

Für authentifizierte Benutzer

> curl -i -b cookie.txt "http://localhost:9000/app/hello/world"

HTTP/1.1 200
> curl -i -b cookie.txt -X POST "http://localhost:9000/app/hello" -d "message=WORLD" -d "_csrf={CSRF-TOKEN}"

HTTP/1.1 200

Pre-Login-API

Eine API, die das bei der Anmeldung erforderliche CSRF-TOKEN zurückgibt.

prelogin


> curl -i -c cookie.txt "http://localhost:9000/app/prelogin"

HTTP/1.1 200

{CSRF-TOKEN}

Login-API

Greifen Sie zunächst auf die Pre-Login-API zu und erhalten Sie das CSRF-TOKEN, das beim Anmelden verwendet werden soll.

Für ein gültiges Konto

login


> curl -i -b cookie.txt -c cookie.txt -X POST "http://localhost:9000/app/login" -d "[email protected]" -d "pass=iWKw06pvj" -d "_csrf={CSRF-TOKEN}"

HTTP/1.1 200

Die folgenden Inhalte sind übrigens in cookie.txt geschrieben.

> type cookie.txt

# Netscape HTTP Cookie File
# http://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost     FALSE   /app    FALSE   0       XSRF-TOKEN      d10bdddd-d66d-4cfb-9417-fcdb9a3d4d71
#HttpOnly_localhost     FALSE   /app    FALSE   0       JSESSIONID      99096C52A9CCDC52ED4A15BCB0079CB5

Bei ungültigem Konto (falsche E-Mail-Adresse, falsches Passwort usw.)

login


> curl -i -b cookie.txt -c cookie.txt -X POST "http://localhost:9000/app/login" -d "[email protected]" -d "pass=hogehoge" -d "_csrf={CSRF-TOKEN}"

HTTP/1.1 401

APIs, auf die authentifizierte Benutzer zugreifen können

Für authentifizierte Benutzer

> curl -i -b cookie.txt "http://localhost:9000/app/memo/1"

HTTP/1.1 200

Für nicht authentifizierte Benutzer

> curl -i "http://localhost:9000/app/memo/1"

HTTP/1.1 401

APIs, die Authentifizierung und USER-Rollen erfordern

Für Benutzer mit der Rolle USER

> curl -i -b cookie.txt "http://localhost:9000/app/user"

HTTP/1.1 200

Wenn das CSRF-Token für den Anforderungshauptteil nicht angegeben werden kann, geben Sie es im Header an.

> curl -i -b cookie.txt -H "Content-Type:application/json" -H "x-xsrf-token:{CSRF-TOKEN}" -X POST "http://localhost:9000/app/user/echo" -d "{\"message\": \"abc\"}"

HTTP/1.1 200

Für nicht authentifizierte Benutzer

> curl -i "http://localhost:9000/app/user"

HTTP/1.1 401

APIs, die eine Authentifizierung und eine ADMIN-Rolle erfordern

Für Benutzer mit der ADMIN-Rolle

> curl -i -b cookie.txt "http://localhost:9000/app/admin"

HTTP/1.1 200
> curl -i -b cookie.txt -H "Content-Type:application/json" -H "x-xsrf-token:{CSRF-TOKEN}" -X POST "http://localhost:9000/app/admin/echo" -d "{\"message\": \"abc\"}"

HTTP/1.1 200

Für ungültige CSRF-Token

> curl -i -b cookie.txt -H "Content-Type:application/json" -H "x-xsrf-token:{INVALID-CSRF-TOKEN}" -X POST "http://localhost:9000/app/admin/echo" -d "{\"message\": \"abc\"}"

HTTP/1.1 403

Für Benutzer, die nicht über die ADMIN-Rolle verfügen (Melden Sie sich vor der Überprüfung als Benutzer an, der nicht über die ADMIN-Rolle verfügt.)

> curl -i -b cookie.txt "http://localhost:9000/app/admin"

HTTP/1.1 403

Für nicht authentifizierte Benutzer

> curl -i "http://localhost:9000/app/admin"

HTTP/1.1 401

Abmelde-API

Für authentifizierte Benutzer

logout


> curl -i -b cookie.txt -H "x-xsrf-token:{CSRF-TOKEN}" -X POST "http://localhost:9000/app/logout"

HTTP/1.1 200

Für nicht authentifizierte Benutzer

logout


> curl -i -H "x-xsrf-token:{CSRF-TOKEN}" -X POST "http://localhost:9000/app/logout"

HTTP/1.1 401

Für ungültige CSRF-Token

logout


> curl -i -b cookie.txt -H "x-xsrf-token:{INVALID-CSRF-TOKEN}" -X POST "http://localhost:9000/app/logout"

HTTP/1.1 403

Beschreibung des Testcodes

In diesem Abschnitt werden der Einheits- und Integrationstest der Controller-Klasse der Anwendung mit Spring Security beschrieben.

Test der Steuereinheit

In diesem Artikel wird der Test mit der Annotation MockMvcTest als Komponententest betrachtet. Der verwirrende Teil des Komponententests besteht darin, dass die Basisauthentifizierung aktiviert ist, das von SecurityConfig angepasste Verhalten jedoch nicht berücksichtigt wird. Die Einstellungen für den Testcode ändern sich abhängig davon, ob der sicherheitsrelevante Teil (Inhalt in der SecurityConfig-Klasse festgelegt) auch im Komponententest getestet wird.

Deaktivieren Sie die Spring Security-Funktionen

Wenn Sie ohne Berücksichtigung des Sicherheitsteils testen möchten, können Sie die Spring Security-Funktion deaktivieren, indem Sie im Secure-Attribut der WebMvcTest-Annotation false angeben.

@RunWith(SpringRunner.class)
@WebMvcTest(value = UserController.class, secure = false)
public class UserControllerTests {

    //Testcode

}

Mit dieser Einstellung können APIs, die eine Authentifizierung erfordern, ohne Authentifizierung getestet werden.

@Test
public void getEcho() throws Exception {
    RequestBuilder builder = MockMvcRequestBuilders.get("/user/echo/{message}", "abc")
        .accept(MediaType.TEXT_PLAIN);

    mvc.perform(builder)
        .andExpect(status().isOk())
        .andExpect(content().contentType(contentTypeText))
        .andExpect(content().string("ABC"))
        .andDo(print());
}

Wenn die Handler-Methode jedoch das Authentifizierungsobjekt des Benutzers als Argument verwendet, kann es nicht getestet werden, da das Authentifizierungsobjekt nicht injiziert werden kann.

Beispiel für die Verwendung eines Authentifizierungsobjekts als Argument


@GetMapping
public String greeting(@AuthenticationPrincipal(expression = "user") User user) {

}

Aktivieren Sie einige Spring Security-Funktionen

@RunWith(SpringRunner.class)
@WebMvcTest(value = UserController.class, secure = true)
public class UserControllerTests {

    //

}
@Test
public void greeting() throws Exception {
    //
    User user = new User(1L, "test_user", "pass", "[email protected]", true);
    SimpleLoginUser loginUser = new SimpleLoginUser(user);

    RequestBuilder builder = MockMvcRequestBuilders.get("/user")
        .with(user(loginUser))
        .accept(MediaType.TEXT_PLAIN);

    mvc.perform(builder)
        .andExpect(status().isOk())
        .andExpect(authenticated().withUsername("test_user").withRoles("USER", "ADMIN"))
        .andExpect(content().contentType(contentTypeText))
        .andExpect(content().string("hello aaaa"))
        //.andExpect(cookie().exists("XSRF-TOKEN"))
        .andExpect(forwardedUrl(null))
        .andExpect(redirectedUrl(null))
        .andDo(print());
    }
@WithMockUser(roles = "USER")
@Test
public void getEcho() throws Exception {
    RequestBuilder builder = MockMvcRequestBuilders.get("/user/echo/{message}", "abc")
        .accept(MediaType.TEXT_PLAIN);

    mvc.perform(builder)
        .andExpect(status().isOk())
        .andExpect(content().contentType(contentTypeText))
        .andExpect(content().string("ABC"))
        .andDo(print());
}

Testcode Wenn die Handler-Methode des zu testenden Controllers das Authentifizierungsobjekt des Benutzers als Argument verwendet, können Sie ein Dummy-Authentifizierungsobjekt mit "with (user (...))" angeben. Ich versuche, das CSRF-Token in einem Cookie in SecurityConfig zu speichern, aber das XSRF-TOKEN-Cookie ist nicht vorhanden, da es im Test nicht berücksichtigt wird. Generieren eines Dummy-Authentifizierungsobjekts Wenn die Handler-Methode kein Authentifizierungsobjekt als Argument in einer API verwendet, für die eine Authentifizierung erforderlich ist, müssen Sie nur die WithMockUser-Annotation angeben. Wenn Sie die WithMockUser-Annotation nicht hinzufügen, wird der HTTP-Status 401 zurückgegeben, da er sich in einem nicht authentifizierten Zustand befindet.

@Test
public void getEcho_401() throws Exception {
    RequestBuilder builder = MockMvcRequestBuilders.get("/user/echo/{message}", "abc");

    mvc.perform(builder)
        .andExpect(status().is(HttpStatus.UNAUTHORIZED.value()))
        .andDo(print());
}

Der nächste Test schlägt jedoch fehl, da die Autorisierungseinstellung nicht aktiviert ist. Auf die zu testende API kann nur ein Benutzer mit der Rolle USER zugreifen, aber auch ein Benutzer mit der Rolle ADMIN.

@WithMockUser(roles = "ADMIN")
@Test
public void getEcho_403() throws Exception {
    RequestBuilder builder = MockMvcRequestBuilders.get("/user/echo/{message}", "abc");

    mvc.perform(builder)
        .andExpect(status().is(HttpStatus.FORBIDDEN.value()))
        .andDo(print());
}

Geben Sie "with (csrf ())" für APIs an, für die ein CSRF-Token erforderlich ist, z. B. die POST-Methode. Wenn Sie ein ungültiges CSRF-Token verwenden möchten, verwenden Sie with (csrf (). UseInvalidToken ()).

@WithMockUser(roles = "USER")
@Test
public void postEcho() throws Exception {
    RequestBuilder builder = MockMvcRequestBuilders.post("/user/echo")
        .contentType(MediaType.APPLICATION_JSON_UTF8)
        .content("{\"message\": \"hello world\"}")
        .with(csrf())
        .accept(MediaType.APPLICATION_JSON_UTF8_VALUE);

    mvc.perform(builder)
        .andExpect(status().isOk())
        .andExpect(content().contentType(contentTypeJson))
        .andExpect(content().string("{message=hello world}"))
        .andDo(print());
}

Bitte beachten Sie, dass APIs, für die keine Authentifizierung erforderlich ist, eine Authentifizierung erfordern. Die zu testende API ist in SecurityConfig als API definiert, für die keine Authentifizierung erforderlich ist. Der Test schlägt jedoch ohne die WithMockUser-Annotation fehl.

@RunWith(SpringRunner.class)
@WebMvcTest(value = HelloController.class)
public class HelloControllerTests {

    @Autowired
    private MockMvc mvc;

    final private MediaType contentTypeText = new MediaType(MediaType.TEXT_PLAIN.getType(),
            MediaType.TEXT_PLAIN.getSubtype(), Charset.forName("utf8"));

    @WithMockUser
    @Test
    public void greeting() throws Exception {
        RequestBuilder builder = MockMvcRequestBuilders.get("/hello")
            .accept(MediaType.TEXT_PLAIN);

        mvc.perform(builder)
            .andExpect(status().isOk())
            .andExpect(content().contentType(contentTypeText))
            .andExpect(content().string("hello world"))
            .andDo(print());
    }

}

Reflektieren Sie die SecurityConfig-Einstellungen

Wenn die Unit-Test-Anforderungen auch das enthalten, was Sie in SpringConfig festgelegt haben, importieren Sie die SecurityConfig-Klasse. Bis zu diesem Punkt wird der Test unter fast denselben Bedingungen wie der Integrationstest durchgeführt. Je nach Detaillierungsgrad des Tests ist es daher möglicherweise besser, den Integrationstest zu verwenden.

Für den Import von SecurityConfig ist eine Instanz der Klasse erforderlich, die die UserDetailsService-Schnittstelle implementiert. Daher bereiten wir eine verspottete Instanz mit MockBean vor.

@RunWith(SpringRunner.class)
@WebMvcTest(value = UserController.class)
@Import(value = {SecurityConfig.class})
public class UserControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean(name = "simpleUserDetailsService")
    private UserDetailsService userDetailsService;

    //Testcode

}

Der Autorisierungstest, der oben unter "Aktivieren einiger Spring Security-Funktionen" fehlgeschlagen ist, ist ebenfalls erfolgreich. Der HTTP-Status 403 wird für Benutzer zurückgegeben, die keine zugängliche Rolle haben.

@WithMockUser(roles = "ADMIN")
@Test
public void getEcho_403() throws Exception {
    RequestBuilder builder = MockMvcRequestBuilders.get("/user/echo/{message}", "abc");

    mvc.perform(builder)
        .andExpect(status().is(HttpStatus.FORBIDDEN.value()))
        .andDo(print());
}

Controller-Integrationstest

In diesem Artikel wird der Test mit der SpringBootTest-Annotation als Integrationstest bezeichnet. Im Integrationstest sind die Einstellungen der SecurityConfig-Klasse standardmäßig aktiviert.

Im Unit-Test wurde MockMvc automatisch verdrahtet, aber es scheint, dass es mit der Methode mit Before-Annotation erstellt werden muss, wie im folgenden Code gezeigt.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class UserControllerIntegrationTests {

    @Autowired
    private WebApplicationContext context;

    private MockMvc mvc;

    @Before
    public void setup() {
        mvc = MockMvcBuilders
            .webAppContextSetup(context)
            .apply(springSecurity())
            .build();
    }

    //Testcode

}

Der Testcode entspricht fast dem Unit-Testcode für "Reflect Security Config settings".

@WithMockUser(roles = "USER")
@Test
public void getEcho() throws Exception {
    RequestBuilder builder = MockMvcRequestBuilders.get("/user/echo/{message}", "abc")
        .accept(MediaType.TEXT_PLAIN);

    mvc.perform(builder)
        .andExpect(status().isOk())
        .andExpect(content().contentType(contentTypeText))
        .andExpect(content().string("ABC"))
        .andDo(print());
}

Recommended Posts

Implementieren Sie eine einfache Rest-API mit Spring Security mit Spring Boot 2.0
Implementieren Sie eine einfache Rest-API mit Spring Security & JWT mit Spring Boot 2.0
Implementieren Sie einen einfachen Web-REST-API-Server mit Spring Boot + MySQL
Implementieren Sie die REST-API mit Spring Boot
Erstellen Sie mit Spring Security 2.1 eine einfache Demo-Site mit Spring Security
Implementieren Sie die REST-API mit Spring Boot und JPA (Application Layer).
Implementieren Sie die REST-API mit Spring Boot und JPA (Infrastructure Layer).
Erstellen wir eine einfache API mit EC2 + RDS + Spring Boot ①
Implementieren Sie die REST-API mit Spring Boot und JPA (Domain Layer Edition).
Ich habe ein einfaches Suchformular mit Spring Boot + GitHub Search API erstellt.
Erstellen Sie mit Spring Boot eine einfache Such-App
Erstellen Sie einen Web-API-Server mit Spring Boot
Implementieren Sie GraphQL mit Spring Boot
Hallo Welt (REST API) mit Apache Camel + Spring Boot 2
[Spring Boot] Benutzerinformationen mit Rest API abrufen (Anfänger)
Passen Sie die Antwort auf REST-API-Fehler mit Spring Boot an (Teil 2).
Ein Memorandum beim Erstellen eines REST-Service mit Spring Boot
Passen Sie die Antwort auf REST-API-Fehler mit Spring Boot an (Teil 1).
Erstellen Sie mit Spring Batch eine einfache On-Demand-Charge
Implementieren Sie CRUD mit Spring Boot + Thymeleaf + MySQL
Implementieren Sie die Paging-Funktion mit Spring Boot + Thymeleaf
[Anfänger] Versuchen Sie, die REST-API für die Todo-App mit Spring Boot zu schreiben
Erreichen Sie die BASIC-Authentifizierung mit Spring Boot + Spring Security
Erstellen Sie eine Website mit Spring Boot + Gradle (jdk1.8.x)
Hash-Passwörter mit Spring Boot + Spring Security (mit Salt, mit Stretching)
Versuchen Sie, die Anmeldefunktion mit Spring Boot zu implementieren
[Einführung in Spring Boot] Authentifizierungsfunktion mit Spring Security
Erstellen Sie mit Docker eine Spring Boot-Entwicklungsumgebung
Erstellen Sie mit Spring Boot 2.0 einen Spring Cloud Config Server mit Sicherheit
Mit Spring Boot herunterladen
Ordnen Sie DTO automatisch Entitäten mit der Spring Boot-API zu
Spring Boot mit Spring Security Filter-Einstellungen und Suchtpunkten
[JUnit 5] Schreiben Sie einen Validierungstest mit Spring Boot! [Parametrisierungstest]
Führen Sie swagger-ui in die in Spring Boot implementierte REST-API ein
Ich habe jetzt einen Test mit Spring Boot + JUnit 5 geschrieben
Generieren Sie mit Spring Boot einen Barcode
Hallo Welt mit Spring Boot
Beginnen Sie mit Spring Boot
Führen Sie LIFF mit Spring Boot aus
SNS-Login mit Spring Boot
Datei-Upload mit Spring Boot
Spring Boot beginnt mit dem Kopieren
Anmeldefunktion mit Spring Security
Spring Boot beginnend mit Docker
Hallo Welt mit Spring Boot
Setzen Sie Cookies mit Spring Boot
REST-API-Test mit REST Assured
Verwenden Sie Spring JDBC mit Spring Boot
Modul mit Spring Boot hinzufügen
Erste Schritte mit Spring Boot
API mit Spring + Vue.js verknüpfen
Versuchen Sie es mit Spring Boot Security
Erstellen Sie mit Spring Boot einen Mikrodienst
Mail mit Spring Boot verschicken
Behandeln Sie die Java 8-Datums- und Uhrzeit-API mit Thymeleaf mit Spring Boot
Eine Geschichte voller Grundlagen von Spring Boot (gelöst)
Versuchen Sie, die Springcode-Such-API mit Spring Boot aufzurufen
Ich habe mit Spring Boot ein einfaches MVC-Beispielsystem erstellt
Hash beim Spring-Boot das Passwort und verwenden Sie die Mitgliederregistrierung und die Spring-Sicherheit, um die Anmeldefunktion zu implementieren.
Lassen Sie uns herausfinden, wie Sie mit Request Body mit der REST-API von Spring Boot empfangen können
Implementierte Authentifizierungsfunktion mit Spring Security ②