簡單的開始
創建SpringBoot項目
首先創建一個簡單的springboot項目,假設端口為8888,添加controller控制層,并在其中添加TestController
控制類,那么啟動springboot項目之后,訪localhost:8888/api/message
頁面會顯示my first message
@RestController
@RequestMapping("/api")
public TestController{@GetMapping("/messages")public String myMessage(){return "my first message";}
}
添加SpringSecurity的依賴
<dependencies><!-- ... 其他依賴元素 ... --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId><!--可以通過下面的內容進行版本指定--><spring-security.version>6.2.0-SNAPSHOT</spring-security.version></dependency>
</dependencies>
SpringSecurity認證登錄
運行springboot項目后,在控制臺輸出窗口出現:
Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336
嘗試訪問localhost:port
任意后端接口地址,可以發現出現了登錄窗口,
使用user: user
password:8e557245-73e2-4286-969a-ff57fe326336
這里的密碼就是控制臺輸出的密碼。
這就是springsecurity的端口認證機制。
原理說明
Filter和FilterChain
當客戶端向應用程序發送請求時,SpringSecurity會創建一系列的Filter
來過濾請求,這樣的Filter
有多個,這些Filter
構成了從客戶端到Servlet的一個FilterChain
,在通過FilterChain
的過濾之后,這個請求才會被Servlet處理。
需要注意的是Filter
會影響下游的 Filter
實例,當匹配到一個Filter
之后就不再匹配下面的Filter
流程如下所示。
過濾過程的偽代碼
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {// 在過濾之前的動作chain.doFilter(request, response); // 進行過濾// 過濾之后的動作
}
FilterChainProxy和SecurityFilterChain
基本流程
當一個請求來臨時,我們常常會有這樣的動作,對于某一個請求接口,去查看對應的過濾器,比如對account相關的接口,我們就會給account制定對應的過濾器,當account相關的請求來臨時,我們必然的就需要去通過account的過濾器去處理該請求。
FilterChainProxy
就是這樣的一個角色, 用來確定當前請求應該調用哪些 Spring Security Filter
實例。
SecurityFilterChain
的作用就是將過濾器進行分類,用來被FilterChainProxy
識別調用。
因此當設計多個接口過濾器時,基本架構如下圖所示
舉例說明
FilterChainProxy
決定應該使用哪個 SecurityFilterChain
。只有第一個匹配的 SecurityFilterChain
被調用。
- 如果請求的URL是
/api/messages
,它首先與/api/**
的SecurityFilterChain0
模式匹配,所以只有SecurityFilterChain0
被調用,盡管它也與SecurityFilterChainn
匹配。 - 如果請求的URL是
/messages
,它與/api/**
的SecurityFilterChain_0
模式不匹配,所以FilterChainProxy
會繼續順序嘗試下面的SecurityFilterChain
。假設沒有其他SecurityFilterChain
實例相匹配,則調用SecurityFilterChain_n
。
工作流程
1. 自動配置的過程
-
UserDetailsServiceAutoConfiguration
類上的條件注解-
@ConditionalOnClass(AuthenticationManager.class)
? 確保
AuthenticationManager
類在類路徑上。 -
@ConditionalOnBean(ObjectPostProcessor.class)
? 確保 Spring 容器中存在
ObjectPostProcessor
的 Bean -
@ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class })
? 確保 Spring 容器中沒有定義
AuthenticationManager
、AuthenticationProvider
和UserDetailsService
的 Bean。
-
-
默認InMemoryUserDetailsManager 的Bean 的創建
如果上述條件都滿足,
UserDetailsServiceAutoConfiguration
會創建一個InMemoryUserDetailsManager
的 Bean 作為默認的用戶詳細信息服務管理器。這個管理器會在內存中創建一個用戶,通常用戶名為 “user”,密碼為隨機生成的 UUID,這個角色為 “USER”。在創建
InMemoryUserDetailsManager
時,UserDetailsServiceAutoConfiguration
會檢查SecurityProperties
中定義的用戶密碼。如果密碼是生成的,它會記錄一條日志,顯示使用的密碼。同時,它還會檢查密碼是否已經使用某種算法進行了編碼,如果沒有,它會使用{noop}
前綴,表示密碼沒有被編碼。創建過程代碼:可以簡單瀏覽,之后自己創建配置時會借鑒到
@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,// springsecurity的配置文件,里面有默認的用戶名,可以進入 SecurityProperties 查看詳細數據ObjectProvider<PasswordEncoder> passwordEncoder) // 密碼編碼器{SecurityProperties.User user = properties.getUser(); // 配置文件中的用戶List<String> roles = user.getRoles(); // 獲取角色return new InMemoryUserDetailsManager(new UserDetails[]{User.withUsername(user.getName()) // 賬號.password( // 密碼this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)) //角色.build()});}
-
注冊Bean
最終,
UserDetailsServiceAutoConfiguration
會將InMemoryUserDetailsManager
注冊為 Spring 應用上下文中的一個 Bean,這樣 Spring Security 在認證時就可以使用這個默認的用戶詳細信息服務。
2. UserDetailsService的作用
我們通過上面InMemoryUserDetailsManager的類,可以分析得出
-
InMemoryUserDetailsManager實現了UserDetailsManager、UserDetailsPasswordService中的方法
-
UserDetailsManager繼承UserDetailsService
因此InMemoryUserDetailsManager的關鍵就是UserDetailsManager
、UserDetailsPasswordService
以及實現自UserDetailsService
中的方法
接口 | 方法 | 描述 |
---|---|---|
UserDetailsManager | void createUser(UserDetails user) | 根據提供的用戶詳情創建一個新用戶賬號 |
void updateUser(UserDetails user) | 更新指定的用戶賬號 | |
void deleteUser(String username) | 從系統中刪除具有給定登錄名的用戶賬號 | |
void changePassword(String oldPassword, String newPassword) | 修改用戶賬號的密碼。這應該在持久的用戶存儲庫中更改用戶的密碼(數據庫、LDAP等) | |
boolean userExists(String username) | 檢查具有給定登錄名的用戶賬號是否存在于系統中 | |
UserDetails loadUserByUsername(String username) | 根據用戶名加載用戶信息,此方法從 UserDetailsService 繼承 | |
UserDetailsPasswordService | UserDetails updatePassword(UserDetails user, String newPassword) | 更新用戶密碼。在用戶登錄成功后,如果檢測到密碼需要更新(例如,密碼策略變更),則調用此方法 |
? 而我們可以通過上面部分自動配置過程
可以知道,假如Spring 容器中定義了 AuthenticationManager
、AuthenticationProvider
和 UserDetailsService
的 Bean,那么自動配置文件將不會生效。
3. AuthenticationManager的作用
在Spring Security中,AuthenticationManager
是一個核心接口,負責對用戶的認證請求進行處理。它定義了一個 authenticate
方法,該方法接受一個 Authentication
對象作為參數,并返回一個完全認證過的 Authentication
對象。如果認證失敗,則拋出 AuthenticationException
。
ProviderManager
是 AuthenticationManager
的一個常見實現,它使用一個 AuthenticationProvider
列表來處理認證請求。每個 AuthenticationProvider
都有機會對認證請求進行處理,如果一個 AuthenticationProvider
無法處理請求,ProviderManager
會嘗試下一個。這個過程會一直持續,直到找到一個能夠成功認證請求的 AuthenticationProvider
,或者所有的 AuthenticationProvider
都嘗試完畢。
4. 手動配置賬號密碼
1)創建配置類、用戶管理器
因此我們創建自己的WebSecurityConfig
類 ,在里面進行InMemoryUserDetailsManager
的注入,并實現構造方法。這樣我們就手動創建了自己的配置內容。
@Configuration
public class WebSecurityConfig {@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager() {return new InMemoryUserDetailsManager();}
}
當然我們里面還沒有給InMemoryUserDetailsManager
添加任何用戶。
2)初始化用戶
添加下面代碼,在創建InMemoryUserDetailsManager
時新建一個用戶
@Configuration
public class WebSecurityConfig {@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager() {return new InMemoryUserDetailsManager(User.withUsername("user") // 用戶名.password("{noop}password") // 密碼,以{noop}開頭的話代表不加密.roles("a") // 使用可變參數傳遞角色.build());}
}
這樣,當我們啟動時,就可以根據上面的賬號和密碼進行登錄
3)添加用戶
當然我們也可以通過調用InMemoryUserDetailsManager
中的createUser
方法添加用戶的方式,來初始化manager用戶管理器,下面我們展示創建兩個用戶的過程。
@Configuration
public class WebSecurityConfig {@Beanpublic InMemoryUserDetailsManager inMemoryUserDetailsManager() {InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();manager.createUser(new User("admin", "{noop}123456", List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))));manager.createUser(new User("user", "{noop}654321", List.of(new SimpleGrantedAuthority("ROLE_USER"))));return manager;}
}
4)認證過程
我們通過上面的內容已經知道了初始化用戶
添加用戶
,同樣的里面的updateUser
deleteUser
changePassword
userExists
方法也基本類似,不再贅述。
接下來需要弄懂的就是如何認證的呢,我們明明沒有寫這些相關的方法。
通過最開始的流程圖,我們可以知道在配置好認證用戶之后,之后程序對于每一個請求都會進行攔截。
請求攔截
AbstractAuthenticationProcessingFilter
將請求攔截,并通過調用attemptAuthentication
方法進行處理,而這個方法的具體實現存在于UsernamePasswordAuthenticationFilter
中
將請求進行攔截,然后交給授權管理器AuthenticationManager
進行控制
授權管理器認證
進入authenticate()方法發現進入到一個AuthenticationManager
接口中,而這個接口的實現類是ProviderManager
在ProviderManager
類中的authenticate
方法委派認證工作給一個或多個AuthenticationProvider
驗證用戶是否存在
AuthenticationProvider
仍為一個接口,其默認實現類為AbstractUserDetailsAuthenticationProvider
AbstractUserDetailsAuthenticationProvider
的authenticate
方法流程如下:
- 開始認證:認證過程開始。
- 檢查Authentication類型:確保傳入的
Authentication
對象是UsernamePasswordAuthenticationToken
類型。 - 拋出異常:如果類型不匹配,拋出異常。
- 確定用戶名:從
Authentication
對象中獲取用戶名。 - 從Cahce緩存獲取UserDetails:嘗試從Cahce緩存中獲取
UserDetails
對象。 - Cahce未命中:如果Cahce未命中,從用戶信息源(如數據庫)檢索用戶信息。
- 用戶不存在:如果用戶不存在,根據配置拋出
UsernameNotFoundException
或BadCredentialsException
。 - 用戶存在:如果用戶存在,校驗用戶狀態(如賬戶是否過期、是否鎖定等)。
- 用戶狀態無效:如果用戶狀態無效,拋出
AuthenticationException
。 - 執行額外的認證檢查:執行任何額外的認證檢查(如密碼過期檢查)。
- 認證檢查失敗:如果認證檢查失敗,重新檢索用戶信息并再次執行檢查。
- 執行后置認證檢查:執行認證成功后的后置檢查。
- 后置檢查失敗:如果后置檢查失敗,拋出
AuthenticationException
。 - 檢查是否使用緩存:檢查認證過程中是否使用了緩存。
- 使用了緩存:如果沒有使用緩存,將用戶信息放入緩存。
- 創建認證成功的Authentication對象:創建一個新的
Authentication
對象,表示認證成功。 - 返回認證成功的Authentication對象:返回認證成功的
Authentication
對象。
先看前半部分查看用戶是否存在
在這里調用了retrieveUser
方法來進行用戶驗證獲取驗證結果,這個方法在DaoAuthenticationProvider
中進行驗證,是否存在該用戶。
在DaoAuthenticationProvider
中調用loadUserByUsername
方法進行具體內容的驗證,這個方法在前面UserDetailsService的作用中看到過
驗證密碼是否正確
在完成用戶存在驗證后,我們繼續看AbstractUserDetailsAuthenticationProvider
類,在這個類中使用additionalAuthenticationChecks
方法進行賬號密碼的驗證。
具體內容的實現仍在在DaoAuthenticationProvider
中
5)請求攔截
上面我們可以知道UsernamePasswordAuthenticationFilter
攔截器,攔截的只是login的請求,那對于之后的每一次請求是個什么樣的流程呢
通過攔截每一次請求,接著驗證是否被授權,因此我們之后在處理請求攔截時,可以同樣采用這樣的方式,進行借鑒
6)匯總
7)關于加密的過程
很多配置都是通過大致流程,因此可以擴展到理解其他的一些配置項。
我們發現在上面密碼驗證時,是設置了編碼器,那我們從來沒有配置過DaoAuthenticationProvider
,這里的密碼加密器是怎么配置的呢?
在DaoAuthenticationProvider
構造方法設置加密器的位置添加斷點。然后執行程序時不斷進入斷點。
進入了InitializeUserDetailsBeanManagerConfigurer
5. 結合數據庫進行用戶認證
數據庫和上面配置過程不同的是:
手動配置
- 首先創建springSecurity的用戶
- 在登錄時對用戶進行認證
- 與前面創建的用戶進行匹配
數據庫配置
- 不需要創建用戶
- 登錄時直接與數據庫中的用戶進行匹配
經過上面的過程,我們可以知道,主要的過程就是DaoAuthenticationProvider
創建時設置的UserDetailsService
,可以控制用戶的認證。
1)引入數據庫
我們采用springdatajpa操作數據庫
向pom中添加
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
設置yml內容
spring:datasource:url: jdbc:mysql://localhost:3306/springsecurityusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: update #自動生成數據庫show-sql: true
1)創建實體類
創建好之后記得手動在數據庫中添加一條數據用于測試
@Data
@Entity
@Table(name = "sys_user")
public class User {@Column(name = "user_id", unique = true, nullable = false, insertable = false, updatable = false)@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private int userId;@Column(name = "mobile")private String mobile;@Column(name = "pwd")private String password;@Column(name="identity")private int identity;@Column(name="nick_name")private String nickName;
}
2)創建Dao層
@Repository
public interface UserDao extends JpaRepository<User,Integer> {User findByMobileAndPassword(String mobile,String pwd);User findByMobile(String mobile);
}
3)仿照InMemoryUserDetailsManager創建MyUserDetailsManager
我們上面知道了,要想控制賬號密碼的驗證,我們就需要自己注入UserDetailsService
,這樣他就不會采用系統本身的驗證方案了。
@Component
public class MyUserDetailsManager implements UserDetailsService {@Resourceprivate UserDao userDao;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userDao.findByMobile(username);Collection<? extends GrantedAuthority> authorities = new ArrayList<>();return new org.springframework.security.core.userdetails.User(user.getMobile(),"{noop}"+user.getPassword(),// 這里{noop}前綴代表不進行加密,也就是匹配時與數據庫中的明文相同即可true,true,true,true,authorities);}
}
4)進行登錄測試
6.漏洞保護
6.1 csrf跨域保護請求禁用
如果不禁用csrf,那么所有的post請求均會被拒絕
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extendsWebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) {http.csrf(csrf -> csrf.disable());}
}
springsecurity實戰應用
1. 構建項目
項目框架
配置文件
pom
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><!--jwt依賴--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><!-- https://mvnrepository.com/artifact/cn.hutool/hutool-jwt --><dependency><groupId>cn.hutool</groupId><artifactId>hutool-jwt</artifactId><version>5.8.27</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-test</artifactId><scope>test</scope></dependency>
yml
server:port: 11012spring:datasource:url: jdbc:mysql://localhost:3306/springsecurityusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverjpa:hibernate:ddl-auto: updateshow-sql: true
實體類
@Data
@Entity
@Table(name = "sys_user")
public class User {@Column(name = "user_id", unique = true, nullable = false, insertable = false, updatable = false)@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private int userId;@Column(name = "mobile")private String mobile;@Column(name = "pwd")private String password;@Column(name="identity")private int identity;@Column(name="nick_name")private String nickName;
}
在執行項目之后,會自動構建數據庫,在構建好數據庫之后
記得手動插入一條數據
dao層
@Repository
public interface UserDao extends JpaRepository<User,Integer> {User findByMobileAndPassword(String mobile,String pwd);User findByMobile(String mobile);
}
服務層
public interface UserService {String login(String username,String password);
}
@Service
public class UserServiceImpl implements UserService {UserDao userDao;public UserServiceImpl(UserDao userDao) {this.userDao = userDao;}@Overridepublic String login(String username, String password) {User user = userDao.findByMobileAndPassword(username, password);if (user!= null) {return "login success"+user.getNickName();} else {return "login fail";}}
}
控制層
@RestController
@RequestMapping("/user")
public class UserController {UserService userService;public UserController(UserService userService) {this.userService = userService;}@GetMapping("/test")public String test() {return "tt";}@GetMapping("/login")public String login(@RequestParam(name = "account") String account, @RequestParam(name = "password") String password) {return userService.login(account, password);}
}
springSecurity
WebSecurityConfig
@Configuration
public class WebSecurityConfig {//加密器@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 授權管理器@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {return authConfig.getAuthenticationManager();}@Bean@Order(1)public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {http.securityMatcher("/user/login").authorizeHttpRequests(authorize -> authorize.anyRequest().anonymous() // 允許匿名訪問 /user/login);return http.build();}//Spring Security過濾鏈@Beanpublic SecurityFilterChain otherFilterChain(HttpSecurity http) throws Exception {http.authorizeHttpRequests(authorize -> authorize.anyRequest().hasRole("ADMIN")).httpBasic(withDefaults());return http.build();}}
Order的大小用于指明在第幾層,越小越靠上,可以理解為優先級,越小越大
如果不設置order,那么會按照先后順序進行配置
- 首先請求先通過order為1的過濾鏈,就是
/user/login
的請求,設置為允許匿名訪問 - 而對于沒有設置過濾鏈的請求,就會使用第二個配置
otherFilterChain
。這個配置被認為在apiFilterChain
之后,因為它的@Order
值在1
之后(沒有@Order
默認為最后)
DBUserDetailsManager
繼承了UserDetailsService,當加載用戶的時候,就會執行這里的loadUserByUsername
@Component
public class DBUserDetailsManager implements UserDetailsService {@Resourceprivate UserDao userDao;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userDao.findByMobile(username);if (user == null) {throw new UsernameNotFoundException(username);}return new MyUserDetail(user);}
}
MyUserDetail
新建自己的UserDetails,繼承原來的UserDetails,在里面添加我們自己定義的用戶類,這樣可以方便的存儲我們自己的用戶信息
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyUserDetail implements UserDetails {private User user; // 這是自己定義的用戶類@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getMobile();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
2. 初步測試
在上面已經完成了接口的簡單控制
我們可以通過訪問localhost:11012/user/login?account=123&password=123
發現可以訪問,并且登錄成功
但是當我們訪問localhost:11012/user/test
需要我們進行springsecurity的登錄
BasicAuthenticationFilter