Während ich meine Bewerbung schrieb, stieß ich auf einen Mangel an klaren Artikeln darüber, wie man den Benutzer dazu bringen kann, sich sowohl per E-Mail als auch über soziale Netzwerke zu registrieren. Es gab gute Tutorials zum Einrichten des klassischen Anmeldeformulars. Es gab gute Tutorials zu OAuth2 . Es gab erschreckend wenige Informationen darüber, wie man die beiden Methoden kombinieren könnte. Während des Suchprozesses konnten wir eine praktikable Lösung finden. Es erhebt nicht den Anspruch, die ultimative Wahrheit zu sein, aber es erfüllt seine Funktion. In diesem Artikel werde ich zeigen, wie man einen Notizspeicherdienst mit einer ähnlichen Spring Security-Konfiguration von Grund auf implementiert. Hinweis: Es ist gut, wenn der Leser mindestens ein paar Tutorials zu Spring durchgelesen hat, da sich die Aufmerksamkeit nur auf Spring Security konzentriert, ohne detaillierte Erklärungen zu Repositorys, Controllern usw. Andernfalls würde sich herausstellen, dass der Artikel ohnehin schon recht umfangreich ist gigantisch sein. Inhalt
Wir sehen, dass ein neuer Benutzer in der Datenbank aufgetaucht ist. Das Passwort ist verschlüsselt.
Die Notizen werden in der Datenbank gespeichert.
Wir sehen, dass das Projekt erfolgreich gestartet ist und läuft. Für vollkommenes Glück brauchen wir nur die Möglichkeit, uns über soziale Netzwerke anzumelden. Nun, fangen wir an!
Jetzt können sowohl Benutzer als auch OAuth2Authentication als Principal fungieren. Da wir im UserService im Voraus die Belastung des Benutzers durch Google-Daten berücksichtigt haben, funktioniert die Anwendung für beide Benutzertypen. Wir ändern den Hauptseiten-Controller des Projekts so, dass er mit OAuth2 angemeldete Benutzer auf die Notizenseite umleitet.
Der Benutzer meldet sich erfolgreich sowohl über das reguläre Formular als auch über ein Google-Konto an. Das ist es, was wir wollten! Ich hoffe, dieser Artikel hat einige Punkte zum Erstellen einer Webanwendung, deren Sicherung mit Spring Security und der Kombination verschiedener Anmeldemethoden geklärt. Mit dem vollständigen Projektcode können Sie
- Ein Projekt erstellen
- Erstellen von Entitäten und Anwendungslogik
- Konfigurieren von Spring Security für die klassische Anmeldung
- Grundkonfiguration SecurityConfig
- Benutzerdefinierte Benutzeranmeldung
- Lassen Sie uns den Controller verbessern
- Start
- Einrichten von OAuth2 am Beispiel von Google in Spring Security
- Filterkonfiguration und application.properties
- Highlights der Registrierung einer Anwendung bei der Google Cloud Platform
- CustomUserInfoTokenServices
- Endgültiger Start des Projekts
Ein Projekt erstellen
Wir gehen zu start.spring.io und bilden die Grundlage des Projekts:- Web – Starten einer Anwendung auf dem integrierten Tomcat, URL-Zuordnungen und dergleichen;
- JPA – Datenbankverbindung;
- Moustache ist eine Template-Engine zum Generieren von Webseiten.
- Sicherheit – Anwendungsschutz. Dafür wurde dieser Artikel erstellt.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
Die application.properties-Konfiguration sieht derzeit wie folgt aus:
spring.datasource.url=jdbc:mysql://localhost:3306/springsectut?createDatabaseIfNotExist=true&useSSL=false&autoReconnect=true&useLegacyDatetimeCode=false&serverTimezone=UTC&useUnicode=yes&characterEncoding=UTF-8
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.username=yourUsername
spring.datasource.password=yourPassword
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.connection.characterEncoding=utf-8
spring.jpa.properties.connection.CharSet=utf-8
spring.jpa.properties.connection.useUnicode=true
spring.mustache.expose-request-attributes=true
Erstellen von Entitäten und Anwendungslogik
Entitäten
Erstellen wir ein Paket,entities
in dem wir die Datenbankentitäten platzieren. Der Benutzer wird durch eine Klasse beschrieben User
, die die Schnittstelle implementiert UserDetails
, die für die Spring Security-Konfiguration benötigt wird. Der Benutzer verfügt über eine ID, einen Benutzernamen (dies ist eine E-Mail), ein Passwort, einen Namen, eine Rolle, eine Aktivitätsmarkierung, einen Google-Kontonamen und eine E-Mail-Adresse ( googleName
und googleUsername
).
@Entity
@Table(name = "user")
public class User implements UserDetails
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private String password;
private String name;
private boolean active;
private String googleName;
private String googleUsername;
@ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
@CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
@Enumerated(EnumType.STRING)
private Set<Role> roles;
//Геттеры, сеттеры, toString(), equals(), hashcode(), имплементация UserDetails
}
Benutzerrollen werden verwendet, um den Zugriff in Spring Security zu regulieren. Unsere Anwendung verwendet nur eine Rolle:
public enum Role implements GrantedAuthority
{
USER;
@Override
public String getAuthority()
{
return name();
}
}
Erstellen wir eine Notizklasse mit ID, Notiztitel, Notiztext und ID des Benutzers, zu dem sie gehört:
@Entity
@Table(name = "note")
public class Note
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private String note;
private Long userId;
//Геттеры, сеттеры, toString(), equals(), hashcode()
}
Repositories
Um Entitäten in der Datenbank zu speichern, benötigen wir Repositorys, die die ganze Drecksarbeit für uns erledigen. Lassen Sie uns ein Paket erstellenrepos
. Darin erstellen wir Schnittstellen, die von der Schnittstelle UserRepo
geerbt werden . NoteRepo
JpaRepository<Entity, Id>
@Service
@Repository
public interface UserRepo extends JpaRepository<User, Long>
{}
@Service
@Repository
public interface NoteRepo extends JpaRepository<Note, Long>
{
List<Note> findByUserId(Long userId);
}
Controller
Unser Notizendienst wird die folgenden Seiten umfassen:- Heim;
- Anmeldung;
- Eingang;
- Liste der Benutzernotizen.
controllers
mit einer Klasse, IndexController
die das übliche Get-Mapping der Hauptseite enthält. Die Klasse RegistrationController
ist für die Registrierung des Benutzers verantwortlich. Post-Mapping übernimmt Daten aus dem Formular, speichert den Benutzer in der Datenbank und leitet ihn zur Anmeldeseite weiter. PasswordEncoder
wird später beschrieben. Es dient der Verschlüsselung von Passwörtern.
@Controller
public class RegistrationController
{
@Autowired
private UserRepo userRepo;
@Autowired
private PasswordEncoder passwordEncoder;
@GetMapping("/registration")
public String registration()
{
return "registration";
}
@PostMapping("/registration")
public String addUser(String name, String username, String password)
{
User user = new User();
user.setName(name);
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
userRepo.save(user);
return "redirect:/login";
}
Der für die Notizenlistenseite verantwortliche Controller enthält derzeit vereinfachte Funktionen, die nach der Implementierung von Spring Security komplexer werden.
@Controller
public class NoteController
{
@Autowired
private NoteRepo noteRepo;
@GetMapping("/notes")
public String notes(Model model)
{
List<Note> notes = noteRepo.findAll();
model.addAttribute("notes", notes);
return "notes";
}
@PostMapping("/addnote")
public String addNote(String title, String note)
{
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
noteRepo.save(newNote);
return "redirect:/notes";
}
}
Wir werden keinen Controller für die Anmeldeseite schreiben, da dieser von Spring Security verwendet wird. Stattdessen benötigen wir eine spezielle Konfiguration. Erstellen wir wie üblich ein weiteres Paket, nennen es config
und platzieren die Klasse dort MvcConfig
. Wenn wir die Spring Security-Konfiguration schreiben, weiß sie, auf welche Seite wir uns beziehen, wenn wir „/login“ verwenden.
@Configuration
public class MvcConfig implements WebMvcConfigurer
{
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
Seiten
Ich verwende die Mustache-Vorlagen-Engine, um Seiten zu erstellen . Sie können eine andere implementieren, das spielt keine Rolle. Für die Metainformationen, die auf allen Seiten verwendet werden, wurde eine meta.mustache-Datei erstellt. Es enthält auch Bootstrap, um die Seiten unseres Projekts schöner aussehen zu lassen. Seiten werden im Verzeichnis „src/main/resources/templates“ erstellt. Die Dateien haben die Erweiterung mustache. Wenn Sie den HTML-Code direkt im Artikel platzieren, wird dieser zu groß. Daher finden Sie hier einen Link zum Vorlagenordner im GitHub-Repository des Projekts .Konfigurieren von Spring Security für die klassische Anmeldung
Spring Security hilft uns, die Anwendung und ihre Ressourcen vor unbefugtem Zugriff zu schützen. Wir erstellen eine prägnante Arbeitskonfiguration in einerSecurityConfig
von geerbten Klasse WebSecurityConfigurerAdapter
, die wir in das Paket einfügen config
. Markieren wir es mit der Annotation @EnableWebSecurity, die die Spring Security-Unterstützung aktiviert, und der Annotation @Configuration, die angibt, dass diese Klasse einige Konfigurationen enthält. Hinweis: Die automatisch konfigurierte pom.xml enthielt die Version der übergeordneten Spring Boot-Komponente 2.1.4.RELEASE, die verhinderte, dass Security auf die etablierte Weise implementiert wurde. Um Konflikte im Projekt zu vermeiden, wird empfohlen, die Version auf 2.0.1.RELEASE zu ändern.
Grundkonfiguration SecurityConfig
Unsere Konfiguration wird in der Lage sein:-
Passwörter verschlüsseln mit
BCryptPasswordEncoder
:@Autowired private PasswordEncoder passwordEncoder; @Bean PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; }
-
Melden Sie sich mit einem speziell geschriebenen Authentifizierungsanbieter an:
@Autowired private AuthProvider authProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authProvider); }
-
Ermöglichen Sie anonymen Benutzern den Zugriff auf die Startseite sowie die Registrierungs- und Anmeldeseiten. Alle anderen Anfragen müssen von angemeldeten Benutzern durchgeführt werden. Lassen Sie uns das zuvor beschriebene „/login“ als Anmeldeseite zuweisen. Bei erfolgreicher Anmeldung wird der Benutzer auf eine Seite mit einer Liste von Notizen weitergeleitet; bei einem Fehler verbleibt der Benutzer auf der Anmeldeseite. Nach erfolgreichem Beenden wird der Benutzer zur Hauptseite weitergeleitet.
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/resources/**", "/", "/login**", "/registration").permitAll() .anyRequest().authenticated() .and().formLogin().loginPage("/login") .defaultSuccessUrl("/notes").failureUrl("/login?error").permitAll() .and().logout().logoutSuccessUrl("/").permitAll(); }
Benutzerdefinierte Benutzeranmeldung
Ein selbst verfasstes FormularAuthProvider
ermöglicht es dem Benutzer, sich nicht nur per E-Mail, sondern auch mit seinem Benutzernamen anzumelden.
@Component
public class AuthProvider implements AuthenticationProvider
{
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
String username = authentication.getName();
String password = (String) authentication.getCredentials();
User user = (User) userService.loadUserByUsername(username);
if(user != null && (user.getUsername().equals(username) || user.getName().equals(username)))
{
if(!passwordEncoder.matches(password, user.getPassword()))
{
throw new BadCredentialsException("Wrong password");
}
Collection<? extends GrantedAuthority> authorities = user.getAuthorities();
return new UsernamePasswordAuthenticationToken(user, password, authorities);
}
else
throw new BadCredentialsException("Username not found");
}
public boolean supports(Class<?> arg)
{
return true;
}
}
Wie Sie vielleicht bemerkt haben, ist die UserService
im Paket enthaltene Klasse für das Laden des Benutzers verantwortlich services
. In unserem Fall wird ein Benutzer nicht nur anhand des Felds gesucht username
, wie bei der integrierten Implementierung, sondern auch anhand des Benutzernamens, des Google-Kontonamens und der E-Mail-Adresse des Google-Kontos. Die letzten beiden Methoden werden uns bei der Implementierung der Anmeldung über OAuth2 nützlich sein. Hier wird die Vorlesung in gekürzter Form wiedergegeben.
@Service
public class UserService implements UserDetailsService
{
@Autowired
private UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User userFindByUsername = userRepo.findByUsername(username);
//Остальные поиски
if(userFindByUsername != null)
{
return userFindByUsername;
}
//Остальные проверки
return null;
}
}
Hinweis: Vergessen Sie nicht, die erforderlichen Methoden einzutragen UserRepo
!
Lassen Sie uns den Controller verbessern
Wir haben Spring Security konfiguriert. Jetzt ist es an der Zeit, dies in Ihrem Notizen-Controller zu nutzen. Jetzt akzeptiert jede Zuordnung einen zusätzlichen Principal-Parameter, anhand dessen versucht wird, den Benutzer zu finden. Warum kann ich die Klasse nicht direkt einfügenUser
? Dann kommt es zu einem Konflikt aufgrund einer Nichtübereinstimmung der Benutzertypen, wenn wir uns über soziale Netzwerke anmelden. Wir sorgen schon im Vorfeld für die nötige Flexibilität. Unser Notizen-Controller-Code sieht jetzt so aus:
@GetMapping("/notes")
public String notes(Principal principal, Model model)
{
User user = (User) userService.loadUserByUsername(principal.getName());
List<Note> notes = noteRepo.findByUserId(user.getId());
model.addAttribute("notes", notes);
model.addAttribute("user", user);
return "notes";
}
@PostMapping("/addnote")
public String addNote(Principal principal, String title, String note)
{
User user = (User) userService.loadUserByUsername(principal.getName());
Note newNote = new Note();
newNote.setTitle(title);
newNote.setNote(note);
newNote.setUserId(user.getId());
noteRepo.save(newNote);
return "redirect:/notes";
}
Hinweis: Im Projekt ist der CSRF- Schutz standardmäßig aktiviert . Deaktivieren Sie ihn also entweder für sich selbst (http.csrf().disable()) oder vergessen Sie als Autor des Artikels nicht, ein verstecktes Feld mit einem CSRF-Token hinzuzufügen an alle Postanfragen.
Start
Wir versuchen, das Projekt zu starten.Einrichten von OAuth2 am Beispiel von Google in Spring Security
Bei der Implementierung von OAuth2 habe ich mich auf dieses offizielle Tutorial von Spring verlassen . Um OAuth2 zu unterstützen, fügen Sie die folgende Bibliothek zu pom.xml hinzu:<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
Ändern wir unsere Spring Security-Konfiguration im SecurityConfig
. Fügen wir zunächst die Annotation @EnableOAuth2Client hinzu. Es wird automatisch angezeigt, was Sie für die Anmeldung über soziale Netzwerke benötigen.
Filterkonfiguration und application.properties
Fügen wir OAuth2ClientContext ein, um es in unserer Sicherheitskonfiguration zu verwenden.@Autowired
private OAuth2ClientContext oAuth2ClientContext;
OAuth2ClientContext wird beim Erstellen eines Filters verwendet, der die Social-Login-Anfrage des Benutzers validiert. Der Filter ist dank der Annotation @EnableOAuth2Client verfügbar. Alles was wir tun müssen, ist es in der richtigen Reihenfolge vor dem Hauptfilter von Spring Security aufzurufen. Nur dann können wir Weiterleitungen während des Anmeldevorgangs mit OAuth2 abfangen. Dazu verwenden wir FilterRegistrationBean
, in dem wir die Priorität unseres Filters auf -100 setzen.
@Bean
public FilterRegistrationBean oAuth2ClientFilterRegistration(OAuth2ClientContextFilter oAuth2ClientContextFilter)
{
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(oAuth2ClientContextFilter);
registration.setOrder(-100);
return registration;
}
private Filter ssoFilter()
{
OAuth2ClientAuthenticationProcessingFilter googleFilter = new OAuth2ClientAuthenticationProcessingFilter("/login/google");
OAuth2RestTemplate googleTemplate = new OAuth2RestTemplate(google(), oAuth2ClientContext);
googleFilter.setRestTemplate(googleTemplate);
CustomUserInfoTokenServices tokenServices = new CustomUserInfoTokenServices(googleResource().getUserInfoUri(), google().getClientId());
tokenServices.setRestTemplate(googleTemplate);
googleFilter.setTokenServices(tokenServices);
tokenServices.setUserRepo(userRepo);
tokenServices.setPasswordEncoder(passwordEncoder);
return googleFilter;
}
Sie müssen außerdem einen neuen Filter zur Funktion configure(HttpSecurity http) hinzufügen:
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
Der Filter muss außerdem wissen, dass sich der Kunde über Google registriert hat. Die Annotation @ConfigurationProperties gibt an, nach welchen Konfigurationseigenschaften in application.properties gesucht werden soll.
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
return new AuthorizationCodeResourceDetails();
}
Um die Authentifizierung abzuschließen, müssen Sie den Endpunkt für Google-Benutzerinformationen angeben:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
return new ResourceServerProperties();
}
Nachdem wir unsere Anwendung in der Google Cloud Platform registriert haben , fügen wir Eigenschaften mit den entsprechenden Präfixen zu application.properties hinzu:
google.client.clientId=yourClientId
google.client.clientSecret=yourClientSecret
google.client.accessTokenUri=https://www.googleapis.com/oauth2/v4/token
google.client.userAuthorizationUri=https://accounts.google.com/o/oauth2/v2/auth
google.client.clientAuthenticationScheme=form
google.client.scope=openid,email,profile
google.resource.userInfoUri=https://www.googleapis.com/oauth2/v3/userinfo
google.resource.preferTokenInfo=true
Highlights der Registrierung einer Anwendung bei der Google Cloud Platform
Pfad: APIs und Dienste -> Anmeldeinformationen OAuth-Zugriffsanforderungsfenster:- Anwendungsname: Spring-Anmeldeformular und OAuth2-Tutorial
- Support-E-Mail-Adresse: Ihre E-Mail
- Geltungsbereich für die Google-API: E-Mail, Profil, OpenID
- Autorisierte Domains: me.org
- Link zur Hauptseite der Anwendung: http://me.org:8080
- Link zur Datenschutzrichtlinie der App: http://me.org:8080
- Link zu den Nutzungsbedingungen der Anwendung: http://me.org:8080
- Typ: Webanwendung
- Titel: Spring-Anmeldeformular und OAuth2-Tutorial
- Zulässige JavaScript-Quellen: http://me.org, http://me.org:8080
- Zulässige Weiterleitungs-URIs: http://me.org:8080/login, http://me.org:8080/login/google
CustomUserInfoTokenServices
Ist Ihnen das Wort „Benutzerdefiniert“ in der Beschreibung der Filterfunktion aufgefallen? KlasseCustomUserInfoTokenServices
. Ja, wir werden unsere eigene Klasse mit Blackjack und der Möglichkeit erstellen, den Benutzer in der Datenbank zu speichern! Mit der Tastenkombination Strg-N in IntelliJ IDEA können Sie herausfinden und sehen, wie die UserInfoTokenServices
Standardeinstellung implementiert ist. Kopieren wir den Code in die neu erstellte Klasse CustomUserInfoTokenServices
. Das meiste davon kann unverändert bleiben. Bevor wir die Logik der Funktionen ändern, fügen wir UserRepo
und als private Felder der Klasse hinzu PasswordEncoder
. Lassen Sie uns Setter für sie erstellen. Fügen wir @Autowired UserRepo userRepo zur SecurityConfig-Klasse hinzu. Wir schauen zu, wie der Hinweis auf den Fehler in der Filtererstellungsmethode verschwindet, und freuen uns. Warum konnte @Autowired nicht direkt auf CustomUserInfoTokenServices angewendet werden? Weil diese Klasse die Abhängigkeit nicht übernimmt, da sie nicht mit einer Spring-Annotation markiert ist und ihr Konstruktor explizit erstellt wird, wenn der Filter deklariert wird. Dementsprechend weiß der DI-Mechanismus von Spring nichts davon. Wenn wir @Autowired für irgendetwas in dieser Klasse mit Anmerkungen versehen, erhalten wir bei Verwendung eine NullPointerException. Aber durch explizite Setter funktioniert alles sehr gut. Nach der Implementierung der erforderlichen Komponenten wird das Hauptobjekt von Interesse zur Funktion „loadAuthentication“, in der die Map<String, Object> mit Informationen über den Benutzer abgerufen wird. In diesem Projekt habe ich die Speicherung eines über ein soziales Netzwerk angemeldeten Benutzers in der Datenbank implementiert. Da wir als OAuth2-Anbieter ein Google-Konto nutzen, prüfen wir, ob die Karte das für Google typische „sub“-Feld enthält. Wenn es vorhanden ist, bedeutet dies, dass die Informationen über den Benutzer korrekt empfangen wurden. Wir erstellen einen neuen Benutzer und speichern ihn in der Datenbank.
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException
{
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if(map.containsKey("sub"))
{
String googleName = (String) map.get("name");
String googleUsername = (String) map.get("email");
User user = userRepo.findByGoogleUsername(googleUsername);
if(user == null)
{
user = new User();
user.setActive(true);
user.setRoles(Collections.singleton(Role.USER));
}
user.setName(googleName);
user.setUsername(googleUsername);
user.setGoogleName(googleName);
user.setGoogleUsername(googleUsername);
user.setPassword(passwordEncoder.encode("oauth2user"));
userRepo.save(user);
}
if (map.containsKey("error"))
{
this.logger.debug("userinfo returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
Wenn Sie mehrere Anbieter verwenden, können Sie unterschiedliche Optionen in einem CustomUserInfoTokenServices angeben und unterschiedliche Klassen ähnlicher Dienste in der Filterdeklarationsmethode registrieren.
@GetMapping("/")
public String index(Principal principal)
{
if(principal != null)
{
return "redirect:/notes";
}
return "index";
}
GO TO FULL VERSION