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="https://www.cnblogs.com/liwenruo/archive/2022/07/28/${_csrf.token}">
<!-- http.rememberMe(): "記住我"功能,可以指定引數,使用時,添加如下代碼:-->
<input 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="https://www.cnblogs.com/liwenruo/archive/2022/07/28/@{/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="https://www.cnblogs.com/liwenruo/archive/2022/07/28/@{/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="https://www.cnblogs.com/liwenruo/archive/2022/07/28/登出">
</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="https://www.cnblogs.com/liwenruo/archive/2022/07/28/登錄"></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()
.formLogin();
}
????(4)創建物體,添加測驗資料
????這里要創建3個物體,分別是用戶【sys_user】(id、cnname、enabled、name、password)、權限【sys_permission】(id、available、name、parent_id、parent_ids、permission、resource_type、url)和角色物體【sys_role】(id、available、cnname、description、role),另外還需創建兩張表【sys_role_permission】(role_id、permission_id)、【sys_user_role】(role_id、uid)
????(5)啟動專案后進行測驗
- 訪問http://localhost:8080/admin/rbac,會提示無權訪問,跳轉到登錄頁面,http:"localhost: 8080/admin/login"
- 在登錄頁面輸入用戶名、密碼(admin/lzh)登錄,會提示登錄成功,
- 訪問"http://localhost:8080/admin/rbac", 提示訪問成功
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/500534.html
標籤:其他
上一篇:從零開始實作lmax-Disruptor佇列(六)Disruptor 解決偽共享、消費者優雅停止實作原理決議
下一篇:JavaWeb-MVC、過濾器

