1.認識Spring Security Spring Security提供了聲明式的安全訪問控制解決方案(僅支持基於Spring的應用程式),對訪問許可權進行認證和授權,它基於Spring AOP和Servlet過濾器,提供了安全性方面的全面解決方案。 除常規的認證和授權外,它還提供了 ACLs、LD ...
1.認識Spring Security
Spring Security提供了聲明式的安全訪問控制解決方案(僅支持基於Spring的應用程式),對訪問許可權進行認證和授權,它基於Spring AOP和Servlet過濾器,提供了安全性方面的全面解決方案。
除常規的認證和授權外,它還提供了 ACLs、LDAP、JAAS、CAS等高級特性以滿足複雜環境下的安全需求。
1.1 核心概念
Spring Security的3個核心概念。
- Principle:代表用戶的對象Principle ( User),不僅指人類,還包括一切可以用於驗證的設備。
- Authority: 代表用戶的角色Authority ( Role ),每個用戶都應該有一種角色,如管理員或是會員。
- Permission:代表授權,複雜的應用環境需要對角色的許可權進行表述。
在Spring Security中,Authority和Permission是兩個完全獨立的概念,兩者並沒有必然的聯繫。它們之間需要通過配置進行關聯,可以是自己定義的各種關係。
1.2 認證和授權
安全主要分為驗證(authentication)和授權(authorization )兩個部分。
(1)驗證(authentication)
驗證指的是,建立系統使用者信息(Principal)的過程。使用者可以是一個用戶、設備,和可以在應用程式中執行某種操作的其他系統。用戶認證一般要求用戶提供用戶名和密碼,系統通過校驗用戶名和密碼的正確性來完成認證的通過或拒絕過程。Spring Security支持主流的認證方式,包括HTTP基本認證、HTTP表單驗證、HTTP摘要認證、OpenlD和LDAP等。
Spring Security進行驗證的步驟如下:
- 用戶使用用戶名和密碼登錄。
- 過濾器(UsernamePasswordAuthenticationFilter)獲取到用戶名、密碼,然後封裝成 Authentication。
- AuthenticationManager 認證 token ( Authentication 的實現類傳遞)。
- AuthenticationManager認證成功,返回一個封裝了用戶許可權信息的Authentication對象, 用戶的上下文信息(角色列表等)。
- Authentication對象賦值給當前的SecurityContext,建立這個用戶的安全上下文(通過調用 getContext().setAuthentication())。
- 用戶進行一些受到訪問控制機制保護的操作,訪問控制機制會依據當前安全上下文信息檢查這個操作所需的許可權。
除利用提供的認證外,還可以編寫自己的Filter(過濾器),提供與那些不是基於Spring Security 的驗證系統的操作。
(2)授權(authorization)。
在一個系統中,不同用戶具有的許可權是不同的。一般來說,系統會為不同的用戶分配不同的角色,而每個角色則對應一系列的許可權。它判斷某個Principal在應用程式中是否允許執行某個操作。在進行授權判斷之前,要求其所要使用到的規則必須在驗證過程中已經建立好了。對Web資源的保護,最好的辦法是使用過濾器。對方法調用的保護,最好的辦法是使用AOP。Spring Security在進行用戶認證及授予許可權時,也是通過各種攔截器和AOP來控制許可權訪問的,從而實現安全。
1.3 模塊
- 核心模塊——spring-security-core.jar:包含核心驗證和訪問控制類和介面,以及支持遠程配置的基本API。
- 遠程調用——spring-security-remoting.jar:提供與 Spring Remoting 集成。
- 網頁——spring-security-web.jar:包括網站安全的模塊,提供網站認證服務和基於URL 訪問控制。
- 配置——spring-security-config.jar:包含安全命令空間解析代碼。
- LDAP——spring-security-ldap.jar: LDAP 驗證和配置。
- ACL——spring-security-acl.jar:對 ACL 訪問控製表的實現。
- CAS——spring-security-cas.jar:對 CAS 客戶端的安全實現。
- OpenlD——spring-security-openid.jar:對 OpenlD 網頁驗證的支持。
- Test——spring-security-test.jar:對 Spring Security 的測試的支持。
2. 核心類
2.1 SecurityContext
Securitycontext中包含當前正在訪問系統的用戶的詳細信息,它只有以下兩種方法。
- getAuthentication():獲取當前經過身份驗證的主體或身份驗證的請求令牌。
- setAuthentication():更改或刪除當前已驗證的主體身份驗證信息。
SecurityContext 的信息是由 SecurityContextHolder 來處理的。
2.2 SecurityContextHolder
SecurityContextHolder 用來保存 SecurityContext。最常用的是 getContext()方法,用來獲得當前 SecurityContext。
SecurityContextHolder中定義了一系列的靜態方法,而這些靜態方法的內部邏輯是通過 SecurityContextHolder 持有的 SecurityContextHolderStrategy 來實現的,如 clearContext()、 getContext ()、setContext、createEmptyContext()。SecurityContextHolderStrategy 介面的關鍵代碼如下:
查看代碼
/*
* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core.context;
/**
* A strategy for storing security context information against a thread.
*
* <p>
* The preferred strategy is loaded by {@link SecurityContextHolder}.
*
* @author Ben Alex
*/
public interface SecurityContextHolderStrategy {
// ~ Methods
// ========================================================================================================
/**
* Clears the current context.
*/
void clearContext();
/**
* Obtains the current context.
*
* @return a context (never <code>null</code> - create a default implementation if
* necessary)
*/
SecurityContext getContext();
/**
* Sets the current context.
*
* @param context to the new argument (should never be <code>null</code>, although
* implementations must check if <code>null</code> has been passed and throw an
* <code>IllegalArgumentException</code> in such cases)
*/
void setContext(SecurityContext context);
/**
* Creates a new, empty context implementation, for use by
* <tt>SecurityContextRepository</tt> implementations, when creating a new context for
* the first time.
*
* @return the empty context.
*/
SecurityContext createEmptyContext();
}
(1)strategy 實現
預設使用的 strategy 就是基於 ThreadLocal 的 ThreadLocalSecurityContextHolderStralegy 來實現的。
除了上述提到的,Spring Security還提供了 3種類型的strategy來實現。
- GlobalSecurityContextHolderStrategy:表示全局使用同一個 SecuntyContext,如 C/S 結構的客戶端。
- InheritableThreadLocalSecuntyContextHolderStrategy:使用 InhentableThreadLocal 來存放Security Context, 即子線程可以使用父線程中存放的變數。
- ThreadLocalSecuntyContextHolderStrategy: 使用ThreadLocal 來存放 SecurityContext
—般情況下,使用預設的strategy即可。但是,如果要改變預設的strategy, Spring Security 提供了兩種方法來改變"strategyName"
SecuntyContextHolder 類中有 3 種不同類型的 strategy,分別為 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL和MODE_GLOBAL,關鍵代碼如下:
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
publrc static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";
public static final String SYSTEM_PROPERTY = "spring.security.strategy";
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
MODE_THREADLOCAL是預設的方法。
如果要改變strategy,則有下麵兩種方法:
- 通過 SecurityContextHolder 的靜態方法 setStrategyName(java.Iang.String.strategyName) 來改變需要使用的strategy
- 通過系統屬性(SYSTEM_PROPERTY )進行指定,其中屬性名預設為"spring.security.strategy",屬性值為對應strategy的名稱。
(2)獲取當前用戶的SecurityContext()
Spring Security使用一個Authentication對象來描述當前用戶的相關信息。Security-ContextHolder中持有的是當前用戶的SecurityContext,而SecurityContext持有的是代表當前用戶相關信息的Authentication的引用。
這個Authentication對象不需要自己創建,Spring Security會自動創建相應的Authentication 對象,然後賦值給當前的SecurityContext。但是,往往需要在程式中獲取當前用戶的相關信息, 比如最常見的是獲取當前登錄用戶的用戶名。在程式的任何地方,可以通過如下方式獲取到當前用戶的用戶名。
public String getCurrentUsername(){
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails){
return ((UserDetails) principal).getUsername();
}
if (principal instanceof Principal){
return ((Principal) principal).getName();
}
return String.valueOf(principal);
}
getAuthentication()方法會返回認證信息。
getPrincipal()方法返回身份信息,它是UserDetails對身份信息的封裝。
獲取當前用戶的用戶名,最簡單的方式如下:
public String getCurrentUsername(){
return SecurityContextHolder.getContext().getAuthentication().getName();
}
在調用 SecurityContextHolder.getContext()獲取 SecurityContext 時,如果對應的 Securitycontext 不存在,則返回空的 SecurityContext。
2.3 ProviderManager
ProviderManager會維護一個認證的列表,以便處理不同認證方式的認證,因為系統可能會存在多種認證方式,比如手機號、用戶名密碼、郵箱方式。
在認證時,如果ProviderManager的認證結果不是null,則說明認證成功,不再進行其他方式的認證,並且作為認證的結果保存在SecurityContext中。如果不成功,則拋出錯誤信息 "ProviderNotFoundException"
2.4 DaoAuthenticationProvider
它是AuthenticationProvider最常用的實現,用來獲取用戶提交的用戶名和密碼,併進行正確性比對。如果正確,則返回一個資料庫中的用戶信息。
當用戶在前臺提交了用戶名和密碼後,就會被封裝成UsernamePasswordAuthentication-Token。然後,DaoAuthenticationProvider 根據 retrieveUser方法,交給 additionalAuthentication- Checks方法完成 UsernamePasswordAuthenticationToken 和 UserDetails 密碼的比對。如果這個方法沒有拋出異常,則認為比對成功。
比對密碼需要用到PasswordEncoder和SaltSource。
2.5 UserDetails
UserDetails是Spring Security的用戶實體類,包含用戶名、密碼、許可權等信息。Spring Security預設實現了內置的User類,供Spring Security安全認證使用,當然,也可以自己實現。
UserDetails 介面和 Authentication 介面很類似,都擁有 username 和 authorities。一定要區分清楚Authentication 的 getCredentials()與 UserDetails 中的 getPassword()。前者是用戶提交的密碼憑證,不一定是正確的,或資料庫不一定存在;後者是用戶正確的密碼,認證器要進行比對的就是兩者是否相同。
Authentication 中的 getAutho「ities()方法是由 UserDetails 的 getAuthoritiesl)傳遞而形成 的。UserDetails的用戶信息是經過Authenticationprovider認證之後被填充的
UserDetails中提供了以下幾種方法。
- String getPassword():返回驗證用戶密碼,無法返回則顯示為null。
- String getUsemame():返回驗證用戶名,無法返回則顯示為nulL
- boolean isAccountNonExpired():賬戶是否過期:過期無法驗證。
- boolean isAccountNonLocked():指定用戶是否被鎖定或解鎖,鎖定的用戶無法進行身份驗證。
- boolean isCredentialsNonExpired():指定是否已過期的用戶的憑據(密碼),過期的憑據無法認證。
- boolean isEnabled():是否被禁用。禁用的用戶不能進行身份驗證。
2.6 UserDetailsService
用戶相關的信息是通過UserDetailsService介面來載入的。該介面的唯一方法是 loadUserByUsername(String username),用來根據用戶名載入相關信息。這個方法的返回值是 UserDetails介面,其中包含了用戶的信息,包括用戶名、密碼、許可權、是否啟用、是否被鎖定、 是否過期等。
2.7 GrantedAuthority
GrantedAuthonty中只定義了一個getAuthority()方法。該方法返回一個字元串,表示對應許可權的字元串。如果對應許可權不能用字元串表示,則返回nulL
GrantedAuthority 介面通過 UserDetailsService 進行載入,然後賦予 UserDetails。
Authentication的getAuthorities()方法可以返回當前Authentication對象擁有的許可權,其返回值是一個GrantedAuthority類型的數組。每一個GrantedAuthority對象代表賦予當前用戶的一 種許可權。
2.8 Filter
(1)SecurityContextPersistenceFilter
它從SecurityContextRepository中取出用戶認證信息。為了提高效率,避免每次請求都要查詢認證信息,它會從Session中取岀已認證的用戶信息,然後將其放入SecurityContextHolder 中,以便其他Filter使用。
(2)WebAsyncManagerlntegrationFilter
集成了 SecurityContext 和 WebAsyncManager,把 Securitycontext 設置到非同步線程,使其也能獲取到用戶上下文認證信息。
(3)HanderWriterFilter
它對請求的Header添加相應的信息。
(4)CsrfFilter
跨域請求偽造過濾器。通過客戶端傳過來的token與伺服器端存儲的token進行對比,來判斷請求的合法性。
(5)LogoutFilter
匹配登岀URL。匹配成功後,退出用戶,並清除認證信息。
(6)UsernamePasswordAuthenticationFilter
登錄認證過濾器,預設是對“/login”的POST請求進行認證。該方法會調用attemptAuthentication, 嘗試獲取一個Authentication認證對象,以保存認證信息,然後轉向下一個Filter,最後調用 successfulAuthenlication 執行認證後的事件。
(7)AnonymousAuthenticationFilter
如果SecurityContextHolder中的認證信息為空,則會創建一個匿名用戶到Security-ContextHolder 中
(8)SessionManagementFilter
持久化登錄的用戶信息。用戶信息會被保存到Session、Cookie、或Redis中。
3.配置Spring Security
3.1 繼承 WebSecurityConfigurerAdapter
通過重寫抽象介面 WebSecurityConfigurerAdapter,再加上註解@EnableWebSecurity, 可以實現Web的安全配置。
WebSecurityConfigurerAdapter Config 模塊一共有 3 個 builder (構造程式)。
- AuthenticationManagerBuilder:認證相關builder,用來配置全局的認證相關的信息。它包含AuthenticationProvider和UserDetailsService f前者是認證服務提供者,後者是用戶詳情查詢服務。
- HttpSecurity:進行許可權控制規則相關配置。
- WebSecurity:進行全局請求忽略規則配置、HttpFirewall配置、debug配置、全局 SecurityFilterChain 配置。
配置安全,通常要重寫以下方法:
//通過auth對象的方法添加身份驗證
protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception{}
//通常用於設置忽略許可權的靜態資源
public void configure(WebSecurity webSecurity) throws Exception{}
//通過HTTP對象的authorizeRequests()方法定義URL訪問許可權。預設為formLogin()提供一個簡單的登錄驗證頁面
protected void configure(HttpSecurity httpSecurity) throws Exception{}
3.2 配置自定義策略
配置安全需要繼承WebSecurityConfigurerAdapter,然後重寫其方法,見以下代碼:
package com.intehel.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
//指定為配置類
@EnableWebSecurity
//指定為 Spring Security 如果是 WebFlux,則需要啟用@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//如果要啟用方法安全設置,則開啟此項。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
//不攔截靜態資源
web.ignoring().antMatchers("/static/**");
}
@Bean
public PasswordEncoder passwordEncoder() {
//使用BCrypt加密
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().usernameParameter("uname").passwordParameter("pwd").loginPage("admin/login").permitAll()
.and().authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN")
//除上面外的所有請求全部需要鑒權認證
.anyRequest().authenticated();
http.logout().permitAll();
http.rememberMe().rememberMeParameter("rememberMe");
//處理異常,拒絕訪問就重定向到403頁面
http.exceptionHandling().accessDeniedPage("/403");
http.logout().logoutSuccessUrl("/");
http.csrf().ignoringAntMatchers("/admin/upload");
}
}
代碼解釋如下。
- authorizeRequests(): 定義哪些URL需要被保護,哪些不需要被保護。
- antMatchers("/admin/**").hasRole("ADMIN"),定義/admin/下的所有 URL。只有擁有 admin角色的用戶才有訪問許可權。
- formLogin():自定義用戶登錄驗證的頁面。
- http.csrfO:配置是否開JSCSRF保護,還可以在開啟之後指定忽略的介面。
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/tyemeleaf-extras-springsecurity5">
<head>
<!--如果開啟了 CSRF,則一定在驗證頁面加入以下代碼以傳遞token值:-->
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
</head>
<body>
<form>
<!--如果要提交表單,則需要在表單中添加以下代碼以提交token值-->
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<!-- http.rememberMe(): "記住我"功能,可以指定參數。使用時,添加如下代碼:-->
<input class="i-checks" type="checkbox" name="rememberme"/> 記住我
</form>
</body>
</html>
3.3 配置加密方式
@Bean
public PasswordEncoder passwordEncoder() {
//使用BCrypt加密
return new BCryptPasswordEncoder();
}
在業務代碼中,可以用以下方式對密碼進行加密:
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encryptedPassword = bCryptPasswordEncoder.encode(password);
3.4 自定義加密規則
除預設的加密規則,還可以自定義加密規則。具體見以下代碼:
protected void encode(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(UserService()).passwordEncoder(new PasswordEncoder(){
@Override
public String encode(CharSequence rawPassword) {
return MD5Util.encode((String)rawPassword);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5Util.encode((String)rawPassword));
}
})
}
3.5 配置多用戶系統
一個完整的系統一般包含多種用戶系統,比如"後臺管理系統+前端用戶系統”。Spring Security 預設只提供一個用戶系統,所以,需要通過配置以實現多用戶系統。
比如,如果要構建一個前臺會員系統,則可以通過以下步驟來實現。
(1)構建UserDetailsService用戶信息服務介面
package com.intehel.service;
import com.intehel.domain.User;
import com.intehel.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
public class UserSecurityService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByName(username);
if (user==null){
User mobileUser = userRepository.findByMobile(username);
if (mobileUser==null){
User emailUser = userRepository.findByEmail(username);
if (emailUser==null){
throw new UsernameNotFoundException("用戶名,郵箱或手機號不存在!");
}else {
user = userRepository.findByEmail(username);
}
}else {
user = userRepository.findByMobile(username);
}
}else if ("locked".equals(user.getStatus())){
throw new LockedException("用戶被鎖定");
}
return user;
}
}
(2)進行安全配置
在繼承 WebSecurityConfigurerAdapter 的 Spring Security 配置類中,配置 UserSecurity- Service 類。
@Bean
UserDetailsService UserService() {
return new UserSecurityService();
}
如果要加入後臺管理系統,則只需要重覆上面步驟即可。
3.6 獲取當前登錄用戶信息的幾種方式
獲取當前登錄用戶的信息,在許可權開發過程中經常會遇到。而對新人來說,不太瞭解怎麼獲取, 經常遇到獲取不到或報錯的問題。所以,本節講解如何在常用地方獲取當前用戶信息。
(1)在Thymeleaf視圖中獲取
要Thymeleaf視圖中獲取用戶信息,可以使用Spring Security的標簽特性。
在Thymeleaf頁面中引入Thymeleaf的Spring Security依賴,見以下代碼:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div sec:authorize="isAnonymous()">
未登錄,單擊<a th:href="@{/home/login}">登錄</a>
</div>
<div sec:authorize="isAuthenticated()">
<p>已登錄</p>
<p>登錄名:<span sec:authentication="name"></span></p>
<p>角色:<span sec:authentication="principal.authorities"></span></p>
<p>name:<span sec:authentication="principal.username"></span></p>
<p>password:<span sec:authentication="principal.password"></span></p>
</div>
</body>
</html>
這裡要特別註意版本的對應。如果引入了 thymeleaf-extras-springsecurity依賴依然獲取不到信息,那麼可能是Thymeleaf版本和thymeleaf-extras-springsecurity的版本不對,請檢查在pom.xrnl文件的兩個依賴,見以下代碼,springboot中需加入starter依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.1.0.M1</version>
</dependency>
(2)在Controller中獲取
在控制器中獲取用戶信息有3種方式
@GetMapping("userinfo")
public String getProduct(Principal principal, Authentication authentication,HttpServletRequest request){
/**
* 1.通過Principal獲取
* */
String username1 = principal.getName();
/**
* 2.通過Authentication獲取
* */
String username2 = authentication.getName();
/**
* 3.通過HttpServletRequest獲取
* */
Principal httpPrincipal = request.getUserPrincipal();
String username3 = httpPrincipal.getName();
return username1;
}
(3)在Bean中獲取
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof AnonymousAuthenticationToken)){
String username = authentication.getName();
return username;
}
在其他 Authentication 類也可以這樣獲取。比如在 UsemamePasswoEAuthenticationToken 類中。
如果上面代嗎獲取不到,並不是代碼錯誤,則可能是因為以下原因造成的
- 要使上面的獲取生效,必須在繼承 WebSecurityConfigurerAdapter的類中的http.antMatcher("/*")的鑒權 URI 範圍內。
- 沒有添加 Thymeleaf 的 thymeleaf-extras-springsecurity 依賴。
- 添加了 Spring Security 的依械,但是版本不對,比如 Spring Security 和 Thymeleaf 的版本不對。
3.7 用Spring Security來實現後臺登錄及許可權認證功能
(1)引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.1.0.M1</version>
</dependency>
(2)創建許可權開放的頁面
這個頁面是不需要鑒權即可訪問的,以區別演示需要鑒權的頁面,見以下代碼:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>security案例</title>
</head>
<body>
<h1>welcome</h1>
<p><a th:href="@{/home}">會員中心</a></p>
</body>
</html>
(3)創建需要許可權驗證的頁面
其實可以和不需要鑒權的頁面一樣,鑒權可以不在HTML頁面中進行,見以下代碼:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<title>home</title>
</head>
<body>
<h1>hello 會員中心</h1>
<p th:inline="text">hello<span sec:authentication="name"></span></p>
<form th:action="@{/logout}" method="post">
<input type="submit" value="登出">
</form>
</body>
</html>
使用 Spring Security 5 之後,可以在模板中用<span sec:authentication="name"></span> 或[[${#httpServletRequest.remoteUser}]]來獲取用戶名。登岀請求將被髮送到“/logout”。成功 註銷後,會將用戶重定向到"/login?logout"
(4)配置 Spring Security
1.配置 Spring MVC
可以繼承WebMvcConfigurer,具體使用見以下代碼:
package com.intehel.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("authorize_security");
registry.addViewController("/").setViewName("open_security");
registry.addViewController("/login").setViewName("login");
}
}
2.配置 Spring Security
Spring Security的安全配置需要繼承WebSecurityConfigurerAdapter,然後重寫其方法, 見以下代碼:
package com.intehel.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
//指定為配置類
@EnableWebSecurity
//指定為 Spring Security 如果是 WebFlux,則需要啟用@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
//如果要啟用方法安全設置,則開啟此項。
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","/welcome","/login").permitAll()
//除上面外的所有請求全部需要鑒權認證
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/home")
.and()
.logout().permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(password)
.roles("USER");
}
}
代碼解釋如下
- @EnableWebSecurity 註解:集成了 Spmg Security 的 Web 安全支持。
- @WebSecurityConfig:在配置類的同時集成了 WebSecurityConfigurerAdapter,重寫了其中的特定方法,用於自定義Spring Security配置。Spring Security的工作量都集中在該配置類。
- configure(HttpSecurity):定義了哪些URL路徑應該被攔截。
- configureGlobal(AuthenticationManagerBuilder):任記憶體中配置一個用戶, admin/123這個用戶擁有User角色。
3.創建登錄頁面
登錄頁面要特別註意是否開啟了 CSRF功能。如果開啟了,則需要提交token信息。創建的登錄頁面見以下代碼:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>security example</title>
</head>
<body>
<div th:if="${param.error}">
無效的用戶名或密碼
</div>
<div th:if="${param.logout}">
你已經登出
</div>
<form th:action="@{/login}" method="post">
<div><label>用戶名:<input type="text" name="username"></label></div>
<div><label>密碼:<input type="password" name="password"></label></div>
<div><input type="submit" value="登錄"></div>
</form>
</body>
</html>
測試許可權:
(1)啟動項目,訪問首頁"http://localhost:8080",單擊“會員中心”,嘗試訪問受限的頁面 "http://localhost:8080/home"。由於未登錄,結果被強制跳轉到登錄頁面"http://localhost: 8080/login"
(2)輸入正確的用戶名和密碼(admin、123)之後,跳轉到之前想要訪問的“/home:", 顯示用戶名admin。
(3)單擊“登出”按鈕,回到登錄頁面。
3.8 許可權控制方式
(1)Spring EL許可權表達式
Spring Security支持在定義URL訪問或方法訪問許可權時使用Spring EL表達式。根據表達式返回的值(true或false)來授權或拒絕對應的許可權。Spring Security可用表達式對象的基類是 SecurityExpressionRoot,它提供了通用的內置表達式,見下表。
在視圖模扳文件中,可以通過表達式控制顯示許可權,如以下代碼:
<p sec:authorize="hasRole('ROLE_ADMIN')">管理員</p>
<p sec:authorize="hasRole('ROLE_USER')">管理員</p>
在WebSecurityConfig中添加兩個記憶體用戶用於測試,角色分別是ADMIN、USER:
.withUser("admin").password("123456").roles("ADMIN")
.and().withUser("user").password("123456").roles("USER");
用戶admin登錄,則顯示:
管理員
用戶user登錄,則顯示:
普適用戶
然後,在WebSecurityConfig中加入如下的URL許可權配置:
.antMatchers("/home").hasRole("ADMIN")
這時,當用admin用戶訪問“home”頁面時能正常訪問,而用user用戶訪問時則會提示“403 禁止訪問”。因為,這段代碼配置使這個頁面訪問必須具備ADMIN (管理員)角色,這就是通過 URL控制許可權的方法。
(2)通過表達式控制URL許可權
如果要限定某類用戶訪問某個URL,則可以通過Spring Security提供的基於URL的許可權控制來實現。Spring Security 提供的保護URL 的方法是重寫configure(HttpSecurity http)方法, HttpSecurity提供的方法見下表
還需要額外補充以下幾點。
- authenticated:保護URL,需要用戶登錄。如:anyRequest().authenticated()代表其他未配置的頁面都已經授權。
- permitAII(): 指定某些URL不進行保護。一般針對靜態資源文件和註冊等未授權情況下需要訪問的頁面。
- hasRole(String role):限制單個角色訪問。在Spring Security中,角色是被預設增加 “ROLE_”首碼的,所以角色"ADMIN"代表"ROLE_ADMIN”。
- hasAnyRole(String- roles):允許多個角色訪問。這和Spring Boot1. x版本有所不同。
- access(String attribute):該方法可以創建複雜的限制,比如可以增加RBAC的許可權表達式。
- haslpAddress(String ipaddressExpression):用於限制 IP 地址或子網。
具體用法見以下代碼:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/static","/register").permitAll()
.antMatchers("/user/**").hasAnyRole("USER","ADMIN")
.antMatchers("/admin/**").access("hasRole('ADMIN') and hasIpAddress('localhost')")
.anyRequest().authenticated();
}
(3)通過表達式控制方法許可權
要想在方法上使用許可權控制,則需要使用啟用方法安全設置的註解@EnableGlobalMethodSecurity()它預設是禁用的,需要在繼承WebSecurityConfigurerAdapter的類上加註解來啟用, 還需要配置啟用的類型,它支持開啟如下三種類型。
- @EnableGlobalMethodSecurity(jsr250Enabled = true):開啟 JSR-250,
- @EnableGlobalMethodSecurity(prePostEnabled = true):開啟 prePostEnabled。
- @EnableGlobalMethodSecurity(securedEnabled= true):開啟 secured。
1.JSR-250
JSR是Java Specification Requests的縮寫,是Java規範提案。任何人都可以提交JSR, 以向Java平臺増添新的API和服務。JSR是Java的一個重要標準。
Java 提供了很多 JSR,比如 JSR-250、JSR-303、JSR-305、JSR-308。初學者可能會 對JSR有疑惑。大家只需要記住“不同的JSR其功能定義是不一樣的”即可。比如:JSR-303 主要是為數據的驗證提供了一些規範的API。這裡的JSR-250是用於提供方法安全設置的,它主要提供了註解 @RolesAllowed。
它提供的方法主要有如下幾種。
- @DenyAII:拒絕所有訪問°
- @RolesAllowed({"USER","ADMIN"}):該方法只要具有“USER”、“ADMIN”任意一種許可權就可以訪問。
- @PermitAII: 允許所有訪問。
2.prePostEnabled
除JSR-250註解外,還有prePostEnabled 它也是基於表達式的註解,並可以通過繼承GlobalMethodSecurityConfiguration類來實現自定義功能。如果沒有訪問方法的許可權,則會拋出 AccessDeniedException.
它主要提供以下4種功能註解。
(1)@PreAuthorize
它在方法執行之前執行,使用方法如下:
a.限制userid的值是否等於principal中保存的當前用戶的userid,或當前用戶是否具有 ROLE_ADMIN 許可權。
@PreAuthorize("#userld == authentication.principal.userid or hasAuthority('ADMIN')")
b.限制擁有ADMIN角色才能執行。
@PreAuthorize("hasRole('ROLE_ADMIN')")
c.限制擁有ADMIN角色或USER角色才能執行。
@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
d.限制只能査詢id小於3的用戶才能執行。
@PreAuthorize("#id<3")
e.限制只能查詢自己的信息,這裡一定要在當前頁面經過許可權驗證,否則會報錯。
@PreAuthorize("principal.username.equals(#username)")
f.限制用戶名只能為long的用戶。
@PreAuthorize("#user.name.equals('long')")
對於低版本的Spring Security,添加註解之後還需要將AuthenticationManager定義為 Bean,具體見以下代碼:
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
(2)@PostAuthorize
表示在方法執行之後執行,有時需要在方法調用完後才進行許可權檢查。可以通過註解 @PostAuthorize 達到這一效果。
註解@PostAuthorize是在方法調用完成後進行許可權檢查的,它不能控制方法是否能被調用, 只能在方法調用完成後檢查許可權,來決定是否要拋岀AccessDeniedException,這裡也可以調用方法的返回值。如果EL為false,那麼該方法己經執行完了,可能會回滾。EL 變數returnObject表示返回的對象,如:
@PostAuthorize("returnObject.userId==authentication.principal.userId or hasPermission(returnObject,'ADMIN')")
(3)@PreFilter
表示在方法執行之前執行。它可以調用方法的參數,然後對參數值進行過濾、處理或修改。EL 變數filterObject表示參數。如有多個參數,則使用filterTarget註解參數。方法參數必須是集合或數組
(4)@postFilter
表示在方法執行之後執行。而且可以調用方法的返回值,然後對返回值進行過濾、處理或修改, 並返回。EL變數returnObject表示返回的對象。方法霊要返回集會或數組。
如使用@PreFilter和@PostFilter時,Spring Security將移除使對應表達式的結果為false 的元素。
當Filter標註的方法擁有多個集合類型的參數時,需要通過filterTarget屬性指定當前是針對哪個參數進行過濾的。
4. securedEnabled
開啟securedEnabled支持後,可以使用註解@Secured來認證用戶是否有許可權訪問。使用方法見以下代碼:
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")public User getUser(Long userId);
@Secured("ROLE_TELLER")
實例:使用JSR-250註解
(1)開啟支持
在安全配置類中,啟用@EnableGlobalMethodSecurity(jsr250Enabled = true)
(2)創建user服務介面 UserService,見以下代碼:
package com.intehel.service;
public interface UserService {
public String addUser();
public String updateUser();
public String deleteUser();
}
(3)實現user服務介面的方法,見以下代碼:
package com.intehel.service.Impl;
import com.intehel.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.security.RolesAllowed;
@Service
public class UserServiceImpl implements UserService {
@Override
public String addUser() {
System.out.println("addUser");
return null;
}
@Override
@RolesAllowed({"ROLE_USER","ROLE_ADMIN"})
public String updateUser() {
System.out.println("updateUser");
return null;
}
@Override
@RolesAllowed({"ROLE_ADMIN"})
public String deleteUser() {
System.out.println("deleteUser");
return null;
}
}
(4)編寫控制器,見以下代碼:
package com.intehel.controller;
import com.intehel.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/addUser")
public void addUser(){
userService.addUser();
}
@GetMapping("/updateUser")
public void updateUser(){
userService.updateUser();
}
@GetMapping("/deleteUser")
public void deleteUser(){
userService.deleteUser();
}
}
(5)配置類
package com.intehel.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
//指定為配置類
@EnableWebSecurity
//指定為 Spring Security 如果是 WebFlux,則需要啟用@EnableWebFluxSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","/welcome","/login").permitAll()
.antMatchers("/home").hasRole("ADMIN")
//除上面外的所有請求全部需要鑒權認證
.anyRequest().authenticated()
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/home")
.and()
.logout().permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("admin").password(password)
.roles("ADMIN")
.and().withUser("user").password(password).roles("USER");
}
}
(6)測試
啟動項目,登錄user用戶訪問localhost:8080/user/addUser 則控制台輸出提示:
addUser
訪問http://localhost:8080/user/deleteUser 則會提示沒有許可權:
實例:實現RBAC許可權模型
本實例介紹在Spring Security配置類上配置自定義授權策略,可以通過加入access屬性和 URL判斷來實現RBAC許可權模型的核心功能。
RBAC模型簡化了用戶和許可權的關係。通過角色對用戶進行分組,分組後可以很方便地逬行權 限分配與管理。RBAC模型易擴展和維護。下麵介紹具體步驟
(1)創建RBAC驗證服務介面。
用於許可權檢查,見以下代碼
public interface RabeService {
boolean check(HttpServletRequest request, Authentication authentication);
}
(2)編寫RBAC服務實現,判斷URL是否在許可權表中
要實現RBAC服務,步驟如下:
- 通過註入用戶和該用戶所擁有的許可權(許可權任登錄成功時已經緩存起來,當需要訪問該用戶的許可權時,直接從緩存取岀)驗證該請求是否有許可權,有就返回true,沒有則返回false,不允許訪問該URL。
- 傳入request,可以使用request獲取該次請求的類型。
- 根據Restful風格使用它來控制的許可權。如請求是POST,則證明該請求是向伺服器發送一 個新建資源請求,可以使用getMethod()來獲取該請求的方式。
- 配合角色所允許的許可權路徑進行判斷和授權操作。
- 如果獲取到的Principal對象不為空,則代表授權已經通過。
本實例不針對HTTP請求進行判斷,只根據URL逬行鑒權,具體代碼如下
@Component("rabcService")
public class RabeServiceImpl implements RabeService {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
private SysPermissionRepository permissionRepository;
@Autowired
private SysUserRepository sysUserRepository;
@Override
public boolean check(HttpServletRequest request, Authentication authentication) {
Object principal = authentication.getPrincipal();
boolean hasPermission = false;
if (principal!=null && principal instanceof UserDetails){
String userName = ((UserDetails) principal).getUsername();
Set<String> urls = new HashSet<String>();
SysUser sysUser = sysUserRepository.findByName(userName);
try {
for (SysRole role : sysUser.getRoles()) {
for (SysPermission permission: role.getPermissions()) {
urls.add(permission.getUrl());
}
}
}catch (Exception e) {
e.printStackTrace();
}
for (String url : urls){
if (AntPathMatcher.match(url, request.getRequestURI())){
hasPermission = true;
break;
}
}
}
return hasPermission;
}
}
(3)配置 HttpSecurity
在繼承 WebSecurityConfigurerAdapter 的類中重寫 void configure(HttpSecurity http)方法,添加如下代碼:
.antMatchers("/admin/**").access("@rabcService.check(request,authentication)")
這裡註意,@rbacService介面的名字是服務實現上定義的名字,即註解@Component("rbacService")定義的參數。具體代碼如下
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin").permitAll()
.antMatchers("/admin/rabc").access("@rabcService.check(request,authentication)")
.and()