在編寫我的應用程式時,我面臨著缺乏關於如何讓用戶透過電子郵件和社交網路註冊的明確文章。關於設定經典登入表單有很好的教學。關於OAuth2有很好的教學。關於如何結合這兩種方法的資訊少之又少。在搜尋過程中,我們找到了可行的解決方案。它並不聲稱是終極真理,但它履行了它的功能。在本文中,我將展示如何從頭開始使用類似的 Spring Security 設定實現筆記儲存服務。 注意:如果讀者已經閱讀了至少幾個關於Spring 的教程,那是很好的,因為注意力只會集中在Spring Security 上,而不會詳細解釋存儲庫、控制器等。否則,一篇已經相當大的文章將變成是巨大的。 內容
我們看到資料庫中出現了一個新用戶。密碼已加密。
註釋將保存到資料庫中。
我們看到該專案已成功啟動並運行。為了獲得徹底的幸福,我們只需要能夠透過社群網路登入即可。好吧,讓我們開始吧!
現在 User 和 OAuth2Authentication 都可以充當主體。由於我們預先在 UserService 中考慮到透過 Google 資料載入用戶,因此該應用程式將適用於兩種類型的用戶。我們修改專案的主頁控制器,以便它將使用 OAuth2 登入的使用者重新導向至註解頁面。
使用者透過常規表單和 Google 帳戶成功登入。這就是我們想要的!我希望這篇文章已經澄清了有關創建 Web 應用程式、使用 Spring Security 保護它以及組合不同登入方法的一些要點。有了完整的專案程式碼,您可以
創建專案
我們造訪start.spring.io並形成該專案的基礎:- Web - 在內建 Tomcat 上啟動應用程式、url 映射等;
- JPA——資料庫連接;
- Mustache是一個用於生成網頁的模板引擎;
- 安全-應用程式保護。這就是本文的創建目的。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
application.properties配置目前如下:
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
建立實體和應用程式邏輯
實體
讓我們建立一個包entities
,在其中放置資料庫實體。使用者將由User
實作該介面的類別來描述UserDetails
,Spring Security 配置需要該類別。使用者將擁有 ID、使用者名稱(這是電子郵件)、密碼、姓名、角色、活動標誌、Google 帳戶名稱和電子郵件(googleName
和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
}
使用者角色用於管理 Spring Security 中的存取。我們的應用程式將只使用一個角色:
public enum Role implements GrantedAuthority
{
USER;
@Override
public String getAuthority()
{
return name();
}
}
讓我們建立一個筆記類,其中包含 id、筆記標題、筆記正文和所屬使用者的 id:
@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()
}
儲存庫
為了將實體保存到資料庫中,我們需要儲存庫來為我們完成所有髒活。讓我們建立一個包repos
,在其中我們將建立從介面UserRepo
繼承的介面。 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);
}
控制器
我們的筆記服務將包含以下頁面:- 家;
- 登記;
- 入口;
- 使用者註釋列表。
controllers
其中包含一個類IndexController
,該類包含主頁的常用獲取映射。此類別RegistrationController
負責註冊用戶。後映射從表單中獲取數據,將使用者儲存到資料庫並重定向到登入頁面。PasswordEncoder
稍後將進行描述。它用於加密密碼。
@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";
}
負責筆記清單頁面的控制器目前包含簡化的功能,在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";
}
}
我們不會為登入頁面編寫控制器,因為它是由 Spring Security 使用的。相反,我們需要一個特殊的配置。像往常一樣,讓我們創建另一個包,調用它config
,並將類別放置在那裡MvcConfig
。當我們編寫Spring Security配置時,當我們使用「/login」時,它會知道我們指的是哪個頁面。
@Configuration
public class MvcConfig implements WebMvcConfigurer
{
public void addViewControllers(ViewControllerRegistry registry)
{
registry.addViewController("/login").setViewName("login");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
頁數
我使用Mustache 模板引擎來建立頁面。您可以實現另一個,這並不重要。已為所有頁面上使用的元資訊建立了一個meta.mustache 檔案。它還包括 Bootstrap 以使我們專案的頁面看起來更漂亮。頁面在「src/main/resources/templates」目錄中建立。這些檔案的副檔名是 Mustache。將html程式碼直接放在文章中會使其太大,因此這裡提供了專案的GitHub儲存庫中的templates資料夾的連結。配置 Spring Security 進行經典登錄
Spring Security 協助我們保護應用程式及其資源免於未經授權的存取。SecurityConfig
我們將在繼承自的類別中建立一個簡潔的工作配置WebSecurityConfigurerAdapter
,並將其放置在套件中config
。讓我們用 @EnableWebSecurity 註釋來標記它,這將啟用 Spring Security 支持,以及 @Configuration 註釋,這表明該類別包含一些配置。 注意:自動配置的pom.xml包含Spring Boot父元件2.1.4.RELEASE的版本,這導致安全性無法以既定方式實作。為避免專案衝突,建議將版本變更為2.0.1.RELEASE。
基本設定SecurityConfig
我們的配置將能夠:-
使用以下方法加密密碼
BCryptPasswordEncoder
:@Autowired private PasswordEncoder passwordEncoder; @Bean PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); return passwordEncoder; }
-
使用專門編寫的身份驗證提供者登入:
@Autowired private AuthProvider authProvider; @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(authProvider); }
-
允許匿名使用者存取主頁、註冊和登入頁面。所有其他請求必須由登入使用者執行。讓我們將前面描述的「/login」指定為登入頁面。如果登入成功,使用者將進入包含註釋清單的頁面;如果發生錯誤,使用者將保留在登入頁面。成功退出後,用戶將被帶到主頁。
@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(); }
自訂用戶登入
自寫的登入AuthProvider
將允許使用者不僅可以透過電子郵件登錄,還可以透過使用者名稱登入。
@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;
}
}
UserService
您可能已經注意到,位於套件中的 類別負責載入 user services
。username
在我們的例子中,它不僅像內建實作那樣透過欄位搜尋用戶,而且還透過用戶名、Google 帳戶名稱和 Google 帳戶電子郵件來搜尋用戶。當我們透過 OAuth2 實作登入時,後兩種方法將很有用。這裡給出了該類別的縮寫版本。
@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;
}
}
注意:不要忘記在 中寫入必要的方法UserRepo
!
讓我們改進控制器
我們已經配置了 Spring Security。現在是在筆記控制器中利用這一點的時候了。現在,每個映射都將接受一個附加的主體參數,它將嘗試透過該參數找到使用者。為什麼我不能直接注入類別User
?那麼當我們透過社群網路編寫登入時,就會因為使用者類型不符而產生衝突。我們提前提供必要的靈活性。我們的筆記控制器程式碼現在如下所示:
@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";
}
注意:該項目預設啟用了CSRF保護,因此要么自己禁用它(http.csrf().disable()),要么不要忘記,作為本文的作者,添加帶有 csrf 令牌的隱藏字段所有帖子請求。
發射
我們正在努力啟動該專案。在 Spring Security 中使用 Google 設定 OAuth2 作為範例
在實作 OAuth2 時,我依賴Spring 的官方教學。若要支援 OAuth2,請將下列程式庫新增至 pom.xml:<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
讓我們修改 .xml 檔案中的 Spring Security 設定SecurityConfig
。首先,我們新增 @EnableOAuth2Client 註解。它會自動提取您透過社群網路登入所需的內容。
過濾器配置和application.properties
讓我們注入 OAuth2ClientContext 以在我們的安全配置中使用。@Autowired
private OAuth2ClientContext oAuth2ClientContext;
建立驗證使用者社群登入要求的篩選器時使用 OAuth2ClientContext。由於 @EnableOAuth2Client 註釋,該過濾器可用。我們需要做的就是在主 Spring Security 過濾器之前以正確的順序呼叫它。只有這樣我們才能在使用 OAuth2 的登入過程中擷取重定向。為此,我們使用FilterRegistrationBean
,其中我們將過濾器的優先順序設為 -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;
}
您還需要在configure(HttpSecurity http)函數中新增一個新的篩選器:
http.addFilterBefore(ssoFilter(), UsernamePasswordAuthenticationFilter.class);
過濾器還需要知道客戶端已透過 Google 註冊。@ConfigurationProperties 註解指定要在 application.properties 中尋找哪些設定屬性。
@Bean
@ConfigurationProperties("google.client")
public AuthorizationCodeResourceDetails google()
{
return new AuthorizationCodeResourceDetails();
}
要完成身份驗證,您需要指定Google使用者資訊端點:
@Bean
@ConfigurationProperties("google.resource")
public ResourceServerProperties googleResource()
{
return new ResourceServerProperties();
}
在Google Cloud Platform 中註冊我們的應用程式後,我們將向 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
向Google Cloud Platform註冊應用程式的要點
路徑:API 與服務 -> 憑證 OAuth 存取請求視窗:- 應用程式名稱: Spring登入表單和OAuth2教學
- 支援電子郵件地址:您的電子郵件
- Google API 範圍:電子郵件、個人資料、openid
- 授權網域: me.org
- 應用程式主頁連結: http://me.org:8080
- 應用程式隱私權政策連結: http://me.org:8080
- 應用程式使用條款連結: http://me.org:8080
- 類型:網頁應用程式
- 標題: Spring 登入表單和 OAuth2 教學課程
- 允許的 JavaScript 來源: http://me.org、http://me.org:8080
- 允許的重新導向 URI: http://me.org:8080 /login、http://me.org:8080/login/google
自訂使用者資訊令牌服務
您注意到過濾器功能說明中的“自訂”一詞了嗎?班級CustomUserInfoTokenServices
。是的,我們將創建我們自己的類,具有二十一點和將用戶保存在資料庫中的能力!使用 IntelliJ IDEA 中的 Ctrl-N 鍵盤快速鍵,您可以找到並查看UserInfoTokenServices
預設值是如何實現的。讓我們將其程式碼複製到新建立的類別中CustomUserInfoTokenServices
。其中大部分可以保持不變。在更改函數的邏輯之前,讓我們新增UserRepo
和作為類別的私有欄位PasswordEncoder
。讓我們為它們創建 setter。讓我們將@Autowired UserRepo userRepo 加入SecurityConfig 類別。我們看看過濾器創建方法中指向錯誤的指標是如何消失的,我們很高興。為什麼@Autowired 不能直接套用在CustomUserInfoTokenServices?因為這個類別不會取得依賴關係,因為它本身沒有任何 Spring 註解標記,而且它的建構子是在宣告過濾器時明確建立的。因此,Spring 的 DI 機制並不知道這一點。如果我們在此類中的任何內容上註釋@Autowired,那麼在使用時我們將得到一個NullPointerException。但透過顯式設定器,一切都運作良好。實作必要的元件後,感興趣的主要物件成為 loadAuthentication 函數,其中檢索包含使用者資訊的 Map<String, Object>。正是在這個專案中,我實現了將透過社群網路登入的使用者儲存到資料庫中。由於我們使用 Google 帳戶作為 OAuth2 供應商,因此我們檢查地圖是否包含 Google 典型的「子」欄位。如果存在,則表示已正確接收有關使用者的資訊。我們建立一個新用戶並將其保存到資料庫中。
@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);
}
當使用多個提供者時,您可以在一個 CustomUserInfoTokenServices 中指定不同的選項,並在篩選器聲明方法中註冊類似服務的不同類別。
@GetMapping("/")
public String index(Principal principal)
{
if(principal != null)
{
return "redirect:/notes";
}
return "index";
}
GO TO FULL VERSION