Spring Security 的驗證作業實際是交由``AuthenticationProvider 的實作來執行,如
DaoAuthenticationProvider 進行**使用者名稱**和**密碼**的身分驗證,而在驗證方法中會透過呼叫
UserDetailsService.loadUserByUsername(String username) 查詢使用者資訊
UserDetails` ,然後比對使用者資訊與輸入的密碼是否相同來驗證其是否為合法的使用者。
Spring Security 預設所有的路徑都必須先經過身分驗證才可以存取,因此在新增依賴後要記得設置驗證授權規則,否則一律會收到HTTP 401(Unauthorized) 的狀態碼。
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
設置Spring Security 的授權規則必須繼承WebSecurityConfigurerAdapter
並加上@EnableSecurity
註釋,讓該類別的安全配置生效。
而WebSecurityConfigurerAdapter
有三個重要的configure 可以覆寫,一個與驗證相關的AuthenticationManagerBuilder
,另外兩個是與Web 相關的HttpSecurity
和WebSecurity
。
AuthenticationManagerBuilder
: 用來配置全局的驗證資訊,也就是AuthenticationProvider
和UserDetailsService
。WebSecurity
: 用來配置全局忽略的規則,如靜態資源、是否Debug、全局的HttpFirewall、SpringFilterChain 配置、privilegeEvaluator、expressionHandler、securityInterceptor。HttpSecurity
: 用來配置各種具體的驗證機制規則,如OpenIDLoginConfigurer、AnonymousConfigurer、FormLoginConfigurer、HttpBasicConfigurer 等。package com.example.iThomeIronMan.config;
import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// TODO Auto-generated method stub
auth.userDetailsService(userDetailsService)
.passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.authorizeRequests()
// 設定放行名單
.antMatchers("/login", "/register").permitAll()
// 其餘路徑皆須進行驗證
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").usernameParameter("account").passwordParameter("password")
.and()
.logout().logoutUrl("/logout")
.and()
// 關閉CSRF(跨站請求偽造)攻擊的防護,這樣才不會拒絕外部直接對API 發出的請求,例如Postman 與前端
.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// TODO Auto-generated method stub
web.ignoring().antMatchers("/css/**", "/images/**", "/js/**");
}
}
package com.example.iThomeIronMan.model;
import java.util.Collection;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class MemberAccount extends Base implements UserDetails {
private static final long serialVersionUID = 1L;
private String id;
@Email(message = "帳號必須為電子信箱格式")
@NotBlank(message = "帳號不可為空")
private String account;
@NotBlank(message = "密碼不可為空")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[0-9])[a-zA-Z]{1}[a-zA-Z0-9]{5,15}$",
message = "密碼必須為6 至16 位英文及數字組成且首位字元為英文。")
private String password;
private String salt;
@Override
// 取得所有權限
public Collection<? extends GrantedAuthority> getAuthorities() {
// TODO Auto-generated method stub
return null;
}
@Override
// 取得使用者名稱
public String getUsername() {
// TODO Auto-generated method stub
return account;
}
@Override
// 取得密碼
public String getPassword() {
// TODO Auto-generated method stub
return password;
}
@Override
// 帳號是否過期
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override
// 帳號是否被鎖定
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return true;
}
@Override
// 憑證/密碼是否過期
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return true;
}
@Override
// 帳號是否可用
public boolean isEnabled() {
// TODO Auto-generated method stub
return true;
}
}
package com.example.iThomeIronMan.service.impl;
import java.util.UUID;
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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.iThomeIronMan.dao.MemberAccountDao;
import com.example.iThomeIronMan.model.Member;
import com.example.iThomeIronMan.model.MemberAccount;
import com.example.iThomeIronMan.service.MemberAccountService;
import com.example.iThomeIronMan.service.MemberService;
import com.example.iThomeIronMan.service.ex.AccountDuplicateException;
import com.example.iThomeIronMan.service.ex.InsertException;
@Service
public class MemberAccountServiceImpl implements MemberAccountService, UserDetailsService {
@Autowired
private MemberAccountDao memberAccountDao;
@Autowired
private MemberService memberService;
private BCryptPasswordEncoder passwordEncoder;
public MemberAccountServiceImpl() {
this.passwordEncoder = new BCryptPasswordEncoder();
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// TODO Auto-generated method stub
MemberAccount data = memberAccountDao.getMemberAccountByAccount(username);
if(data == null) throw new UsernameNotFoundException("無此帳號");
return data;
}
@Transactional
public String register(MemberAccount memberAccount, String name) {
// 檢查帳號是否已被註冊
MemberAccount data = memberAccountDao.getMemberAccountByAccount(memberAccount.getAccount());
if(data != null) throw new AccountDuplicateException("該帳號已被註冊");
// 產生鹽值
String salt = UUID.randomUUID().toString().toUpperCase().replaceAll("-", "");
memberAccount.setSalt(salt);
// 密碼加密
String encoderPassword = passwordEncoder.encode(memberAccount.getPassword());
memberAccount.setPassword(encoderPassword);
// 新增帳號
memberAccount.setCreate_by(memberAccount.getAccount());
memberAccount.setUpdate_by(memberAccount.getAccount());
Integer id = memberAccountDao.add(memberAccount);
if(id == 0) throw new InsertException("新增帳號時發生錯誤");
// 新增會員資訊
Member member = new Member();
member.setMa_id(String.valueOf(id));
member.setName(name);
member.setCreate_by(memberAccount.getAccount());
member.setUpdate_by(memberAccount.getAccount());
Integer result = memberService.add(member);
if(result == 0) throw new InsertException("新增帳號時發生錯誤");
return null;
}
public Member login(MemberAccount memberAccount) {
// 檢查帳號是否存在
MemberAccount data = memberAccountDao.getMemberAccountByAccount(memberAccount.getAccount());
if(data == null) {
return null;
}
// 密碼加密
String encoderPassword = passwordEncoder.encode(memberAccount.getPassword());
// 比對密碼
if(!data.getPassword().equals(encoderPassword)) {
return null;
}
// 取得會員資訊
return memberService.getDataByMa_id(data.getId());
}
}
Spring Security(二)WebSecurityConfigurer配置以及filter順序