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


<<:  世界上最快乐的人 (2) 有所缘禅修

>>:  30天学会C语言: Day 24-排序

【Day 05】领域驱动设计的启动

观察的视角 我们要如何描述一个系统呢? 可以从不同的角度观察,好比瞎子摸象,你摸到甚麽部位,系统就像...

Day 19 - [语料库模型] 07-程序码: 余弦相似性

嗨,昨天语料库模型建好了,下一步要如何使用呢? 我们要如何比对输入的句子与语料库中的哪一句最相似呢?...

Day28 深入解析Elasticsearch Query DSL Match query Part1

Hello大家, 台北阴雨绵绵, 早上到公司裤管都湿答答的="= 不舒服... 今天我们来...

[Day 11] 利用webpack安装Vuetify

昨天有讲到怎麽利用vue-router来设定路由了, 在开始切版之前,还需要先导入Vuetify套件...

离职倒数10天:做产品才知道政治敏感不只存在两岸之间

我第一次在微软 release 产品时,学到一件很意外的事是:这世界上政治敏感区域原来不只有台湾跟中...