iT邦幫忙

2021 iThome 鐵人賽

DAY 25
1
Modern Web

誤打誤撞學了Spring Boot 還當了後端工程師系列 第 25

Day 25 - Spring Security (二) UserDetailsService

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>

新增SecurityConfig

設置Spring Security 的授權規則必須繼承WebSecurityConfigurerAdapter 並加上@EnableSecurity 註釋,讓該類別的安全配置生效。
WebSecurityConfigurerAdapter 有三個重要的configure 可以覆寫,一個與驗證相關的AuthenticationManagerBuilder,另外兩個是與Web 相關的HttpSecurityWebSecurity

  1. AuthenticationManagerBuilder : 用來配置全局的驗證資訊,也就是AuthenticationProviderUserDetailsService
  2. WebSecurity : 用來配置全局忽略的規則,如靜態資源、是否Debug、全局的HttpFirewall、SpringFilterChain 配置、privilegeEvaluator、expressionHandler、securityInterceptor。
  3. 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/**");
	}

}

調整MemberAccount 實體類

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;
	}
	
}

調整MemberAccountService

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順序


上一篇
Day 24 - Spring Security (一) 基本概念與流程
下一篇
Day 26 - Spring Security (三) DaoAuthenticationProvider
系列文
誤打誤撞學了Spring Boot 還當了後端工程師30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言