今日目標,實現自定義登入功能。
在實現自定義功能時,我們會需要有個實例負責儲存使用者的資訊,這個實例就是 UserDetails,並且會有相關 Service 來操作它。
在驗證的流程中,當收到一組帳號密碼,會先去呼叫 UserDetailsService 看是否有這組帳號,如果存在就將相關資訊 (UserDetails) 傳給 AuthenticationProvider,做後續的驗證,當 AuthenticationProvider 驗證通過,就是合法的使用者了。
因此,我們需要分別實現 UserDetails、UserDetailsService、AuthenticationProvider,但這些方法其實都寫好一些介面了,我們只需要實作他們。
package com.example.security;
import com.example.user.UserModel;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
public class CustomUserDetails implements UserDetails {
private UserModel user;
public CustomUserDetails(UserModel user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails
:已經定義好功能的介面,我們只需要實作它即可package com.example.security;
import com.example.user.UserModel;
import com.example.user.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserModel user = userService.findUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("該帳號不存在");
}
return new CustomUserDetails(user);
}
}
UserDetailsService
:也是已經定義好的介面package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.authorizeRequests()
.antMatchers("/register").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}
userDetailsService()
:使用我們自定義的 user details serviceauthenticationProvider()
:自定義驗證提供者,並設定 password encoderauth.authenticationProvider(authenticationProvider())
:設定驗證提供者雖然 spring 幫我們生成了一個 login 的頁面,但我們仍然可以自己定義 login 的頁面,接下來就是 login 頁面的配置,跟 register 差不多,建立 html、controller 控制。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input type="text" id="username" name="username" placeholder="Username">
<input type="password" id="password" name="password" placeholder="Password">
<div th:if="${param.error}">
<div>帳號或密碼錯誤</div>
</div>
<button type="submit" class="btn btn-primary">登入</button>
</form>
</body>
</html>
package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
import java.util.Objects;
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/register")
public String viewRegisterPage(Model model) {
model.addAttribute("name", "註冊");
model.addAttribute("user", new UserModel());
return "register";
}
@PostMapping("/register")
public String registerProcess(@Valid UserModel user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
String message = Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage();
redirectAttributes.addFlashAttribute("error", message);
return "redirect:/register";
}
userService.addUser(user);
return "redirect:/";
}
@GetMapping("/login")
public String viewLoginPage() {
return "login";
}
}
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.authorizeRequests()
.antMatchers("/register", "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error");
}
}
.loginPage("/login")
:設定 login 頁面的路徑.failureUrl("/login?error")
:設定登入失敗的路徑,用於顯示錯誤訊息給使用者看我們之後在程式中會需要先檢驗當前使用者是否為登入狀態,所以先寫好方法,SecurityContextHolder 會負責維護當前使用者的相關資訊,藉由取得 Authentication 來檢驗是否登入,登入的條件就是 Authentication 不為 NULL,而且身分不為匿名身分(Anonymous Authentication)。
package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validator;
import java.util.Set;
@Service
@Validated
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private Validator validator;
@Autowired
private PasswordEncoder passwordEncoder;
public UserModel findUserByEmail(String email) {
return userRepository.findByEmail(email);
}
public UserModel findUserByUsername(String username) {
return userRepository.findByUsername(username);
}
public Integer addUser(UserModel user) {
Set<ConstraintViolation<UserModel>> violations = validator.validate(user);
if (!violations.isEmpty()) {
StringBuilder sb = new StringBuilder();
for (ConstraintViolation<UserModel> constraintViolation : violations) {
sb.append(constraintViolation.getMessage());
}
throw new ConstraintViolationException(sb.toString(), violations);
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
UserModel newUser = userRepository.save(user);
return newUser.getId();
}
public boolean isLogin() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return !(authentication == null || authentication instanceof AnonymousAuthenticationToken);
}
public String getUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}
package com.example.user;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.validation.Valid;
import java.util.Objects;
@Controller
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/register")
public String viewRegisterPage(Model model) {
if (userService.isLogin()) {
return "redirect:/";
}
model.addAttribute("name", "註冊");
model.addAttribute("user", new UserModel());
return "register";
}
@PostMapping("/register")
public String registerProcess(@Valid UserModel user, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
String message = Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage();
redirectAttributes.addFlashAttribute("error", message);
return "redirect:/register";
}
userService.addUser(user);
return "redirect:/";
}
@GetMapping("/login")
public String viewLoginPage() {
if (userService.isLogin()) {
return "redirect:/";
}
return "login";
}
}
package com.example.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http
.authorizeRequests()
.antMatchers("/register", "/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.and()
.logout()
.logoutSuccessUrl("/login").permitAll();
}
}
.logout()
:設定 logout 的動作.logoutSuccessUrl("/login").permitAll()
:登出後跳轉到 login 頁面今天的東西看似很多,但其實大部分 spring security 都幫我們寫好一部分了,所以比較容易一些 (複製貼上就好),只有觀念上需要學習一下。 (最喜歡這種別人都幫忙弄好的功能)