[JAVA] Créez un site de démonstration simple avec Spring Security avec Spring Boot 2.1

Aperçu

J'ai écrit un article "Implémenter une API Rest simple avec Spring Security avec Spring Boot 2.0" avant, mais cette fois c'est simple avec un écran. J'ai créé un site de démonstration, j'ai donc créé à nouveau un article.

Le code source peut être trouvé sur rubytomato / demo-java12-security.

environnement

référence

Principales fonctions du site de démonstration à créer

Gestion des informations d'authentification / d'autorisation

Les informations d'authentification / d'autorisation sont gérées dans le tableau utilisateur suivant.

DROP TABLE IF EXISTS user;
CREATE TABLE user (
  id BIGINT AUTO_INCREMENT                    COMMENT 'Identifiant d'utilisateur',
  name VARCHAR(60) NOT NULL                   COMMENT 'Nom d'utilisateur',
  email VARCHAR(120) NOT NULL                 COMMENT 'adresse mail',
  password VARCHAR(255) NOT NULL              COMMENT 'mot de passe',
  roles VARCHAR(120)                          COMMENT 'rouleau',
  lock_flag BOOLEAN NOT NULL DEFAULT 0        COMMENT 'Drapeau de verrouillage 1:Fermer à clé',
  disable_flag BOOLEAN NOT NULL DEFAULT 0     COMMENT 'Indicateur non valide 1:Invalide',
  create_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  update_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8MB4
COMMENT = 'Table des utilisateurs';

ALTER TABLE user ADD CONSTRAINT UNIQUE KEY UKEY_user_email (email);

Authentification

Étant donné que la connexion utilise une adresse e-mail et un mot de passe, préparez une adresse e-mail (e-mail) et un mot de passe (mot de passe) pour stocker ces informations. UNIQUE KEY est défini dans la colonne d'adresse e-mail pour identifier de manière unique le compte par adresse e-mail. Le mot de passe n'est pas clair et stocke la valeur hachée par le codeur de mot de passe de Spring Security.

Autorisation

Le contrôle d'accès pour certains contenus utilise le rôle de Spring Security. La colonne des rôles stocke les chaînes de rôle affectées au compte, séparées par des virgules. De plus, l'indicateur de verrouillage (lock_flag) est signalé lorsqu'il se bloque temporairement, et l'indicateur de désactivation (disable_flag) est signalé lorsqu'il se fige définitivement. Lors de la suppression d'un compte, les données sont physiquement supprimées, il n'y a donc pas d'indicateur de suppression.

Attributs autres que l'authentification / l'autorisation

En outre, les attributs autres que l'authentification / l'autorisation sont enregistrés dans la table des profils utilisateur. Dans cet exemple, le pseudo (pseudo) et l'image de l'avatar (avatar_image) sont enregistrés. Les images d'avatar stockent des données binaires dans des colonnes de type BLOB et ne gèrent pas le fichier lui-même.

DROP TABLE IF EXISTS user_profile;
CREATE TABLE user_profile (
  id BIGINT AUTO_INCREMENT                    COMMENT 'ID de profil utilisateur',
  user_id BIGINT NOT NULL                     COMMENT 'Identifiant d'utilisateur',
  nick_name VARCHAR(60)                       COMMENT 'surnom',
  avatar_image MEDIUMBLOB                     COMMENT 'Image d'avatar',
  create_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  update_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
  PRIMARY KEY (id)
)
ENGINE = INNODB
DEFAULT CHARSET = UTF8MB4
COMMENT = 'Tableau des profils d'utilisateurs';

ALTER TABLE user_profile ADD CONSTRAINT FOREIGN KEY FKEY_user_profile_id_user_id (user_id) REFERENCES user (id);

rouleau

Certaines pages ont un contrôle d'accès basé sur les rôles. Il existe deux types de rouleaux à préparer. Vous pouvez définir plusieurs paramètres pour un compte ou aucun.

rouleau Utilisation prévue
Pas de roll, l'authentification est possible sans roll
ROLE_USER Rôles pour les utilisateurs généraux
ROLE_ADMIN Rôles des utilisateurs privilégiés

Liste des points de terminaison

point final method Authentification rouleau Numéro de la figure Remarques
/ GET no 1 Première page et page de connexion
/menu GET no 2 Page de menu
/signup GET no 3 Page d'enregistrement du compte
/signup POST no Exécution du processus d'enregistrement de compte, après l'enregistrement/Rediriger vers
/signin GET no 4 Page de connexion
/login POST no Processus de connexion, point de terminaison fourni par Spring Security, après la connexion/Rediriger vers
/signout GET yes - 5 Déconnexion de la page
/logout POST yes - Processus de déconnexion, points de terminaison fournis par Spring Security, après déconnexion/Rediriger vers
/account/change/password GET yes - 6 Page de changement de mot de passe
/account/change/password POST yes - Processus de changement de mot de passe, après changement/Rediriger vers
/account/change/role GET yes - 7 Page de changement de rôle
/account/change/role POST yes - Traitement du changement de rôle, après le changement/Rediriger vers
/account/change/profile GET yes - 8 Page de changement de profil
/account/change/profile POST yes - Processus de changement de profil, après changement/Rediriger vers
/account/delete GET yes - 9 Page de suppression de compte
/account/delete POST yes - Processus de suppression de compte, après suppression/Rediriger vers
/memo GET yes USER, ADMIN Page de contenu du rôle USER ou ADMIN
/user GET yes USER Page de contenu du rôle USER
/admin GET yes ADMIN Page de contenu du rôle ADMIN
/error/denied GET no Page d'erreur, lorsque l'accès est refusé
/error/invalid GET no Page d'erreur, lorsque la session n'est pas valide
/error/expired GET no Page d'erreur, session expirée

Implémentation autour de la sécurité

Accès à la base de données

Utilisez JPA pour accéder à la base de données. Il n'y a pas d'implémentation spéciale liée à Spring Security concernant l'accès aux bases de données.

entité

La mise en œuvre de la classe d'entités correspondant à la table des utilisateurs qui gère les informations d'authentification / autorisation est la suivante.

User


import com.example.demo.auth.UserRolesUtil;
import lombok.Data;
import lombok.ToString;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import java.time.LocalDateTime;

@Entity
@Table(name = "user")
@Data
@ToString(exclude = {"password"})
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(name = "name", length = 60, nullable = false)
  private String name;
  @Column(name = "email", length = 120, nullable = false, unique = true)
  private String email;
  @Column(name = "password", length = 255, nullable = false)
  private String password;
  @Column(name = "roles", length = 120)
  private String roles;
  @Column(name = "lock_flag", nullable = false)
  private Boolean lockFlag;
  @Column(name = "disable_flag", nullable = false)
  private Boolean disableFlag;
  @Column(name = "create_at", nullable = false)
  private LocalDateTime createAt;
  @Column(name = "update_at", nullable = false)
  private LocalDateTime updateAt;

  @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
  private UserProfile userProfile;

  public void setUserProfile(UserProfile userProfile) {
    this.userProfile = userProfile;
    userProfile.setUser(this);
  }

  public String getAvatarImageBase64Encode() {
    return this.userProfile.getAvatarImageBase64Encode();
  }

  @PrePersist
  private void prePersist() {
    this.lockFlag = Boolean.FALSE;
    this.disableFlag = Boolean.FALSE;
    this.createAt = LocalDateTime.now();
    this.updateAt = LocalDateTime.now();
  }

  @PreUpdate
  private void preUpdate() {
    this.updateAt = LocalDateTime.now();
  }

  public static User of(String name, String email, String encodedPassword, String[] roles) {
    return User.of(name, email, encodedPassword, roles, new UserProfile());
  }

  public static User of(String name, String email, String encodedPassword, String[] roles, 
                        UserProfile userProfile) {
    User user = new User();
    user.setName(name);
    user.setEmail(email);
    user.setPassword(encodedPassword);
    String joinedRoles = UserRolesUtil.joining(roles);
    user.setRoles(joinedRoles);
    user.setUserProfile(userProfile);
    return user;
  }

}

UserProfile


import lombok.Data;
import lombok.ToString;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.OneToOne;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Transient;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Base64;

@Entity
@Table(name = "user_profile")
@Data
@ToString(exclude = {"user", "avatarImage", "avatarImageBase64Encode"})
public class UserProfile {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  @Column(name = "nick_name", length = 60)
  private String nickName;
  @Lob
  @Column(name = "avatar_image")
  private byte[] avatarImage;
  @Column(name = "create_at", nullable = false)
  private LocalDateTime createAt;
  @Column(name = "update_at", nullable = false)
  private LocalDateTime updateAt;

  @OneToOne
  @JoinColumn(name = "user_id", nullable = false)
  private User user;

  @Transient
  private String avatarImageBase64Encode;

  public void setAvatarImage(byte[] avatarImage) {
    this.avatarImage = avatarImage;
    this.avatarImageBase64Encode = base64Encode();
  }

  String getAvatarImageBase64Encode() {
    return avatarImageBase64Encode == null ? "" : avatarImageBase64Encode;
  }

  private String base64Encode() {
    return new String(Base64.getEncoder().encode(avatarImage), StandardCharsets.US_ASCII);
  }

  @PostLoad
  private void init() {
    this.avatarImageBase64Encode = base64Encode();
  }

  @PrePersist
  private void prePersist() {
    this.avatarImage = new byte[0];
    this.createAt = LocalDateTime.now();
    this.updateAt = LocalDateTime.now();
  }

  @PreUpdate
  private void preUpdate() {
    this.updateAt = LocalDateTime.now();
  }

}

Dépôt

Ajoutez une méthode de recherche par adresse e-mail.

UserRepository


import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
  Optional<User> findByEmail(String email);
}

Configuration de la sécurité

Les configurations Spring Security héritent de la classe abstraite WebSecurityConfigurerAdapter. Il existe trois configurations principales: ʻAuthenticationManager, WebSecurity et HttpSecurity`.

import com.example.demo.auth.SimpleUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  private SimpleUserDetailsService simpleUserDetailsService;
  private PasswordEncoder passwordEncoder;

  @Autowired
  public void setSimpleUserDetailsService(SimpleUserDetailsService simpleUserDetailsService) {
    this.simpleUserDetailsService = simpleUserDetailsService;
  }

  @Autowired
  public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
    this.passwordEncoder = passwordEncoder;
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  @Bean
  PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // ...Configuration omise(Décrit ci-dessous)...
  }

  @Override
  public void configure(WebSecurity web) throws Exception {
    // ...Configuration omise(Décrit ci-dessous)...
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // ...Configuration omise(Décrit ci-dessous)...
  }

}

UserDetails (principal)

Créez une classe d'informations utilisateur utilisée par Spring Security en implémentant les interfaces ʻUserDetails et CredentialsContainer. Le remplacement des méthodes ʻequals et hashCode doit également être effectué correctement. Si vous n'implémentez pas correctement cette méthode, les vérifications de connexion multiples de la gestion de session ne fonctionneront pas.

SimpleLoginUser


import com.example.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Objects;
import java.util.Set;

@Slf4j
public class SimpleLoginUser implements UserDetails, CredentialsContainer {
  private static final long serialVersionUID = -888887602572409628L;

  private final String username;
  private String password;
  private final Set<GrantedAuthority> authorities;
  private final boolean accountNonExpired;
  private final boolean accountNonLocked;
  private final boolean credentialsNonExpired;
  private final boolean enabled;
  private final User user;

  public SimpleLoginUser(User user) {
    if ((Objects.isNull(user.getEmail()) || "".equals(user.getEmail())) ||
        (Objects.isNull(user.getPassword()) || "".equals(user.getPassword()))) {
      throw new IllegalArgumentException(
          "Cannot pass null or empty values to constructor");
    }
    this.username = user.getEmail();
    this.password = user.getPassword();
    this.authorities = UserRolesUtil.toSet(user.getRoles());
    this.accountNonExpired = true;
    this.accountNonLocked = !user.getLockFlag();
    this.credentialsNonExpired = true;
    this.enabled = !user.getDisableFlag();
    this.user = user;
  }

  @Override
  public String getUsername() {
    return this.username;
  }

  @Override
  public String getPassword() {
    return this.password;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return this.authorities;
  }

  @Override
  public boolean isAccountNonExpired() {
    return this.accountNonExpired;
  }

  @Override
  public boolean isAccountNonLocked() {
    return this.accountNonLocked;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return this.credentialsNonExpired;
  }

  @Override
  public boolean isEnabled() {
    return this.enabled;
  }

  @Override
  public void eraseCredentials() {
    this.password = null;
  }

  public User getUser() {
    return user;
  }

  @Override
  public boolean equals(Object rhs) {
    if (!(rhs instanceof SimpleLoginUser)) {
      return false;
    }
    return this.username.equals(((SimpleLoginUser)rhs).username);
  }

  @Override
  public int hashCode() {
    return this.username.hashCode();
  }

}

Si vous n'avez pas d'implémentation spéciale, il est facile d'hériter de l'implémentation de référence ʻorg.springframework.security.core.userdetails.User`.

public class SimpleLoginUser extends User {

  public SimpleLoginUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
    super(username, password, authorities);
  }

  public SimpleLoginUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
    super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
  }

}

** Reçu par la méthode du gestionnaire du contrôleur **

Vous pouvez recevoir un objet de la classe d'informations utilisateur dans la méthode de gestionnaire du noeud final qui nécessite une authentification. Ajoutez l'annotation @ AuthenticationPrincipal à l'argument reçu.

@PostMapping(value = "change/password")
public String changePassword(@AuthenticationPrincipal SimpleLoginUser loggedinUser,
                             @Validated ChangePasswordForm changePasswordForm, BindingResult result, Model model) {

  User user = loggedinUser.getUser();

  //...réduction...

}

Vous pouvez également recevoir directement l'entité Utilisateur.

@AuthenticationPrincipal(expression = "user") User user

** Accès à partir du modèle (Thymeleaf) **

Vous pouvez accéder à la classe d'informations utilisateur (SimpleLoginUser) avec getPrincipal ().

${#authentication.getPrincipal()}

or

${#authentication.principal}

Vous pouvez également accéder à la classe d'entité User.

${#authentication.getPrincipal().user}

or

${#authentication.principal.user}

UserDetailsService

Créez un code spécifique pour Spring Security afin d'acquérir les informations utilisateur (SimpleLoginUser) requises pour l'authentification / l'autorisation en implémentant l'interface ʻUserDetailsService. La seule méthode qui doit être remplacée est loadUserByUsername ()`, qui dans cet exemple récupère l'entité User à partir de l'adresse e-mail et génère des informations utilisateur (SimpleLoginUser) en fonction de l'entité User.

SimpleUserDetailsService


import com.example.demo.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
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 org.springframework.transaction.annotation.Transactional;

@Service
@Slf4j
public class SimpleUserDetailsService implements UserDetailsService {
  private final UserRepository userRepository;

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

  @Transactional(readOnly = true)
  @Override
  public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
    log.debug("loadUserByUsername(email):[{}]", email);
    return userRepository.findByEmail(email)
        .map(SimpleLoginUser::new)
        .orElseThrow(() -> new UsernameNotFoundException("User not found by email:[" + email + "]"));
  }

}

AuthenticationManager

Remplacez le gestionnaire d'authentification par défaut pour la configuration. AuthenticationManager délègue le processus d'authentification réel à AuthenticationProvider. Il existe plusieurs implémentations d'AuthenticationProvider, et l'implémentation du fournisseur qui récupère les informations utilisateur de la base de données est DaoAuthenticationProvider.

ʻUserDetailsService () et passwordEncoder () sont les configurations de DaoAuthenticationProvider. Définissez ʻuserDetailsService () sur une classe ( SimpleUserDetailsService) qui obtient les informations utilisateur qui implémentent l'interface ʻUserDetailsService, et définissezpasswordEncoder () sur un encodeur de mot de passe (la valeur par défaut est BCryptPasswordEncoder`).

Si ʻeraseCredentials (true) ʻest défini, le mot de passe de la classe d'informations utilisateur (SimpleLoginUser) sera effacé après l'authentification.

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth
    .eraseCredentials(true)
    // ### DaoAuthenticationConfigurer
    .userDetailsService(simpleUserDetailsService)
    // ### DaoAuthenticationConfigurer
    .passwordEncoder(passwordEncoder);
}

À propos, dans le cas d'une authentification qui a des informations utilisateur en mémoire, la mise en œuvre est la suivante.

auth
  // ### InMemoryUserDetailsManagerConfigurer
  .inMemoryAuthentication()
  .withUser("user")
    .password(passwordEncoder.encode("passxxx"))
    .roles("USER")
  .and()
  .withUser("admin")
    .password(passwordEncoder.encode("passyyy"))
    .roles("ADMIN");

WebSecurity

WebSecurity


@Override
public void configure(WebSecurity web) throws Exception {
  // @formatter:off
  web
    .debug(false)
    // ### IgnoredRequestConfigurer
    .ignoring()
      .antMatchers("/images/**", "/js/**", "/css/**")
  ;
  // @formatter:on
}

HttpSecurity

Effectue la configuration principale de Spring Security. Depuis que la quantité de code (paramètre) a augmenté, je les ai énumérés individuellement, mais le plan est le suivant.

HttpSecurity


@Override
protected void configure(HttpSecurity http) throws Exception {
  // @formatter:off
  http
    .authorizeRequests()
      //...Demande de configuration d'autorisation omise(Décrit ci-dessous)...
      .and()
    .exceptionHandling()
      //...Accès refusé à la configuration omise(Décrit ci-dessous)...
      .and()
    .formLogin()
      //...Configuration de connexion omise(Décrit ci-dessous)...
      .and()
    .logout()
      //...Configuration de déconnexion omise(Décrit ci-dessous)...
      .and()
    .csrf()
      //...Configuration CSRF omise(Décrit ci-dessous)...
      .and()
    .rememberMe()
      //...Remember-Configuration moi omise(Décrit ci-dessous)...
      .and()
    .sessionManagement()
      //...Configuration de la gestion de session omise(Décrit ci-dessous)...
  ;
  // @formatter:on
}

Demander l'autorisation

// ### ExpressionUrlAuthorizationConfigurer
.authorizeRequests()
  .mvcMatchers("/", "/signup", "/menu").permitAll()
  .mvcMatchers("/error/**").permitAll()
  .mvcMatchers("/memo/**").hasAnyRole("USER", "ADMIN")
  .mvcMatchers("/account/**").fullyAuthenticated()
  .mvcMatchers("/admin/**").hasRole("ADMIN")
  .mvcMatchers("/user/**").hasRole("USER")
  .anyRequest().authenticated()

permitAll Tout le monde, y compris les anonymes, est autorisé à y accéder.

authenticated Les comptes authentifiés ont accès.

fullyAuthenticated Les comptes qui ont été authentifiés par des méthodes autres que la connexion automatique (Remember-Me) bénéficient d'un accès.

hasRole/hasAnyRole L'accès est accordé aux comptes avec le rôle spécifié.

Accès refusé à la gestion

Définissez l'URL de destination de la transition lorsque l'accès est refusé avec ʻaccessDeniedPage () `. Si vous accédez à une page qui nécessite une authentification dans un état anonyme, l'accès ne vous sera pas refusé, mais vous passerez à la page de connexion et si l'authentification réussit, vous serez redirigé vers cette page.

// ### ExceptionHandlingConfigurer
.exceptionHandling()

  // #accessDeniedUrl: the URL to the access denied page (i.e. /errors/401)
  .accessDeniedPage("/error/denied")

  // #accessDeniedHandler: the {@link AccessDeniedHandler} to be used
  //.accessDeniedHandler(accessDeniedHandler)

AccessDeniedHandler

Vous pouvez personnaliser en implémentant ʻAccessDeniedHandlerau lieu de ʻaccessDeniedPage (). Le code ci-dessous est basé sur ʻorg.springframework.security.web.access.AccessDeniedHandlerImpl`.

private AccessDeniedHandler accessDeniedHandler = (req, res, accessDeniedException) -> {
  if (res.isCommitted()) {
      log.debug("Response has already been committed. Unable to redirect to ");
      return;
  }
  req.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException);
  res.setStatus(HttpStatus.FORBIDDEN.value());
  RequestDispatcher dispatcher = req.getRequestDispatcher("/error/denied");
  dispatcher.forward(req, res);
};
Page d'erreur lors du refus d'accès
<div class="row justify-content-center" th:if="${SPRING_SECURITY_403_EXCEPTION != null}">
    <div class="col">
        <div class="alert alert-warning" role="alert">
            <h5 class="alert-heading">Accès refusé</h5>
            <p class="mb-0" th:text="${SPRING_SECURITY_403_EXCEPTION.message}">message</p>
        </div>
    </div>
</div>

access_denied.png

se connecter

Définissez l'URL de la page avec le formulaire de connexion dans loginPage (). Si l'authentification réussit et que vous définissez «true» dans le deuxième argument de «defaultSuccessUrl ()», vous serez toujours redirigé vers l'URL du premier argument. Si vous définissez false, vous serez redirigé vers l'URL vers laquelle vous tentiez de passer avant l'authentification. Si l'authentification échoue, elle passera à l'URL définie dans failureUrl ().

// ### FormLoginConfigurer
.formLogin()
   // #loginPage: the login page to redirect to if authentication is required (i.e."/login")
  .loginPage("/signin")

   // #loginProcessingUrl: the URL to validate username and password
  .loginProcessingUrl("/login")
  .usernameParameter("email")
  .passwordParameter("password")

  // #defaultSuccessUrl: the default success url
  // #alwaysUse: true if the {@code defaultSuccesUrl} should be used after authentication despite if a protected page had been previously visited
  .defaultSuccessUrl("/", false)

  // #successHandler: the {@link AuthenticationSuccessHandler}.
  //.successHandler(successHandler)

  // #authenticationFailureUrl: the URL to send users if authentication fails (i.e."/login?error").
  .failureUrl("/signin?error")

  // #authenticationFailureHandler: the {@link AuthenticationFailureHandler} to use
  //.failureHandler(failureHandler)

  .permitAll()

Au lieu de defaultSuccessUrl () et failureUrl (), vous pouvez implémenter et personnaliser respectivement ʻAuthenticationSuccessHandler et ʻAuthenticationFailureHandler.

AuthenticationSuccessHandler

private AuthenticationSuccessHandler successHandler = (req, res, auth) -> {
  //Traitement personnalisé
};

AuthenticationFailureHandler

private AuthenticationFailureHandler failureHandler = (req, res, exception) -> {
  //Traitement personnalisé
};
Forme

L'action du formulaire de connexion sera l'URL définie dans loginProcessingUrl (). En plus de l'adresse e-mail et du mot de passe requis pour l'authentification, une case à cocher permet de sélectionner s'il faut se connecter automatiquement avec la fonction REMEMBER-ME.

<form class="text-center" action="#" th:action="@{/login}" method="post">
    <div class="md-form">
        <input type="text" id="email" name="email" class="form-control">
        <label for="email">E-mail</label>
    </div>
    <div class="md-form">
        <input type="password" id="password" name="password" class="form-control">
        <label for="password">Password</label>
    </div>
    <div class="d-flex justify-content-around">
        <div>
            <div class="custom-control custom-checkbox">
                <input type="checkbox" id="remember-me" name="remember-me" value="on" class="custom-control-input">
                <label for="remember-me" class="custom-control-label">Remember me</label>
            </div>
        </div>
        <div>
            <p>Not a member? <a href="/app/signup" th:href="@{/signup}">Register</a></p>
        </div>
    </div>
    <button class="btn indigo accent-4 text-white btn-block my-4" type="submit">Sign in</button>
</form>

signin_form.png

Affichage du message lorsque l'authentification échoue

Si l'authentification échoue, redirigez vers la page de connexion avec le paramètre ʻerror (/ signin? Error). De plus, Spring Security a défini un objet de la classe AuthenticationException (ou une classe qui en hérite, l'exception qui est levée lorsque les informations d'identification sont incorrectes est BadCredentialsException) avec le nom SPRING_SECURITY_LAST_EXCEPTION` dans l'attribut de session.

Dans le modèle utilisant Thymeleaf, le message est affiché à partir de ces deux informations.

<div class="row justify-content-center" th:if="${param['error'] != null && session['SPRING_SECURITY_LAST_EXCEPTION'] != null}">
    <div class="col">
        <div class="alert alert-warning" role="alert">
            <h5 class="alert-heading">la certification a échoué</h5>
            <p class="mb-1" th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}">message</p>
        </div>
    </div>
</div>

signin_form_error.png

Affichage du message lorsque le compte est verrouillé

Si vous essayez de vous connecter avec 1 défini dans le lock_flag de la table des utilisateurs, le message illustré dans la figure s'affiche.

lock.png

Veuillez noter que ces indicateurs sont validés lors de la connexion et ne sont pas affectés par le marquage du compte connecté.

Déconnexion

La déconnexion se fait à partir du formulaire sur la page de déconnexion (/ signout). En effet, si CSRF est activé, la déconnexion (/ logout) doit également être demandée par POST.

Si la déconnexion réussit, vous serez redirigé vers la première page (/), et la session sera modifiée et les cookies seront supprimés.

// ### LogoutConfigurer
.logout()
  // #logoutUrl: the URL that will invoke logout.
  .logoutUrl("/logout")

  // #logoutSuccessUrl: the URL to redirect to after logout occurred
  //.logoutSuccessUrl("/")

  // #logoutSuccessHandler: the {@link LogoutSuccessHandler} to use after a user
  .logoutSuccessHandler(logoutSuccessHandler)

  // #invalidateHttpSession: true if the {@link HttpSession} should be invalidated (default), or false otherwise.
  .invalidateHttpSession(false)

  // #cookieNamesToClear: the names of cookies to be removed on logout success.
  .deleteCookies("JSESSIONID", "XSRF-TOKEN")

D'ailleurs, au lieu de LogoutSuccessHandler () dans la configuration, vous pouvez vous déconnecter avec GET en implémentant comme suit.

//.logoutSuccessHandler(logoutSuccessHandler)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))

LogoutSuccessHandler

Implémentez le traitement après une déconnexion réussie avec LogoutSuccessHandler (). Dans cet exemple, l'ID de session est modifié et la page est redirigée vers la première page.

private LogoutSuccessHandler logoutSuccessHandler = (req, res, auth) -> {
  if (res.isCommitted()) {
    log.debug("Response has already been committed. Unable to redirect to ");
    return;
  }
  if (req.isRequestedSessionIdValid()) {
    log.debug("requestedSessionIdValid session id:{}", req.getRequestedSessionId());
    req.changeSessionId();
  }
  RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
  redirectStrategy.sendRedirect(req, res, "/");
};

Cette fois, j'ai implémenté un gestionnaire, mais vous pouvez également spécifier l'URL de destination de la transition après la déconnexion avec logoutSuccessUrl () dans la configuration.

// #logoutSuccessUrl: the URL to redirect to after logout occurred
.logoutSuccessUrl("/")
Forme

L'action du formulaire de déconnexion sera l'URL définie dans logoutUrl ().

<form class="text-center" action="#" th:action="@{/logout}" method="post">
    <button class="btn indigo accent-4 text-white btn-block my-4" type="submit">Sign out</button>
</form>

signout_form.png

CSRF

Lorsque CSRF est activé, le jeton CSRF est automatiquement défini masqué dans le formulaire de la méthode POST. Par défaut, le jeton CSRF est enregistré dans une session, mais l'implémentation suivante change la destination d'enregistrement en cookie.

// ### CsrfConfigurer
.csrf()
  .csrfTokenRepository(new CookieCsrfTokenRepository())

Exemple) Le nom du paramètre est «_csrf».

<input type="hidden" name="_csrf" value="18331a72-184e-4651-ae0b-e044283a20b3">

Exemple) Le nom du cookie du jeton CSRF est «XSRF_TOKEN». Par défaut, il a l'attribut HttpOnly.

xsrf-token.png

Vous pouvez changer le nom du cookie avec le code ci-dessous.

CookieCsrfTokenRepository customCsrfTokenRepository() {
  CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
  cookieCsrfTokenRepository.setCookieName("NEW-TOKEN-NAME");
  return cookieCsrfTokenRepository;
}

Remember-Me

Si vous cochez Remember-Me sur le formulaire de connexion et que vous vous connectez, un cookie nommé REMEMBER ME sera émis. Si vous accédez au site de démonstration avec ce cookie, vous serez automatiquement authentifié s'il est anonyme.

De plus, il y a «authentifié» et «entièrement authentifié» dans le statut d'authentification de Spring Security, et lorsqu'il est automatiquement connecté avec Remember-Me, il est identifié comme «authentifié» et «RememberMe», et dans le cas de l'authentification par formulaire, «authentifié» et « Identifié comme étant entièrement authentifié », la connexion automatique et l'authentification par formulaire peuvent être distinguées.

// ### RememberMeConfigurer
.rememberMe()
  // #alwaysRemember: set to true to always trigger remember me, false to use the remember-me parameter.
  .alwaysRemember(false)
  // #rememberMeParameter: the HTTP parameter used to indicate to remember the user
  .rememberMeParameter("remember-me")
  // #useSecureCookie: set to {@code true} to always user secure cookies, {@code false} to disable their use.
  .useSecureCookie(true)
  // #rememberMeCookieName: the name of cookie which store the token for remember
  .rememberMeCookieName("REMEMBERME")
  // # Allows specifying how long (in seconds) a token is valid for
  .tokenValiditySeconds(daysToSeconds(3))
  // #key: the key to identify tokens created for remember me authentication
  .key("PgHahck5y6pz7a0Fo#[G)!kt")
Distinguer authentifié et entièrement authentifié

L'authentification par formulaire est requise pour accéder à / account / ** si la configuration de l'autorisation de demande est la suivante. Si vous essayez d'accéder en étant automatiquement connecté par Remember-Me, vous serez redirigé vers la page de connexion et vous devrez explicitement effectuer une authentification par formulaire.

.authorizeRequests()
  .mvcMatchers("/account/**").fullyAuthenticated()
  .mvcMatchers("/admin/**").hasRole("ADMIN")
  .mvcMatchers("/user/**").hasRole("USER")
  .anyRequest().authenticated()

De plus, le contrôle qui a un certain rôle et ne l'autorise que s'il s'agit d'une authentification par formulaire est implémenté comme suit. Dans cet exemple, l'accès à / admin / ** est autorisé si vous avez le rôle ADMIN et l'authentification par formulaire.

  .mvcMatchers("/admin/**").access("hasRole('ADMIN') and isFullyAuthenticated()")

Gestion de session

// ### SessionManagementConfigurer
.sessionManagement()

  .sessionFixation()
    .changeSessionId()

  // #invalidSessionUrl: the URL to redirect to when an invalid session is detected
  .invalidSessionUrl("/error/invalid")

  // ### ConcurrencyControlConfigurer
  // #maximumSessions: the maximum number of sessions for a user
  .maximumSessions(1)
    // #maxSessionsPreventsLogin: true to have an error at time of authentication, else false (default)
    .maxSessionsPreventsLogin(false)
    // #expiredUrl: the URL to redirect to
    .expiredUrl("/error/expired")
Rediriger la destination lorsque la session est désactivée

Définissez l'URL de destination de la transition avec ʻinvalidSessionUrl () `lorsque la session n'est pas valide, c'est-à-dire que lorsque le cookie de session est supprimé, que l'ID de session est falsifié ou qu'il n'y a aucune information de session côté serveur.

Date d'expiration de la session

Vous pouvez définir la date d'expiration de la session dans le fichier de paramètres de l'application. Si vous définissez comme ci-dessous, la session sera invalide s'il n'y a pas d'opération pendant 30 minutes ou plus, et si vous opérez après cela, vous serez redirigé vers l'URL définie par ʻinvalidSessionUrl () `.

server:
  servlet:
    session:
      timeout: 30m

Si ʻinvalidSessionUrl () et ʻexpiredUrl () sont tous les deux définis, ʻinvalidSessionUrl () `semble avoir la priorité.

Contrôle de plusieurs connexions

Si vous souhaitez contrôler plusieurs connexions avec le même compte, vous pouvez contrôler le nombre de sessions simultanées avec maximumSessions () et maxSessionsPreventsLogin ().

Utilisez maximumSessions () pour définir le nombre de sessions pouvant être connectées en même temps et maxSessionsPreventsLogin () pour définir le comportement du contrôle.

maxSessionsPreventsLogin comportement
true Vous ne pouvez pas vous connecter au-delà de maximumSessions tant que la session que vous avez ouverte en premier est valide
false (default) Les sessions connectées ultérieurement seront valides, et les sessions qui ont été connectées plus tôt et dépassent le nombre maximum de sessions seront invalides.

Mise en place de l'enregistrement et de la suppression de compte

L'enregistrement et la suppression de compte ne peuvent pas être définis dans la configuration de Spring Security, ils seront donc mis en œuvre à partir de zéro.

Enregistrement du Compte

La mise en place de ce site de démonstration crée un compte dès votre inscription. Normalement, il est nécessaire de procéder à l'inscription temporaire sans s'inscrire soudainement, d'envoyer un e-mail d'activation à l'adresse e-mail au moment de l'inscription temporaire, puis d'effectuer l'enregistrement principal par l'utilisateur activant.

Forme

Ceci est un formulaire pour créer un compte.

<form class="text-center" action="#" th:action="@{/signup}" th:object="${signupForm}" method="post">
    <div class="md-form">
        <input id="username" class="form-control" type="text" name="username" th:field="*{username}">
        <label for="username">username</label>
        <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}" class="text-danger">error</div>
    </div>
    <div class="md-form">
        <input id="email" class="form-control" type="text" name="email" th:field="*{email}">
        <label for="email">email</label>
        <div th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="text-danger">error</div>
    </div>
    <div class="md-form">
        <input id="password" class="form-control" type="text" name="password" th:field="*{password}">
        <label for="password">password</label>
        <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="text-danger">
            error
        </div>
    </div>
    <div class="md-form">
        <input id="repassword" class="form-control" type="text" name="repassword" th:field="*{repassword}">
        <label for="repassword">(re) password</label>
        <div th:if="${#fields.hasErrors('repassword')}" th:errors="*{repassword}" class="text-danger">error</div>
    </div>
    <div class="text-left justify-content-start">
        <p>roles</p>
        <div class="custom-control custom-checkbox">
            <input id="roles_1" class="custom-control-input" type="checkbox" name="roles" value="ROLE_USER" th:field="*{roles}">
            <label for="roles_1" class="custom-control-label">ROLE_USER</label>
        </div>
        <div class="custom-control custom-checkbox">
            <input id="roles_2" class="custom-control-input" type="checkbox" name="roles" value="ROLE_ADMIN" th:field="*{roles}">
            <label for="roles_2" class="custom-control-label">ROLE_ADMIN</label>
        </div>
        <div th:if="${#fields.hasErrors('roles')}" th:errors="*{roles}" class="text-danger">error</div>
    </div>
    <button class="btn indigo accent-4 text-white btn-block my-4" type="submit">Sign up</button>
</form>

signup_form.png

processus d'inscription

Une implémentation de la classe de service de compte. DI le référentiel pour l'accès à la base de données, PasswordEncoder pour le hachage des mots de passe et AuthenticationManager pour l'authentification dans le constructeur.

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
  private final UserRepository userRepository;
  private final PasswordEncoder passwordEncoder;
  private final AuthenticationManager authenticationManager;

  public AccountServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) {
    this.userRepository = userRepository;
    this.passwordEncoder = passwordEncoder;
    this.authenticationManager = authenticationManager;
  }

  @Transactional
  @Override
  public void register(String name, String email, String rawPassword, String[] roles) {
    log.info("user register name:{}, email:{}, roles:{}", name, email, roles);
    String encodedPassword = passwordEncoder.encode(rawPassword);
    User storedUser = userRepository.saveAndFlush(User.of(name, email, encodedPassword, roles));
    authentication(storedUser, rawPassword);
  }

  private void authentication(User user, String rawPassword) {
    log.info("authenticate user:{}", user);
    SimpleLoginUser loginUser = new SimpleLoginUser(user);
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, rawPassword, loginUser.getAuthorities());
    authenticationManager.authenticate(authenticationToken);
    if (authenticationToken.isAuthenticated()) {
      SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    } else {
      throw new RuntimeException("login failure");
    }
  }

}

** Hachage de mot de passe ** Le mot de passe utilise la valeur hachée par l'encodeur de mot de passe Spring Security.

String encodedPassword = passwordEncoder.encode(rawPassword);

** Persistance d'entité ** La persistance des données de compte utilise un référentiel. L'entité utilisateur à conserver sera utilisée dans le processus d'authentification suivant.

User storedUser = userRepository.saveAndFlush(User.of(name, email, encodedPassword, roles));

** Certification ** Authentifiez-vous par programme avec les informations de compte enregistrées. Créez un objet d'informations utilisateur (SimpleLoginUser) utilisé par Spring Security à partir de l'entité User à conserver. En outre, il générera un jeton d'authentification à partir de cet objet d'informations utilisateur.

Le premier argument du constructeur pour ʻUsernamePasswordAuthenticationToken est l'objet principal(c'est-à-dire les informations utilisateur), le deuxième argument est l'objetcredentials (c'est-à-dire le mot de passe), et le troisième argument est ʻauthorities.

SimpleLoginUser loginUser = new SimpleLoginUser(user);
UsernamePasswordAuthenticationToken authenticationToken =
  new UsernamePasswordAuthenticationToken(loginUser, rawPassword, loginUser.getAuthorities());

Authentifiez-vous avec le jeton d'authentification généré et, en cas de succès, définissez le jeton d'authentification dans SecurityContextHolder. Cela termine l'enregistrement et l'authentification du compte, et les opérations suivantes sont les mêmes que lorsque le formulaire est authentifié.

authenticationManager.authenticate(authenticationToken);
if (authenticationToken.isAuthenticated()) {
  SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
  throw new RuntimeException("authenticate failure user:[" + user.toString() + "]");
}

Suppression de compte

Forme

Puisqu'il n'y a aucune information à saisir à partir du formulaire, ce sera un formulaire avec seulement un bouton d'envoi. (Les jetons CSRF sont définis automatiquement.)

<form class="text-center" action="#" th:action="@{/account/delete}" th:object="${deleteForm}" method="post">
    <button class="btn indigo accent-4 text-white btn-block my-4" type="submit">Delete</button>
</form>

delete_form.png

Supprimer le processus

Dans la classe de service de compte, le processus consiste simplement à supprimer l'entité utilisateur et les informations d'authentification sont détruites du côté du contrôleur appelant.

@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
  //...réduction...

  @Transactional
  @Override
  public void delete(final User user) {
    log.info("delete user:{}", user);
    userRepository.findById(user.getId())
        .ifPresentOrElse(findUser -> {
              userRepository.delete(findUser);
              userRepository.flush();
            },
            () -> {
              log.error("user not found:{}", user);
            });
  }

}

Les informations d'identification sont détruites (la session est détruite, les cookies sont supprimés) en effectuant un traitement de déconnexion.

@PostMapping(value = "delete")
public String delete(@AuthenticationPrincipal SimpleLoginUser user, HttpServletRequest request,
                     DeleteForm deleteForm) {
  log.info("delete form user:{}", user.getUser());
  accountService.delete(user.getUser());
  try {
    request.logout();
  } catch (ServletException e) {
    throw new RuntimeException("delete and logout failure", e);
  }
  return "redirect:/";
}

Supplément

Extension Spring Security de thymeleaf

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

namespace

<html lang="ja"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">

Jugement anonyme de l'utilisateur

<p sec:authorize="isAnonymous()">anonymous user</p>
<p sec:authorize="!isAnonymous()">non anonymous user</p>

Jugement de l'utilisateur authentifié

<p sec:authorize="isAuthenticated()">authenticated user</p>
<p sec:authorize="!isAuthenticated()">non authenticated user</p>

Jugement des utilisateurs avec des rôles

<p sec:authorize="hasRole('USER')">USER role authenticated user</p>

Informations utilisateur de sortie

<p sec:authentication="name">name</p>

Ou

<p sec:authentication="principal.username">username</p>

Le principal dans ce cas est une instance de la classe d'informations utilisateur (SimpleLoginUser).

Une autre façon d'écrire

En plus de la méthode de description ci-dessus, vous pouvez également utiliser les objets utilitaires d'expression # autorisation et # authentication.

#authorization

Un objet utilitaire qui vérifie l'état d'authentification.

<p th:if=${#authorization.expression('isAnonymous()')}></p>

#authentication

Représente un objet d'authentification Spring Security.

<p th:text="${#authentication.name}">name</p>

Capture d'écran de la page

J'ai utilisé Bootstrap 4.3.1 et Material Design pour Bootstrap 4.8.10 comme framework CSS.

** Top n ° 1 ** 1_top.png

Les informations de débogage sont affichées sur la page supérieure. En particulier, l'état est affiché avec le code suivant afin que l'utilisateur accédant à la page d'accueil puisse vérifier dans quel état d'authentification il se trouve.

<tr>
    <th scope="row">Anonymous</th>
    <th:block th:switch="${#authorization.expression('isAnonymous()')}">
        <td th:case="${true}"><span class="badge badge-info">yes</span></td>
        <td th:case="${false}">no</td>
    </th:block>
</tr>
<tr>
    <th>Authenticated</th>
    <th:block th:switch="${#authorization.expression('isAuthenticated()')}">
        <td th:case="${true}"><span class="badge badge-info">yes</span></td>
        <td th:case="${false}">no</td>
    </th:block>
</tr>
<tr>
    <th>FullyAuthenticated</th>
    <th:block th:switch="${#authorization.expression('isFullyAuthenticated()')}">
        <td th:case="${true}"><span class="badge badge-info">yes</span></td>
        <td th:case="${false}">no</td>
    </th:block>
</tr>
<tr>
    <th>RememberMe</th>
    <th:block th:switch="${#authorization.expression('isRememberMe()')}">
        <td th:case="${true}"><span class="badge badge-info">yes</span></td>
        <td th:case="${false}">no</td>
    </th:block>
</tr>

** Menu n ° 2 ** 2_menu.png

** Inscription n ° 3 ** 3_signup.png

** Connexion n ° 4 ** 4_signin.png

** Déconnexion n ° 5 ** 5_signout.png

** N ° 6 Changer le mot de passe ** 6_password_change.png

** Changement de rôle n ° 7 ** 7_role_change.png

** Changement de profil n ° 8 ** 8_profile_change.png

** Suppression du compte n ° 9 ** 9_delete.png

Recommended Posts

Créez un site de démonstration simple avec Spring Security avec Spring Boot 2.1
Créez une application de recherche simple avec Spring Boot
Implémentez une API Rest simple avec Spring Security & JWT avec Spring Boot 2.0
Créez un lot à la demande simple avec Spring Batch
Créez un site Web avec Spring Boot + Gradle (jdk1.8.x)
Créer un serveur API Web avec Spring Boot
Créer un environnement de développement Spring Boot avec docker
Créez un serveur Spring Cloud Config en toute sécurité avec Spring Boot 2.0
Créer un micro service avec Spring Boot
Créez une application avec Spring Boot 2
Créez une application avec Spring Boot
Faisons une API simple avec EC2 + RDS + Spring boot ①
Implémentez un serveur API Web REST simple avec Spring Boot + MySQL
Créez une application Web simple avec Dropwizard
Créer un graphique à barres simple avec MPAndroidChart
J'ai créé un formulaire de recherche simple avec Spring Boot + GitHub Search API.
Obtenez une authentification BASIC avec Spring Boot + Spring Security
Créez un projet de développement d'application Spring Boot avec la commande cURL + tar
Étapes pour créer une application chameau simple avec les démarreurs Apache Camel Spring Boot
Hash des mots de passe avec Spring Boot + Spring Security (avec sel, avec étirement)
Créez un tableau d'affichage simple avec Java + MySQL
Créez une application Spring Boot à l'aide d'IntelliJ IDEA
Créez une application CRUD avec Spring Boot 2 + Thymeleaf + MyBatis
Créez votre propre utilitaire avec Thymeleaf avec Spring Boot
Créer un environnement Spring Boot avec Windows + VS Code
[Introduction à Spring Boot] Fonction d'authentification avec Spring Security
Télécharger avec Spring Boot
Créer un site EC avec Rails 5 ⑨ ~ Créer une fonction de panier ~
Comment créer un projet Spring Boot dans IntelliJ
[Spring Boot] Comment créer un projet (pour les débutants)
Spring Boot avec les paramètres du filtre de sécurité Spring et les points addictifs
[JUnit 5] Ecrivez un test de validation avec Spring Boot! [Test de paramétrage]
Un mémorandum lors de la création d'un service REST avec Spring Boot
Créer Restapi avec Spring Boot (jusqu'à l'exécution de l'application)
J'ai écrit un test avec Spring Boot + JUnit 5 maintenant
J'ai essayé de créer une fonction / écran d'administrateur de site commercial avec Java et Spring
Générer un code à barres avec Spring Boot
Hello World avec Spring Boot
Implémenter GraphQL avec Spring Boot
Démarrez avec Spring Boot
Exécutez LIFF avec Spring Boot
Connexion SNS avec Spring Boot
Téléchargement de fichiers avec Spring Boot
Spring Boot commençant par copie
Créez un terrain de jeu avec Xcode 12
Fonction de connexion avec Spring Security
Hello World avec Spring Boot
Définir des cookies avec Spring Boot
Utiliser Spring JDBC avec Spring Boot
Ajouter un module avec Spring Boot
Premiers pas avec Spring Boot
Essayez d'utiliser Spring Boot Security
Envoyer du courrier avec Spring Boot
Créez un CRUD simple avec SpringBoot + JPA + Thymeleaf ③ ~ Ajouter une validation ~
Une histoire remplie des bases de Spring Boot (résolu)
Créez une application Web Hello World avec Spring Framework + Jetty
J'ai créé un système d'exemple MVC simple à l'aide de Spring Boot
Un nouvel employé a tenté de créer une fonction d'authentification / autorisation à partir de zéro avec Spring Security
Créez un serveur Web simple avec la bibliothèque standard Java com.sun.net.httpserver