概述
Web系統(tǒng)中的認(rèn)證和權(quán)限服務(wù)是現(xiàn)在互聯(lián)網(wǎng)服務(wù)中不可或缺的一部分存炮,目前主流的安全框架無(wú)非就是Apache Shiro和Spring Security。Apache Shiro簡(jiǎn)單易用是一大優(yōu)勢(shì),而Spring Security則功能更為強(qiáng)大且能夠配合Spring使用久窟。
Spring Security的hello world
先聲明本文意在闡明Spring Security的基本原理尉尾,所以代碼沒(méi)有持久層部分(數(shù)據(jù)庫(kù)操作)。
Web項(xiàng)目搭建
先利用SpringBoot搭建一個(gè)簡(jiǎn)單的Web服務(wù)并且加入Spring Security依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
寫一個(gè)簡(jiǎn)單的controller
@RestController
public class HelloController {
@RequestMapping("hello")
public String hello(){
return "hello spring security";
}
}
咋們嘗試訪問(wèn)這個(gè)接口localhost:8080/hello萌庆,不能直接訪問(wèn)了溶褪,而需要表單登錄
在加入Spring Security后就有一個(gè)默認(rèn)的安全配置,所有服務(wù)器資源的訪問(wèn)都需要經(jīng)過(guò)表單登錄認(rèn)證践险。并且會(huì)生成一個(gè)用戶名user的憑證猿妈,密碼是程序啟動(dòng)時(shí)控制臺(tái)輸出的一串字符
加入自定義安全配置
自定義安全配置類,繼承WebSecurityConfigurerAdapter
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
http.formLogin().and().authorizeRequests().anyRequest().authenticated()巍虫,指定了默認(rèn)的驗(yàn)證方式是表單登錄并且任何請(qǐng)求都需要經(jīng)過(guò)認(rèn)證彭则。Spring Security早期的版本默認(rèn)指定的認(rèn)證方式是HttpBasic的認(rèn)證方式,現(xiàn)在的默認(rèn)就是表單登錄驗(yàn)證占遥。所以如果使用了最新版本的Spring Security俯抖,根據(jù)上面的配置和之前不加入配置并沒(méi)有變化
Spring Security工作原理
Spring Security的核心就是過(guò)濾器鏈,在訪問(wèn)服務(wù)器資源時(shí)請(qǐng)求需要經(jīng)過(guò)過(guò)濾器鏈才能獲取資源
圖中綠色的過(guò)濾器是負(fù)責(zé)認(rèn)證的過(guò)濾器瓦胎,比如HttpBasic和表單認(rèn)證蚌成;橙色過(guò)濾器過(guò)濾器鏈的最后一關(guān),它判斷請(qǐng)求用戶是否認(rèn)證和擁有權(quán)限凛捏,沒(méi)通過(guò)會(huì)拋出異常担忧;藍(lán)色是異常處理過(guò)濾器,它對(duì)橙色過(guò)濾器拋出的異常進(jìn)行處理坯癣。其中綠色的過(guò)濾器可以根據(jù)配置決定是否生效和順序瓶盛,藍(lán)色和橙色是固定順序和一定生效的
Spring Security自定義認(rèn)證邏輯
處理用戶信息獲取邏輯和密碼加密解密處理
上面提到Spring Security默認(rèn)會(huì)生成一個(gè)用戶名user的憑證并且提供密碼,但是我們的實(shí)際需求肯定不是這樣的示罗,所以我們自定義處理用戶信息獲取邏輯惩猫。我們通過(guò)自定義用戶信息獲取類,它實(shí)現(xiàn)UserDetailsService接口
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
/**
* 這里實(shí)際情況應(yīng)該是根據(jù)參數(shù)s查詢數(shù)據(jù)庫(kù)用戶數(shù)據(jù)
*/
return new User("admin",bCryptPasswordEncoder.encode("123"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
上面并沒(méi)有對(duì)數(shù)據(jù)庫(kù)的真實(shí)查詢操作蚜点,只是模擬而已轧房。loadUserByUsername方法返回一個(gè)Spring Security提供的User類(這個(gè)類可以是一個(gè)自定義的User類,后面會(huì)說(shuō))绍绘。然后Spring Security配置類中重寫另一個(gè)configure方法對(duì)userDetailsService進(jìn)行配置奶镶,配置成我們自定義的用戶信息獲取類(auth.userDetailsService(myUserDetailService)指定了自定義處理用戶信息獲取的類)迟赃,并且初始化一個(gè)BCryptPasswordEncoder的編碼器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private MyUserDetailService myUserDetailService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.formLogin()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService);
super.configure(auth);
}
}
重新啟動(dòng)服務(wù)嘗試訪問(wèn)http://localhost:8080/hello,輸入用戶名admin厂镇,密碼123
處理用戶檢驗(yàn)邏輯
在實(shí)際需求中纤壁,我們可能需要判斷用戶過(guò)期、用戶鎖定捺信、憑證過(guò)期酌媒、用戶可用等問(wèn)題。Spring Security提供的User類可能并不能解決我們的需求迄靠,自定義一個(gè)User實(shí)現(xiàn)UserDetails
public class User implements UserDetails{
private Integer id;
private String username;
private String password;
private Integer age;
private Boolean isAccountNonExpired;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean getAccountNonExpired() {
return isAccountNonExpired;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
isAccountNonExpired = accountNonExpired;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return this.username;
}
@Override
public String getUsername() {
return this.password;
}
@Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
其中包括一個(gè)用戶的一些基本字段秒咨,通過(guò)重寫isAccountNonExpired、isAccountNonLocked掌挚、isCredentialsNonExpired雨席、isEnabled自定義用戶校驗(yàn)邏輯。比如重寫isAccountNonExpired根據(jù)isAccountNonExpired字段的布爾值判斷是否用戶過(guò)期疫诽。修改MyUserDetailService的loadUserByUsername
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
/**
* 這里實(shí)際情況應(yīng)該是根據(jù)參數(shù)s查詢數(shù)據(jù)庫(kù)用戶數(shù)據(jù)
*/
User user = new User();
user.setId(1);
user.setUsername("admin");
user.setPassword("123");
user.setAge(21);
user.setAccountNonExpired(false);
return user;
//return new User("admin",bCryptPasswordEncoder.encode("123"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
重新啟動(dòng)服務(wù),嘗試訪問(wèn)http://localhost:8080/hello旦委,輸入admin奇徒、123,校驗(yàn)結(jié)果是用戶被鎖定
總結(jié)
- Spring Security的Hello World
- Spring Security的工作原理
- Spring Security自定義認(rèn)證邏輯
Github倉(cāng)庫(kù)地址:https://github.com/iemi/spring-security-hello-world