Ich habe zuvor einen Artikel "Implementieren einer einfachen Rest-API mit Spring Security mit Spring Boot 2.0" geschrieben, aber diesmal ist es mit einem Bildschirm einfach. Ich habe eine Demo-Site erstellt und erneut einen Artikel erstellt.
Den Quellcode finden Sie unter rubytomato / demo-java12-security.
Umgebung
Referenz
Authentifizierungs- / Autorisierungsinformationen werden in der folgenden Benutzertabelle verwaltet.
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT AUTO_INCREMENT COMMENT 'Benutzeridentifikation',
name VARCHAR(60) NOT NULL COMMENT 'Nutzername',
email VARCHAR(120) NOT NULL COMMENT 'Mail Adresse',
password VARCHAR(255) NOT NULL COMMENT 'Passwort',
roles VARCHAR(120) COMMENT 'rollen',
lock_flag BOOLEAN NOT NULL DEFAULT 0 COMMENT 'Sperrflagge 1:Sperren',
disable_flag BOOLEAN NOT NULL DEFAULT 0 COMMENT 'Ungültige Flagge 1:Ungültig',
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 = 'Benutzertabelle';
ALTER TABLE user ADD CONSTRAINT UNIQUE KEY UKEY_user_email (email);
Da bei der Anmeldung eine E-Mail-Adresse und ein Kennwort verwendet werden, bereiten Sie eine Spalte mit E-Mail-Adresse (E-Mail) und Kennwort (Kennwort) vor, um diese Informationen zu speichern. UNIQUE KEY wird in der Spalte E-Mail-Adresse festgelegt, um das Konto anhand der E-Mail-Adresse eindeutig zu identifizieren. Das Kennwort ist nicht eindeutig und speichert den vom Kennwortcodierer von Spring Security gehashten Wert.
Die Zugriffssteuerung für einige Inhalte verwendet die Rolle von Spring Security. In der Rollenspalte werden die dem Konto zugewiesenen Rollenzeichenfolgen durch Kommas getrennt gespeichert. Außerdem wird das Sperrflag (lock_flag) markiert, wenn es vorübergehend einfriert, und das Deaktivierungsflag (disable_flag) wird markiert, wenn es dauerhaft einfriert. Beim Löschen eines Kontos werden die Daten physisch gelöscht, sodass kein Löschflag vorhanden ist.
Darüber hinaus werden andere Attribute als Authentifizierung / Autorisierung in der Benutzerprofiltabelle gespeichert. In diesem Beispiel werden der Spitzname (Spitzname) und das Avatar-Bild (Avatar-Bild) gespeichert. Avatar-Bilder speichern Binärdaten in Spalten vom Typ BLOB und verwalten die Datei selbst nicht.
DROP TABLE IF EXISTS user_profile;
CREATE TABLE user_profile (
id BIGINT AUTO_INCREMENT COMMENT 'Benutzerprofil-ID',
user_id BIGINT NOT NULL COMMENT 'Benutzeridentifikation',
nick_name VARCHAR(60) COMMENT 'Spitzname',
avatar_image MEDIUMBLOB COMMENT 'Avatar Bild',
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 = 'Benutzerprofiltabelle';
ALTER TABLE user_profile ADD CONSTRAINT FOREIGN KEY FKEY_user_profile_id_user_id (user_id) REFERENCES user (id);
Einige Seiten verfügen über eine rollenbasierte Zugriffssteuerung. Es gibt zwei Arten von Brötchen zuzubereiten. Sie können mehrere Einstellungen für ein Konto oder gar keine festlegen.
rollen | Erwartete Verwendung |
---|---|
Keine Rolle, Authentifizierung ist ohne Rolle möglich | |
ROLE_USER | Rollen für allgemeine Benutzer |
ROLE_ADMIN | Rollen für privilegierte Benutzer |
Endpunkt | method | Authentifizierung | rollen | Abbildung Nr | Bemerkungen |
---|---|---|---|---|---|
/ | GET | no | 1 | Startseite und Anmeldeseite | |
/menu | GET | no | 2 | Menüseite | |
/signup | GET | no | 3 | Kontoregistrierungsseite | |
/signup | POST | no | Ausführung des Kontoregistrierungsprozesses nach der Registrierung/Weiterleiten an | ||
/signin | GET | no | 4 | Anmeldeseite | |
/login | POST | no | Anmeldevorgang, Endpunkt, der von Spring Security nach der Anmeldung bereitgestellt wird/Weiterleiten an | ||
/signout | GET | yes | - | 5 | Abmeldeseite |
/logout | POST | yes | - | Abmeldevorgang, von Spring Security bereitgestellte Endpunkte nach dem Abmelden/Weiterleiten an | |
/account/change/password | GET | yes | - | 6 | Seite zum Ändern des Passworts |
/account/change/password | POST | yes | - | Passwortänderungsprozess nach Änderung/Weiterleiten an | |
/account/change/role | GET | yes | - | 7 | Rollenwechselseite |
/account/change/role | POST | yes | - | Rollenwechselverarbeitung nach Änderung/Weiterleiten an | |
/account/change/profile | GET | yes | - | 8 | Seite zum Ändern des Profils |
/account/change/profile | POST | yes | - | Profiländerungsprozess nach Änderung/Weiterleiten an | |
/account/delete | GET | yes | - | 9 | Seite zum Löschen des Kontos |
/account/delete | POST | yes | - | Löschvorgang des Kontos nach dem Löschen/Weiterleiten an | |
/memo | GET | yes | USER, ADMIN | Inhaltsseite für USER- oder ADMIN-Rollen | |
/user | GET | yes | USER | Inhaltsseite der USER-Rolle | |
/admin | GET | yes | ADMIN | Inhaltsseite der ADMIN-Rolle | |
/error/denied | GET | no | Fehlerseite, wenn der Zugriff verweigert wird | ||
/error/invalid | GET | no | Fehlerseite, wenn die Sitzung ungültig ist | ||
/error/expired | GET | no | Fehlerseite, Sitzung abgelaufen |
Verwenden Sie JPA für den Datenbankzugriff. Es gibt keine spezielle Implementierung in Bezug auf Spring Security für den Datenbankzugriff.
Die Implementierung der Entitätsklasse, die der Benutzertabelle entspricht, die Authentifizierungs- / Autorisierungsinformationen verwaltet, ist wie folgt.
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();
}
}
Fügen Sie eine Methode zur Suche nach E-Mail-Adresse hinzu.
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);
}
Spring Security-Konfigurationen erben von der abstrakten Klasse "WebSecurityConfigurerAdapter". Es gibt drei Hauptkonfigurationen: "AuthenticationManager", "WebSecurity" und "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 {
// ...Konfiguration weggelassen(Nachstehend beschrieben)...
}
@Override
public void configure(WebSecurity web) throws Exception {
// ...Konfiguration weggelassen(Nachstehend beschrieben)...
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// ...Konfiguration weggelassen(Nachstehend beschrieben)...
}
}
UserDetails (principal)
Erstellen Sie die von Spring Security verwendete Benutzerinformationsklasse, indem Sie die Schnittstellen "UserDetails" und "CredentialsContainer" implementieren. Das Überschreiben der Methoden "equals" und "hashCode" sollte ebenfalls ordnungsgemäß erfolgen. Wenn Sie diese Methode nicht ordnungsgemäß implementieren, funktionieren mehrere Überprüfungen der Sitzungsverwaltung nicht.
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();
}
}
Wenn Sie keine spezielle Implementierung haben, ist es einfach, die Referenzimplementierung "org.springframework.security.core.userdetails.User" zu erben.
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);
}
}
** Wird von der Handler-Methode des Controllers empfangen **
Sie können ein Objekt der Benutzerinformationsklasse in der Handler-Methode des Endpunkts empfangen, für den eine Authentifizierung erforderlich ist.
Fügen Sie dem empfangenen Argument die Annotation @ AuthenticationPrincipal
hinzu.
@PostMapping(value = "change/password")
public String changePassword(@AuthenticationPrincipal SimpleLoginUser loggedinUser,
@Validated ChangePasswordForm changePasswordForm, BindingResult result, Model model) {
User user = loggedinUser.getUser();
//...Kürzung...
}
Sie können die Benutzerentität auch direkt empfangen.
@AuthenticationPrincipal(expression = "user") User user
** Zugriff von Vorlage (Thymeleaf) **
Sie können mit getPrincipal () auf die Benutzerinformationsklasse (SimpleLoginUser
) zugreifen.
${#authentication.getPrincipal()}
or
${#authentication.principal}
Sie können auch auf die Entitätsklasse Benutzer zugreifen.
${#authentication.getPrincipal().user}
or
${#authentication.principal.user}
UserDetailsService
Erstellen Sie einen spezifischen Code für Spring Security, um die für die Authentifizierung / Autorisierung erforderlichen Benutzerinformationen (SimpleLoginUser
) abzurufen, indem Sie die Schnittstelle UserDetailsService
implementieren.
Die einzige Methode, die überschrieben werden muss, ist "loadUserByUsername ()", die in diesem Beispiel die Benutzerentität von der E-Mail-Adresse abruft und Benutzerinformationen (SimpleLoginUser) basierend auf der Benutzerentität generiert.
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
Überschreiben Sie den Standardauthentifizierungsmanager für die Konfiguration. AuthenticationManager delegiert den eigentlichen Authentifizierungsprozess an AuthenticationProvider. Es gibt verschiedene Implementierungen von AuthenticationProvider, und die Implementierung des Anbieters, der Benutzerinformationen aus der Datenbank abruft, lautet "DaoAuthenticationProvider".
userDetailsService ()
undpasswordEncoder ()
sind die Konfigurationen von DaoAuthenticationProvider
.
Setzen Sie "userDetailsService ()" auf die Klasse ("SimpleUserDetailsService"), die Benutzerinformationen abruft, die die "UserDetailsService" -Schnittstelle implementieren, und setzen Sie "passwordEncoder ()" auf den Kennwortcodierer (Standard ist "BCryptPasswordEncoder").
Wenn Sie "eraseCredentials (true)" festlegen, wird das Kennwort der Benutzerinformationsklasse ("SimpleLoginUser") nach der Authentifizierung auf null gesetzt.
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.eraseCredentials(true)
// ### DaoAuthenticationConfigurer
.userDetailsService(simpleUserDetailsService)
// ### DaoAuthenticationConfigurer
.passwordEncoder(passwordEncoder);
}
Übrigens ist im Fall einer Authentifizierung, die Benutzerinformationen im Speicher hat, die Implementierung wie folgt.
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
Führt die Hauptkonfiguration von Spring Security durch. Da die Menge an Code (Einstellung) zugenommen hat, habe ich sie einzeln aufgelistet, aber die Gliederung ist wie folgt.
HttpSecurity
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.authorizeRequests()
//...Konfiguration der Anforderungsautorisierung weggelassen(Nachstehend beschrieben)...
.and()
.exceptionHandling()
//...Konfiguration für Zugriff verweigert weggelassen(Nachstehend beschrieben)...
.and()
.formLogin()
//...Anmeldekonfiguration weggelassen(Nachstehend beschrieben)...
.and()
.logout()
//...Abmeldekonfiguration weggelassen(Nachstehend beschrieben)...
.and()
.csrf()
//...CSRF-Konfiguration weggelassen(Nachstehend beschrieben)...
.and()
.rememberMe()
//...Remember-Ich Konfiguration weggelassen(Nachstehend beschrieben)...
.and()
.sessionManagement()
//...Sitzungsverwaltungskonfiguration weggelassen(Nachstehend beschrieben)...
;
// @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 Jeder, auch anonym, hat Zugriff.
authenticated Authentifizierten Konten wird Zugriff gewährt.
fullyAuthenticated Konten, die durch andere Methoden als die automatische Anmeldung (Remember-Me) authentifiziert wurden, erhalten Zugriff.
hasRole/hasAnyRole Konten mit der angegebenen Rolle erhalten Zugriff.
Legen Sie die Übergangsziel-URL fest, wenn der Zugriff mit "accessDeniedPage ()" verweigert wird. Wenn Sie auf eine Seite zugreifen, für die eine Authentifizierung in einem anonymen Status erforderlich ist, wird Ihnen der Zugriff nicht verweigert, sondern Sie werden zur Anmeldeseite weitergeleitet. Wenn die Authentifizierung erfolgreich ist, werden Sie zu dieser Seite weitergeleitet.
// ### 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
Sie können anpassen, indem Sie "AccessDeniedHandler" anstelle von "accessDeniedPage ()" implementieren.
Der folgende Code basiert auf 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">Zugriff verweigert</h5>
<p class="mb-0" th:text="${SPRING_SECURITY_403_EXCEPTION.message}">message</p>
</div>
</div>
</div>
Legen Sie die URL der Seite mit dem Anmeldeformular in loginPage ()
fest.
Wenn die Authentifizierung erfolgreich ist und Sie im zweiten Argument von "defaultSuccessUrl ()" "true" setzen, werden Sie immer zur URL des ersten Arguments weitergeleitet. Wenn Sie "false" festlegen, werden Sie zu der URL weitergeleitet, zu der Sie vor der Authentifizierung wechseln wollten.
Wenn die Authentifizierung fehlschlägt, wird die von failUrl ()
festgelegte URL angezeigt.
// ### 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()
Anstelle von "defaultSuccessUrl ()" und "failUrl ()" können Sie "AuthenticationSuccessHandler" bzw. "AuthenticationFailureHandler" implementieren und anpassen.
AuthenticationSuccessHandler
private AuthenticationSuccessHandler successHandler = (req, res, auth) -> {
//Kundenspezifische Bearbeitung
};
AuthenticationFailureHandler
private AuthenticationFailureHandler failureHandler = (req, res, exception) -> {
//Kundenspezifische Bearbeitung
};
Die Aktion des Anmeldeformulars ist die in "loginProcessingUrl ()" festgelegte URL. Zusätzlich zu der für die Authentifizierung erforderlichen E-Mail-Adresse und dem Kennwort gibt es ein Kontrollkästchen, mit dem Sie auswählen können, ob Sie sich automatisch mit der Funktion REMEMBER-ME anmelden möchten.
<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>
Wenn die Authentifizierung fehlschlägt, leiten Sie mit dem Parameter error
( / signin? Error
) zur Anmeldeseite um.
Darüber hinaus hat Spring Security ein Objekt der AuthenticationException-Klasse (oder eine Klasse, die es erbt, die Ausnahme, die ausgelöst wird, wenn die Anmeldeinformationen falsch sind, ist "BadCredentialsException") mit dem Namen "SPRING_SECURITY_LAST_EXCEPTION" im Sitzungsattribut festgelegt.
In der Vorlage mit Thymeleaf wird die Nachricht aus diesen beiden Informationen angezeigt.
<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">Zertifizierung fehlgeschlagen</h5>
<p class="mb-1" th:text="${session['SPRING_SECURITY_LAST_EXCEPTION'].message}">message</p>
</div>
</div>
</div>
Wenn Sie versuchen, sich mit 1 im lock_flag der Benutzertabelle anzumelden, wird die in der Abbildung gezeigte Meldung angezeigt.
Bitte beachten Sie, dass diese Flags bei der Anmeldung überprüft werden und nicht durch das Markieren des angemeldeten Kontos beeinflusst werden.
Die Abmeldung erfolgt über das Formular auf der Abmeldeseite (/ abmelden
). Dies liegt daran, dass bei aktivierter CSRF die Abmeldung (/ logout
) auch vom POST angefordert werden muss.
Wenn die Abmeldung erfolgreich ist, werden Sie zur obersten Seite (/
) weitergeleitet, und die Sitzung wird geändert und die Cookies werden gelöscht.
// ### 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")
Übrigens können Sie sich anstelle von "LogoutSuccessHandler ()" in der Konfiguration mit GET abmelden, indem Sie Folgendes implementieren.
//.logoutSuccessHandler(logoutSuccessHandler)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET"))
LogoutSuccessHandler
Implementieren Sie die Verarbeitung nach erfolgreicher Abmeldung mit "LogoutSuccessHandler ()". In diesem Beispiel wird die Sitzungs-ID geändert und die Seite auf die oberste Seite umgeleitet.
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, "/");
};
Dieses Mal habe ich einen Handler implementiert, aber Sie können die Übergangsziel-URL auch nach dem Abmelden mit "logoutSuccessUrl ()" in der Konfiguration angeben.
// #logoutSuccessUrl: the URL to redirect to after logout occurred
.logoutSuccessUrl("/")
Die Aktion des Abmeldeformulars ist die in "logoutUrl ()" festgelegte URL.
<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
Wenn CSRF aktiviert ist, wird das CSRF-Token automatisch im POST-Methodenformular ausgeblendet. Standardmäßig wird das CSRF-Token in einer Sitzung gespeichert, aber die folgende Implementierung ändert das Speicherziel in ein Cookie.
// ### CsrfConfigurer
.csrf()
.csrfTokenRepository(new CookieCsrfTokenRepository())
Beispiel) Der Parametername lautet "_csrf".
<input type="hidden" name="_csrf" value="18331a72-184e-4651-ae0b-e044283a20b3">
Beispiel) Der Cookie-Name des CSRF-Tokens lautet "XSRF_TOKEN". Standardmäßig hat es das HttpOnly-Attribut.
Sie können den Cookie-Namen mit dem folgenden Code ändern.
CookieCsrfTokenRepository customCsrfTokenRepository() {
CookieCsrfTokenRepository cookieCsrfTokenRepository = new CookieCsrfTokenRepository();
cookieCsrfTokenRepository.setCookieName("NEW-TOKEN-NAME");
return cookieCsrfTokenRepository;
}
Remember-Me
Wenn Sie Remember-Me im Anmeldeformular aktivieren und sich anmelden, wird ein Cookie mit dem Namen REMEMBER ME ausgegeben. Wenn Sie mit diesem Cookie auf die Demo-Site zugreifen, werden Sie automatisch authentifiziert, wenn es anonym ist.
Der Authentifizierungsstatus von Spring Security enthält "authentifiziert" und "vollständig authentifiziert". Wenn Sie automatisch mit Remember-Me angemeldet sind, wird er als "authentifiziert" und "RememberMe" identifiziert und im Falle einer Formularauthentifizierung als "authentifiziert" und "authentifiziert". Als vollständig authentifiziert identifiziert, können automatische Anmeldung und Formularauthentifizierung unterschieden werden.
// ### 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")
Für den Zugriff auf / account / **
ist eine Formularauthentifizierung erforderlich, wenn die Konfiguration der Anforderungsautorisierung wie folgt lautet. Wenn Sie versuchen, während der automatischen Anmeldung durch Remember-Me darauf zuzugreifen, werden Sie zur Anmeldeseite weitergeleitet und müssen die Formularauthentifizierung explizit durchführen.
.authorizeRequests()
.mvcMatchers("/account/**").fullyAuthenticated()
.mvcMatchers("/admin/**").hasRole("ADMIN")
.mvcMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
Darüber hinaus wird das Steuerelement, das eine bestimmte Rolle hat und diese nur zulässt, wenn es sich um eine Formularauthentifizierung handelt, wie folgt implementiert. In diesem Beispiel ist der Zugriff auf "/ admin / **" zulässig, wenn Sie über die ADMIN-Rolle und die Formularauthentifizierung verfügen.
.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")
Verwenden Sie "invalidSessionUrl ()", um die Übergangsziel-URL festzulegen, wenn die Sitzung ungültig ist, dh wenn das Sitzungscookie gelöscht wird, die Sitzungs-ID manipuliert wird oder auf der Serverseite keine Sitzungsinformationen vorhanden sind.
Sie können das Ablaufdatum der Sitzung in der Anwendungseinstellungsdatei festlegen. Wenn Sie Folgendes festlegen, ist die Sitzung ungültig, wenn 30 Minuten oder länger keine Operation ausgeführt wird. Wenn Sie danach arbeiten, werden Sie zu der mit "invalidSessionUrl ()" festgelegten URL umgeleitet.
server:
servlet:
session:
timeout: 30m
Wenn sowohl "invalidSessionUrl ()" als auch "expiredUrl ()" gesetzt sind, scheint "invalidSessionUrl ()" Vorrang zu haben.
Wenn Sie mehrere Anmeldungen mit demselben Konto steuern möchten, können Sie die Anzahl der gleichzeitigen Sitzungen mit "maximumSessions ()" und "maxSessionsPreventsLogin ()" steuern.
Verwenden Sie "maximumSessions ()", um die Anzahl der Sitzungen festzulegen, die gleichzeitig angemeldet werden können, und "maxSessionsPreventsLogin ()", um das Steuerungsverhalten festzulegen.
maxSessionsPreventsLogin | Verhalten |
---|---|
true | Sie können sich nicht über MaximumSessions hinaus anmelden, solange die Sitzung, in der Sie sich zuerst angemeldet haben, gültig ist |
false (default) | Später angemeldete Sitzungen sind gültig, und früher angemeldete Sitzungen, die die maximale Anzahl von Sitzungen überschreiten, werden ungültig. |
Die Registrierung und Löschung von Konten kann in der Spring Security-Konfiguration nicht festgelegt werden, daher wird sie von Grund auf neu implementiert.
Die Implementierung dieser Demo-Site erstellt ein Konto, sobald Sie sich registrieren. Normalerweise ist es erforderlich, sich vorübergehend zu registrieren, ohne sich plötzlich zu registrieren, eine Aktivierungs-E-Mail an die E-Mail-Adresse zum Zeitpunkt der vorübergehenden Registrierung zu senden und dann die Hauptregistrierung durch den aktivierenden Benutzer durchzuführen.
Dies ist ein Formular zur Registrierung eines Kontos.
<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>
Eine Implementierung der Account Service Class. DI das Repository für den Datenbankzugriff, PasswordEncoder für das Hashing von Kennwörtern und AuthenticationManager für die Authentifizierung im Konstruktor.
@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");
}
}
}
** Passwort-Hashing ** Das Kennwort verwendet den vom Spring Security-Kennwortcodierer gehashten Wert.
String encodedPassword = passwordEncoder.encode(rawPassword);
** Entitätspersistenz ** Die Persistenz der Kontodaten verwendet ein Repository. Die Benutzerentität, die beibehalten werden soll, wird für den nachfolgenden Authentifizierungsprozess verwendet.
User storedUser = userRepository.saveAndFlush(User.of(name, email, encodedPassword, roles));
** Zertifizierung **
Programmgesteuerte Authentifizierung mit den registrierten Kontoinformationen.
Erstellen Sie ein Objekt mit Benutzerinformationen (SimpleLoginUser
), das von Spring Security verwendet wird, aus der Benutzerentität, die beibehalten werden soll.
Außerdem wird aus diesem Benutzerinformationsobjekt ein Authentifizierungstoken generiert.
Das erste Argument des Konstruktors "UsernamePasswordAuthenticationToken" ist das "Principal" -Objekt (dh Benutzerinformationen), das zweite Argument ist das "Credentials" -Objekt (dh das Passwort) und das dritte Argument ist "Authorities".
SimpleLoginUser loginUser = new SimpleLoginUser(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, rawPassword, loginUser.getAuthorities());
Authentifizieren Sie sich mit dem generierten Authentifizierungstoken und legen Sie bei Erfolg das Authentifizierungstoken in "SecurityContextHolder" fest. Damit ist die Registrierung und Authentifizierung des Kontos abgeschlossen, und die nachfolgenden Vorgänge sind dieselben wie bei der Authentifizierung des Formulars.
authenticationManager.authenticate(authenticationToken);
if (authenticationToken.isAuthenticated()) {
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
throw new RuntimeException("authenticate failure user:[" + user.toString() + "]");
}
Da aus dem Formular keine Informationen eingegeben werden können, handelt es sich um ein Formular mit nur einer Schaltfläche zum Senden. (CSRF-Token werden automatisch gesetzt.)
<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>
In der Kontodienstklasse wird lediglich die Benutzerentität gelöscht, und die Authentifizierungsinformationen werden auf der Seite des aufrufenden Controllers zerstört.
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
//...Kürzung...
@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);
});
}
}
Anmeldeinformationen werden durch Ausführen der Abmeldeverarbeitung zerstört (Sitzung wird zerstört, Cookies werden gelöscht).
@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">
Anonyme Benutzerbeurteilung
<p sec:authorize="isAnonymous()">anonymous user</p>
<p sec:authorize="!isAnonymous()">non anonymous user</p>
Beurteilung des authentifizierten Benutzers
<p sec:authorize="isAuthenticated()">authenticated user</p>
<p sec:authorize="!isAuthenticated()">non authenticated user</p>
Beurteilung von Benutzern mit Rollen
<p sec:authorize="hasRole('USER')">USER role authenticated user</p>
Benutzerinformationen ausgeben
<p sec:authentication="name">name</p>
Oder
<p sec:authentication="principal.username">username</p>
Principal ist in diesem Fall eine Instanz der Benutzerinformationsklasse (SimpleLoginUser
).
Zusätzlich zu der obigen Beschreibungsmethode können Sie auch die Ausdrucksdienstprogrammobjekte "# authorisation" und "# authentication" verwenden.
#authorization
Ein Dienstprogrammobjekt, das den Authentifizierungsstatus überprüft.
<p th:if=${#authorization.expression('isAnonymous()')}></p>
#authentication
Stellt ein Spring Security-Authentifizierungsobjekt dar.
<p th:text="${#authentication.name}">name</p>
Ich habe Bootstrap 4.3.1 und Material Design für Bootstrap 4.8.10 als CSS-Framework verwendet.
** Nr.1 Top **
Debug-Informationen werden auf der oberen Seite ausgegeben. Insbesondere wird der Status mit dem folgenden Code angezeigt, damit der Benutzer, der auf die obere Seite zugreift, überprüfen kann, in welchem Authentifizierungsstatus er sich befindet.
<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>
** Menü Nr. 2 **
** Nr.3 anmelden **
** Anmeldung Nr. 4 **
** Nr.5 abmelden **
** Nr.6 Passwort ändern **
** Rollenwechsel Nr. 7 **
** Nr.8 Profiländerung **
** Nr.9 Konto löschen **
Recommended Posts