Semasa menulis permohonan saya, saya berhadapan dengan kekurangan artikel yang jelas tentang cara membuat pengguna mendaftar melalui e-mel dan rangkaian sosial. Terdapat tutorial yang baik tentang menyediakan borang log masuk klasik. Terdapat tutorial yang bagus tentang OAuth2 . Terdapat sedikit maklumat jenayah tentang cara menggabungkan kedua-dua kaedah. Semasa proses carian, kami dapat menghasilkan penyelesaian yang boleh dilaksanakan. Ia tidak mendakwa sebagai kebenaran muktamad, tetapi ia memenuhi fungsinya. Dalam artikel ini saya akan menunjukkan cara untuk melaksanakan perkhidmatan storan nota dengan konfigurasi Spring Security yang serupa dari awal. Nota: adalah baik jika pembaca telah melalui sekurang-kurangnya beberapa tutorial tentang Spring, kerana perhatian akan tertumpu hanya pada Spring Security, tanpa penjelasan terperinci tentang repositori, pengawal, dll. Jika tidak, artikel yang sudah agak besar akan bertukar menjadi menjadi raksasa. Kandungan
Kami melihat bahawa pengguna baharu telah muncul dalam pangkalan data. Kata laluan disulitkan.
Nota disimpan ke pangkalan data.
Kami melihat bahawa projek itu berjaya dilancarkan dan berjalan. Untuk kebahagiaan sepenuhnya, kami hanya memerlukan keupayaan untuk log masuk melalui rangkaian sosial. Baiklah, mari kita mulakan!
Kini kedua-dua Pengguna dan OAuth2Authentication boleh bertindak sebagai Prinsipal. Memandangkan kami mengambil kira dalam UserService terlebih dahulu memuatkan pengguna melalui data Google, aplikasi akan berfungsi untuk kedua-dua jenis pengguna. Kami mengubah suai pengawal halaman utama projek supaya ia mengubah hala pengguna yang log masuk menggunakan OAuth2 ke halaman nota.
Pengguna berjaya log masuk melalui borang biasa dan melalui akaun Google. Inilah yang kami mahukan! Saya harap artikel ini telah menjelaskan beberapa perkara tentang membuat aplikasi web, mengamankannya dengan Spring Security, dan menggabungkan kaedah log masuk yang berbeza. Dengan kod projek penuh anda boleh
- Mencipta Projek
- Mencipta Entiti dan Logik Aplikasi
- Mengkonfigurasi Keselamatan Musim Bunga untuk Log Masuk Klasik
- Menyediakan OAuth2 menggunakan Google sebagai contoh dalam Spring Security
- Konfigurasi penapis dan aplikasi.properties
- Sorotan pendaftaran aplikasi dengan Google Cloud Platform
- CustomUserInfoTokenServices
- Pelancaran akhir projek
Mencipta Projek
Kami pergi ke start.spring.io dan membentuk asas projek:- Web - melancarkan aplikasi pada Tomcat terbina dalam, pemetaan url dan seumpamanya;
- JPA - sambungan pangkalan data;
- Misai ialah enjin templat yang digunakan untuk menjana halaman web;
- Keselamatan - perlindungan aplikasi. Untuk tujuan inilah artikel ini dicipta.
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
Konfigurasi application.properties pada masa ini adalah seperti berikut:
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
Mencipta Entiti dan Logik Aplikasi
Entiti
Mari kita buat pakejentities
di mana kita akan meletakkan entiti pangkalan data. Pengguna akan diterangkan oleh kelas User
yang melaksanakan antara muka UserDetails
, yang akan diperlukan untuk konfigurasi Spring Security. Pengguna akan mempunyai id, nama pengguna (ini ialah e-mel), kata laluan, nama, peranan, bendera aktiviti, nama akaun Google dan e-mel ( googleName
dan 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
}
Peranan pengguna digunakan untuk mengawal selia akses dalam Spring Security. Aplikasi kami hanya akan menggunakan satu peranan:
public enum Role implements GrantedAuthority
{
USER;
@Override
public String getAuthority()
{
return name();
}
}
Mari buat kelas nota dengan id, tajuk nota, isi nota dan id pengguna yang menjadi miliknya:
@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()
}
Repositori
Untuk menyimpan entiti ke pangkalan data, kami memerlukan repositori yang akan melakukan semua kerja kotor untuk kami. Mari kita buat pakejrepos
, di dalamnya kita akan mencipta antara muka UserRepo
yang diwarisi NoteRepo
daripada antara muka 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);
}
Pengawal
Perkhidmatan nota kami akan mempunyai halaman berikut:- Rumah;
- Pendaftaran;
- Pintu masuk;
- Senarai nota pengguna.
controllers
yang mengandungi kelas IndexController
yang mengandungi pemetaan dapatkan biasa halaman utama. Kelas RegistrationController
bertanggungjawab untuk mendaftarkan pengguna. Pemetaan selepas mengambil data daripada borang, menyimpan pengguna ke pangkalan data dan mengubah hala ke halaman log masuk. PasswordEncoder
akan diterangkan kemudian. Ia digunakan untuk menyulitkan kata laluan.
@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";
}
Pengawal yang bertanggungjawab untuk halaman senarai nota pada masa ini mengandungi fungsi yang dipermudahkan, yang akan menjadi lebih kompleks selepas pelaksanaan Spring Security.
@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";
}
}
Kami tidak akan menulis pengawal untuk halaman log masuk kerana ia digunakan oleh Spring Security. Sebaliknya, kami memerlukan konfigurasi khas. Seperti biasa, mari buat pakej lain, panggilnya config
dan letakkan kelas di sana MvcConfig
. Apabila kami menulis konfigurasi Spring Security, ia akan mengetahui halaman mana yang kami rujuk apabila kami menggunakan "/log masuk".
@Configuration
public class MvcConfig implements WebMvcConfigurer
{
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
muka surat
Saya menggunakan enjin templat Misai untuk mencipta halaman . Anda boleh melaksanakan satu lagi, tidak mengapa. Fail meta.misai telah dibuat untuk maklumat meta yang digunakan pada semua halaman. Ia juga termasuk Bootstrap untuk menjadikan halaman projek kami kelihatan lebih cantik. Halaman dibuat dalam direktori "src/main/resources/templates". Fail mempunyai misai sambungan. Meletakkan kod html terus dalam artikel akan menjadikannya terlalu besar, jadi berikut ialah pautan ke folder templat dalam repositori GitHub projek .Mengkonfigurasi Keselamatan Musim Bunga untuk Log Masuk Klasik
Spring Security membantu kami melindungi aplikasi dan sumbernya daripada akses tanpa kebenaran. Kami akan mencipta konfigurasi kerja ringkas dalam kelasSecurityConfig
yang diwarisi daripada WebSecurityConfigurerAdapter
, yang akan kami letakkan dalam pakej config
. Mari tandakannya dengan anotasi @EnableWebSecurity, yang akan mendayakan sokongan Spring Security dan anotasi @Configuration, yang menunjukkan bahawa kelas ini mengandungi beberapa konfigurasi. Nota: pom.xml yang dikonfigurasikan secara automatik mengandungi versi komponen induk Spring Boot 2.1.4.RELEASE, yang menghalang Keselamatan daripada dilaksanakan dengan cara yang ditetapkan. Untuk mengelakkan konflik dalam projek, adalah disyorkan untuk menukar versi kepada 2.0.1.RELEASE.
Konfigurasi asas SecurityConfig
Konfigurasi kami akan dapat:-
Sulitkan kata laluan menggunakan
BCryptPasswordEncoder
:@Autowired private PasswordEncoder passwordEncoder; @Bean PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; }
-
Log masuk menggunakan pembekal pengesahan bertulis khas:
@Autowired private AuthProvider authProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authProvider); }
-
Benarkan pengguna tanpa nama mengakses halaman utama, halaman pendaftaran dan log masuk. Semua permintaan lain mesti dilakukan oleh pengguna yang log masuk. Mari kita tetapkan "/log masuk" yang diterangkan sebelum ini sebagai halaman log masuk. Jika log masuk berjaya, pengguna akan dibawa ke halaman dengan senarai nota; jika terdapat ralat, pengguna akan kekal di halaman log masuk. Apabila berjaya keluar, pengguna akan dibawa ke halaman utama.
@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(); }
Log masuk pengguna tersuai
Yang ditulis sendiriAuthProvider
akan membolehkan pengguna log masuk bukan sahaja melalui e-mel, tetapi juga dengan nama pengguna.
@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;
}
}
Seperti yang anda mungkin perasan, kelas UserService
yang terletak dalam pakej bertanggungjawab untuk memuatkan pengguna services
. Dalam kes kami, ia mencari pengguna bukan sahaja mengikut medan username
, seperti pelaksanaan terbina dalam, tetapi juga dengan nama pengguna, nama akaun Google dan e-mel akaun Google. Dua kaedah terakhir akan berguna kepada kami apabila melaksanakan log masuk melalui OAuth2. Di sini kelas diberikan dalam versi singkatan.
@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;
}
}
Nota: jangan lupa tulis kaedah yang diperlukan dalam UserRepo
!
Mari kita perbaiki pengawal
Kami telah mengkonfigurasi Spring Security. Sekarang adalah masa untuk memanfaatkan ini dalam pengawal nota anda. Kini setiap pemetaan akan menerima parameter Prinsipal tambahan, yang mana ia akan cuba mencari pengguna. Mengapa saya tidak boleh menyuntik kelas secara langsungUser
? Kemudian akan berlaku konflik kerana ketidakpadanan jenis pengguna apabila kami menulis log masuk melalui rangkaian sosial. Kami menyediakan fleksibiliti yang diperlukan terlebih dahulu. Kod pengawal nota kami kini kelihatan seperti ini:
@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";
}
Nota: projek ini mempunyai perlindungan CSRF yang didayakan secara lalai , jadi sama ada nyahdayakannya untuk diri sendiri (http.csrf().disable()), atau jangan lupa, sebagai pengarang artikel, untuk menambah medan tersembunyi dengan token csrf kepada semua permintaan jawatan.
Pelancaran
Kami cuba melancarkan projek itu.Menyediakan OAuth2 menggunakan Google sebagai contoh dalam Spring Security
Apabila melaksanakan OAuth2, saya bergantung pada tutorial rasmi ini daripada Spring . Untuk menyokong OAuth2, tambahkan pustaka berikut pada pom.xml:<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
Mari ubah suai konfigurasi Spring Security kami dalam SecurityConfig
. Mula-mula, mari tambah anotasi @EnableOAuth2Client. Ia secara automatik akan menarik apa yang anda perlukan untuk log masuk melalui rangkaian sosial.
Konfigurasi penapis dan aplikasi.properties
Mari kita menyuntik OAuth2ClientContext untuk digunakan dalam konfigurasi keselamatan kami.@Autowired
private OAuth2ClientContext oAuth2ClientContext;
OAuth2ClientContext digunakan semasa membuat penapis yang mengesahkan permintaan log masuk sosial pengguna. Penapis tersedia terima kasih kepada anotasi @EnableOAuth2Client. Apa yang perlu kita lakukan ialah memanggilnya dalam susunan yang betul, sebelum penapis Keselamatan Musim Bunga utama. Hanya selepas itu kami akan dapat menangkap ubah hala semasa proses log masuk dengan OAuth2. Untuk melakukan ini, kami menggunakan FilterRegistrationBean
, di mana kami menetapkan keutamaan penapis kami kepada -100.
@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;
}
Anda juga perlu menambah penapis baharu pada fungsi konfigurasi(HttpSecurity http):
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
Penapis juga perlu mengetahui bahawa pelanggan telah mendaftar melalui Google. Anotasi @ConfigurationProperties menentukan sifat konfigurasi yang hendak dicari dalam application.properties.
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
return new AuthorizationCodeResourceDetails();
}
Untuk melengkapkan pengesahan, anda perlu menentukan titik akhir maklumat pengguna Google:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
return new ResourceServerProperties();
}
Setelah mendaftarkan aplikasi kami dalam Google Cloud Platform , kami akan menambah sifat dengan awalan yang sesuai pada application.properties:
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
Sorotan pendaftaran aplikasi dengan Google Cloud Platform
Laluan: API dan Perkhidmatan -> Tetingkap Permintaan Akses OAuth Bukti kelayakan:- Nama aplikasi: Borang log masuk musim bunga dan tutorial OAuth2
- Alamat e-mel sokongan: e-mel anda
- Skop untuk API Google: e-mel, profil, openid
- Domain dibenarkan: me.org
- Pautan ke halaman utama permohonan: http://me.org:8080
- Pautan ke dasar privasi apl: http://me.org:8080
- Pautan ke syarat penggunaan aplikasi: http://me.org:8080
- Jenis: Aplikasi Web
- Tajuk: Borang log masuk musim bunga dan tutorial OAuth2
- Sumber JavaScript yang dibenarkan: http://me.org, http://me.org:8080
- URI ubah hala yang dibenarkan: http://me.org:8080/login, http://me.org:8080/login/google
CustomUserInfoTokenServices
Adakah anda perasan perkataan Tersuai dalam perihalan fungsi penapis? KelasCustomUserInfoTokenServices
. Ya, kami akan mencipta kelas kami sendiri dengan blackjack dan keupayaan untuk menyelamatkan pengguna dalam pangkalan data! Menggunakan pintasan papan kekunci Ctrl-N dalam IntelliJ IDEA, anda boleh mencari dan melihat cara UserInfoTokenServices
lalai dilaksanakan. Mari salin kodnya ke dalam kelas yang baru dibuat CustomUserInfoTokenServices
. Kebanyakannya boleh dibiarkan tidak berubah. Sebelum menukar logik fungsi, mari tambah UserRepo
dan sebagai medan peribadi kelas PasswordEncoder
. Mari kita buat setter untuk mereka. Mari tambahkan @Autowired UserRepo userRepo ke kelas SecurityConfig. Kami melihat bagaimana penunjuk kepada ralat dalam kaedah penciptaan penapis hilang, dan kami bergembira. Mengapa @Autowired tidak boleh digunakan terus ke CustomUserInfoTokenServices? Kerana kelas ini tidak akan mengambil kebergantungan, kerana ia sendiri tidak ditandakan dengan sebarang anotasi Spring, dan pembinanya dibuat secara eksplisit apabila penapis diisytiharkan. Sehubungan itu, mekanisme DI Spring tidak mengetahui mengenainya. Jika kami memberi anotasi @Autowired pada apa-apa sahaja dalam kelas ini, kami akan mendapat NullPointerException apabila digunakan. Tetapi melalui setter eksplisit semuanya berfungsi dengan baik. Selepas melaksanakan komponen yang diperlukan, objek utama yang diminati menjadi fungsi loadAuthentication, di mana Map<String, Object> dengan maklumat tentang pengguna diambil semula. Dalam projek ini saya melaksanakan penjimatan pengguna yang log masuk melalui rangkaian sosial ke dalam pangkalan data. Memandangkan kami menggunakan akaun Google sebagai penyedia OAuth2, kami menyemak sama ada peta mengandungi medan "sub" yang biasa untuk Google. Jika ia hadir, ini bermakna maklumat tentang pengguna telah diterima dengan betul. Kami mencipta pengguna baharu dan menyimpannya ke pangkalan data.
@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);
}
Apabila menggunakan beberapa pembekal, anda boleh menentukan pilihan berbeza dalam satu CustomUserInfoTokenServices dan mendaftarkan kelas perkhidmatan serupa yang berbeza dalam kaedah pengisytiharan penapis.
@GetMapping("/")
public String index(Principal principal)
{
if(principal != null)
{
return "redirect:/notes";
}
return "index";
}
GO TO FULL VERSION