1、Spring Security介紹 Spring security,是一個強大的和高度可定製的身份驗證和訪問控制框架。它是確保基於Spring的應用程式的標準 ——來自官方參考手冊 Spring security 和 shiro 一樣,具有認證、授權、加密等用於許可權管理的功能。和 shiro 不 ...
1、Spring Security介紹
Spring security,是一個強大的和高度可定製的身份驗證和訪問控制框架。它是確保基於Spring的應用程式的標準 ——來自官方參考手冊
Spring security 和 shiro 一樣,具有認證、授權、加密等用於許可權管理的功能。和 shiro 不同的是,Spring security擁有比shiro更豐富的功能,並且,對於Springboot而言,Spring Security比Shiro更合適一些,因為都是Spring家族成員。今天,我們來為SpringBoot項目集成Spring Security。
本文所使用的版本:
SpringBoot : 2.2.6.RELEASE
Spring Security : 5.2.2.RELEASE
2、配置Spring Security
在SpringBoot中集成Spring Security很簡單,只需要在pom.xml中添加下麵代碼就行:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
這裡可以不指定Spring Security的版本號,它會根據SpringBoot的版本來匹配對應的版本,SpringBoot版本是 2.2.6.RELEASE,對應Spring Security的版本是5.2.2.RELEASE。
然後,我們就可以將springboot啟動了。
當我們嘗試訪問項目時,它會跳轉到這個界面來:
對!在此之前,你什麼也不用做。這就是Spring Security的優雅之處。你只需要引入Spring Security的包,它就能在你的項目中工作。因為它已經幫你實現了一個簡單的登陸界面。根據官方介紹,登錄使用的賬號是user,密碼是隨機密碼,這個隨機密碼可以在控制臺中找到,類似這樣的一句話:
Using generated security password: 1cb77bc5-8d74-4846-9b6c-4813389ce096
Using generated security password後面的的就是系統給的隨機密碼,我們可以使用這個密碼進行登錄。隨機密碼在每一次啟動服務後生成(如果你配置了熱部署devtools,你得隨時留意控制台了,因為每當你修改了代碼,系統會自動重啟,那時隨機密碼就會重新生成)。
當然,這樣的功能一定不是你想要的,也一定不會就這樣拿給你的用戶使用。那麼,接下來,讓我們把它配置成我們想要的樣子。
要實現自定義配置,首先要創建一個繼承於WebSecurityConfigurerAdapter的配置類:
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}
這裡使用了@EnableWebSecurity註解,這個註解是Spring Security用於啟用web安全的註解。具體實現,這裡就不深入了。
要實現自定義攔截配置,首先得告訴Spring Security,用戶信息從哪裡獲取,以及用戶對應的角色等信息。這裡就需要重寫WebSecurityConfigurerAdapter的configure(AuthenticationManagerBuilder auth)方法了。這個方法將指使Spring Security去找到用戶列表,然後再與想要通過攔截器的用戶進行比對,再進行下麵的步驟。
Spring Security的用戶存儲配置有多個方案可以選擇,包括:
- 記憶體用戶存儲
- 資料庫用戶存儲
- LDAP用戶存儲
- 自定義用戶存儲
我們分別來看看這幾種用戶存儲的配置方法:
1.記憶體用戶存儲
此配置方式是直接將用戶信息存儲在記憶體中,這種方式在速度上無疑是最快的。但只適用於有限個用戶數量,且這些用戶幾乎不會發生改變。我們來看看配置方法:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(passwordEncoder())
.withUser("zhangsan").password(passwordEncoder().encode("123456")).authorities("ADMIN")
.and()
.withUser("lisi").password(passwordEncoder().encode("123456")).authorities("ORDINARY");
}
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
可以看到,AuthenticationManagerBuilder使用構造者方式來構建的。在上面方法中,先調用了inMemoryAuthentication()方法,它來指定用戶存儲在記憶體中。接下來又調用了passwordEncoder()方法,這個方法的作用是告訴Spring Security認證密碼的加密方式。因為在Spring security5過後,必須指定某種加密方式,不然程式會報錯。接下來調用的withUser()、password()、authorities()方法,分別是在指定用戶的賬號、密碼以及許可權名。在添加完一個用戶後,要使用and()方法來連接下一個用戶的添加。
如果使用這種配置方法,你會發現,在修改用戶時,就必須修改代碼。對於絕大多數項目來說,這種方式是滿足不了需求的,至少我們需要一個註冊功能。
2.資料庫用戶存儲
將用戶信息存儲在資料庫中,讓我們可以很方便地對用戶信息進行增刪改查。並且還可以為用戶添加除認證信息外的附加信息,這樣的設計也是我們很多小心應用所採取的方式。讓我們來實現以下:
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder())
.usersByUsernameQuery(
"select username, password, status from Users where username = ?")
.authoritiesByUsernameQuery(
"select username, authority from Authority where username = ?");
}
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
調用jdbcAuthentication()來告訴Spring Security使用jdbc的方式來查詢用戶和許可權,dataSource()方法指定資料庫連接信息,passwordEncoder()指定密碼加密規則,用戶的密碼數據應該以同樣的方式進行加密存儲,不然,兩個加密方式不同的密碼,匹配補上。usersByUsernameQuery()和authoritiesByUsernameQuery()方法分別定義了查詢用戶和許可權信息的sql語句。其實,Spring security為我們預設了查詢用戶、許可權甚至還有群組用戶授權的sql,這三條預設的sql存放在org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl中,有興趣的小伙伴可以點進去看看。如果你要使用預設的,那你的表中關鍵性的欄位必須和語句中的一致。
使用資料庫來存儲用戶和許可權等信息已經可以滿足大部分的需求。但是Spring security還為我們提供了另外一種配置方式,讓我們來看一下。
3.LDAP用戶存儲
LDAP:輕型目錄訪問協議,是一個開放的,中立的,工業標準的應用協議,通過IP協議提供訪問控制和維護分散式信息的目錄信息。簡單來說,就是將用戶信息存放在另外一臺伺服器中(當然,也可以在同一臺伺服器,但我們一般不這麼做),通過網路來進行訪問的技術。
我們來簡單配置一下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
LdapAuthenticationProviderConfigurer<AuthenticationManagerBuilder> configurer = auth.ldapAuthentication()
.userSearchBase("ou=people")
.userSearchFilter("(uid={0})")
.groupSearchBase("ou=groups")
.groupSearchFilter("member={0}");
configurer.passwordCompare()
.passwordEncoder(passwordEncoder())
.passwordAttribute("passcode");
configurer.contextSource().url("ldap://xxxxx.com:33389/dc=xxxxxx,dc=com");
}
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
userSearchFilter()和groupSearchFilter()設置的是用戶和群組的過濾條件,而userSearchBase()和groupSearchBase()設置了搜索起始位置,contextSource().url()設置LDAP伺服器的地址。如果沒有遠程的伺服器可以使用contextSource().root()來使用嵌入式LDAP伺服器,此方式將使用項目中的用戶數據文件來提供認證服務。
如果以上幾種方式還不能滿足我們的需求,我們可以用自定義的方式來配置。
4.自定義用戶存儲
自定義用戶存儲,就是自行使用認證名稱來查找對應的用戶數據,然後交給Spring Security使用。我們需要定義一個實現UserDetailsService的service類:
@Service
public class MyUserDetailsService implements UserDetailsService{
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.getUserByUsername(username);
return user == null ? new User() : user;
}
}
public class User implements UserDetails {
...
}
該類只需要實現一個方法:loadUserByUsername()。該方法需要做的是使用傳過來的username來匹配一個帶有密碼等信息的用戶實體。需要註意的是這裡的User類需要實現UserDetails,也就是說,查到的信息里,必須得有Spring Security所需要的信息。
下麵,讓我們來繼續配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
private PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
這樣的配置方法就很簡單了,只需要告訴Spring Security你的UserDetailsService實現類是哪個就可以了,它會去調用loadUserByUsername()來查找用戶。
以上就是Spring Security所提供的4種用戶存儲方式,接下來,需要考慮的是,怎麼攔截請求。
3、請求攔截
1.安全規則
Spring Security的請求攔截配置方法是用戶存儲配置方法的重載方法,我們先來簡單配置一下:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user", "/menu")
.hasRole("ADMIN")
.antMatchers("/", "/**").permitAll();
}
}
調用authorizeRequests()方法後,就可以添加自定義攔截路徑了。antMatchers()方法配置了請求路徑,hasRole()和permitAll()指定了訪問規則,分別表示擁有“ADMIN”許可權的用戶才能訪問、所有用戶可以訪問。
需要註意的是:這裡的配置需要成對出現,並且配置的順序也很重要。聲明在前面的規則擁有更高的優先順序。也就是說,如果我們將.antMatchers("/", "/").permitAll()**放到了最前面,像這樣:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/", "/**").permitAll()
.antMatchers("/user", "/menu")
.hasRole("ADMIN");
}
那麼,下麵的"/user"和 "/menu"的配置是徒勞,因為前面的規則已經指明所有路徑能被所有人訪問。當然許可權的規則方法還有很多,我這裡只列舉了兩個。以下為常見的內置表達式:
表達 | 描述 |
---|---|
hasRole(String role) | 返回true 當前委托人是否具有指定角色。例如, hasRole('admin') 預設情況下,如果提供的角色不是以“ ROLE_”開頭,則會添加該角色。可以通過修改defaultRolePrefix on來自定義DefaultWebSecurityExpressionHandler 。 |
hasAnyRole(String… roles) |
返回true 當前委托人是否具有提供的任何角色(以逗號分隔的字元串列表形式)。例如, hasAnyRole('admin', 'user') 預設情況下,如果提供的角色不是以“ ROLE_”開頭,則會添加該角色。可以通過修改defaultRolePrefix on來自定義DefaultWebSecurityExpressionHandler 。 |
hasAuthority(String authority) |
返回true 當前委托人是否具有指定許可權。例如, hasAuthority('read') |
hasAnyAuthority(String… authorities) |
返回true 如果當前主體具有任何所提供的當局的(給定為逗號分隔的字元串列表)例如, hasAnyAuthority('read', 'write') |
principal |
允許直接訪問代表當前用戶的主體對象 |
authentication |
允許直接訪問Authentication 從SecurityContext |
permitAll |
始終評估為 true |
denyAll |
始終評估為 false |
isAnonymous() |
返回true 當前委托人是否為匿名用戶 |
isRememberMe() |
返回true 當前主體是否是“記住我”的用戶 |
isAuthenticated() |
true 如果用戶不是匿名的,則返回 |
isFullyAuthenticated() |
返回true 如果用戶不是匿名或記得,我的用戶 |
hasPermission(Object target, Object permission) |
返回true 用戶是否可以訪問給定許可權的給定目標。例如,hasPermission(domainObject, 'read') |
hasPermission(Object targetId, String targetType, Object permission) |
返回true 用戶是否可以訪問給定許可權的給定目標。例如,hasPermission(1, 'com.example.domain.Message', 'read') |
除此之外,還有一個支持SpEL表達式計算的方法,它的使用方法如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user", "/menu")
.access("hasRole('ADMIN')")
.antMatchers("/", "/**").permitAll();
}
它所實現的規則和上面的方法一樣。Spring Security還提供了其他豐富的SpEL表達式,如:
表達 | 描述 |
---|---|
hasRole(String role) |
返回true 當前委托人是否具有指定角色。例如, hasRole('admin') 預設情況下,如果提供的角色不是以“ ROLE_”開頭,則會添加該角色。可以通過修改defaultRolePrefix on來自定義DefaultWebSecurityExpressionHandler 。 |
hasAnyRole(String… roles) |
返回true 當前委托人是否具有提供的任何角色(以逗號分隔的字元串列表形式)。例如, hasAnyRole('admin', 'user') 預設情況下,如果提供的角色不是以“ ROLE_”開頭,則會添加該角色。可以通過修改defaultRolePrefix on來自定義DefaultWebSecurityExpressionHandler 。 |
hasAuthority(String authority) |
返回true 當前委托人是否具有指定許可權。例如, hasAuthority('read') |
hasAnyAuthority(String… authorities) |
返回true 如果當前主體具有任何所提供的當局的(給定為逗號分隔的字元串列表)例如, hasAnyAuthority('read', 'write') |
principal |
允許直接訪問代表當前用戶的主體對象 |
authentication |
允許直接訪問Authentication 從SecurityContext |
permitAll |
始終評估為 true |
denyAll |
始終評估為 false |
isAnonymous() |
返回true 當前委托人是否為匿名用戶 |
isRememberMe() |
返回true 當前主體是否是“記住我”的用戶 |
isAuthenticated() |
true 如果用戶不是匿名的,則返回 |
isFullyAuthenticated() |
返回true 如果用戶不是匿名或記得,我的用戶 |
hasPermission(Object target, Object permission) |
返回true 用戶是否可以訪問給定許可權的給定目標。例如,hasPermission(domainObject, 'read') |
hasPermission(Object targetId, String targetType, Object permission) |
返回true 用戶是否可以訪問給定許可權的給定目標。例如,hasPermission(1, 'com.example.domain.Message', 'read') |
2.登錄
如果此時,我們有自己的登錄界面,需要替換掉Spring Security所提供的預設的界面,這時可以用fromLogin()和loginPage()方法來實現:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user", "/menu")
.access("hasRole('ADMIN')")
.antMatchers("/", "/**").permitAll()
.and()
.formLogin()
.loginPage("/login");
}
這便將登錄地址指向了“/login”。如果需要指定登錄成功時,跳轉的地址,可以使用defaultSuccessUrl()方法:
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/home")
此時用戶登錄過後,將跳轉到主頁來。
下麵,我們來看看登出。
3.登出
和登錄類似的,可以使用logout()和logoutSuccessUrl()方法來實現:
.and()
.logout()
.logoutSuccessUrl("/login")
上面例子中,用戶登出後將跳轉到登錄界面。
4、小結
至此,我們已基本瞭解了Spring Security配置,可以將它配置成我們想要的樣子(基本)。其實Spring Security能做的事還有很多,光看我這篇文章是不夠的。學習它最有效的方法就是閱讀官方文檔。裡面有關於Spring Security最全最新的知識!(官網地址:https://spring.io/projects/spring-security)
公眾號:良許Linux