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
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);
É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.
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.
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);
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 |
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 |
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.
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();
}
}
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);
}
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éfinissez
passwordEncoder () 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
}
// ### 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é.
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);
};
<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>
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é
};
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>
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>
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.
Veuillez noter que ces indicateurs sont validés lors de la connexion et ne sont pas affectés par le marquage du compte connecté.
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("/")
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>
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.
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")
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()")
// ### 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")
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.
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é.
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. |
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.
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.
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>
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'objet
credentials (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() + "]");
}
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>
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:/";
}
<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
).
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>
J'ai utilisé Bootstrap 4.3.1 et Material Design pour Bootstrap 4.8.10 comme framework CSS.
** Top n ° 1 **
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 **
** Inscription n ° 3 **
** Connexion n ° 4 **
** Déconnexion n ° 5 **
** N ° 6 Changer le mot de passe **
** Changement de rôle n ° 7 **
** Changement de profil n ° 8 **
** Suppression du compte n ° 9 **
Recommended Posts