1.簡介
當前絕大多數(shù)網(wǎng)站都都存在著用戶認證
和用戶授權
這最基本的功能稠通,關于這兩個功能概述如下:
- 用戶認證:驗證某個用戶身份為系統(tǒng)中合法的身份藐俺,說白了就是驗證用戶有沒有權限來操作系統(tǒng)某些功能炕置。傳統(tǒng)做法通過==用戶名==和==密碼==來完成認證的功能
- 用戶授權:校驗某個用戶是否有權限去執(zhí)行某個操作辩涝。在一個系統(tǒng)中吏够,不同的用戶擁有的權限是不同的推正。例如:后臺管理系統(tǒng)顿颅,不同的用戶登錄進去缸濒,看到的界面不同,這就是用戶授權
Spring Security 就是一個這樣的用戶認證與授權框架粱腻,其介紹如下:
官網(wǎng)地址:https://spring.io/projects/spring-security
官方文檔地址:https://docs.spring.io/spring-security/site/docs/5.4.2/reference/html5/
2.對比
除了Spring Security
可以進行授權認證以外庇配,Apach Shiro
也可以進行授權認證,簡單對比如下:
-
使用方面
Shiro
比Spring
更容易使用绍些,實現(xiàn)和最重要的理解,在SSM
階段捞慌,授權認證一致都是Shiro
的天下,雖然Spring Security
已經(jīng)出現(xiàn)好久遇革,但是由于其配置復雜性卿闹,就讓很多人望而卻步了,同時對于一般的項目來說萝快,Shiro
也完全能夠勝任锻霎。
但是SpringBoot
以后,它的自動配置功能揪漩,簡化了Spring Security
配置步驟旋恼,只需要使用更少的配置來使用該框架
因此,具體是使用Shiro
還是Security
具體看整個項目的架構奄容,常見組合如下:
ssm + shiro
springboot / spring cloud + spring security
-
其他方面
Spring Security
與Spring
天然無縫結合冰更,同時還提供了 對 OAuth 與 OpenId的支持产徊,但是Shiro
則需要手動實現(xiàn)
3.實現(xiàn)
在這里先通過實現(xiàn)一個最基本的HelloWorld,來了解Spring Security工作原理
-
版本:
Spring Boot : 2.3.7.RELEASE
-
步驟
創(chuàng)建SpringBoot項目蜀细,并且導入相關依賴舟铜,具體POM文件如下:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- spring security 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
創(chuàng)建
Controller
,具體如下:package com.briup.security.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/test") public class TestController { @GetMapping("/hello") public String add() { return "hello security"; } }
由于SpringBoot項目默認使用8080端口奠衔,在這里修改其默認端口谆刨,如下:
server.port=8081
啟動項目,并且訪問地址:http://127.0.0.1:8081/test/hello
發(fā)現(xiàn)地址自動跳轉到了登錄界面归斤,進行授權認證
只要導入
Spring Security
依賴痊夭,那么默認就會有一個用戶名為user
,其密碼為啟動時打印出的加密字符串,如下將用戶名脏里,密碼填充進去即可完成認證她我,如下
這樣就完成了一個基本
Spring Security
的簡單用戶認證,實際項目中迫横,用戶名與密碼肯定是要從數(shù)據(jù)庫中查詢出來番舆,這里只是做一個簡單的認證感受一下該框架的魅力
4.原理
思考:
==從上述的例子中,體驗了Spring Security
認證流程员淫,那么它的原理到底是什么呢?==
Spring Security
是 基于 Servlet
過濾器鏈進行安全認證的合蔽,如下:
當客戶端發(fā)送請求,那么過濾器就會把該請求攔截下來介返,進行校驗拴事,校驗通過過濾器則放行,具體的過濾器如下:
從上圖中可以看出當用戶發(fā)送請求圣蝎,首先經(jīng)過了用戶名密碼校驗過濾器刃宵,我們來看一下該過濾器的源碼
UsernamePasswordAuthenticationFilter
部分源碼如下:
從源碼可以以下特點:
- 先判斷認證請求是否是
Post
請求,如果不是則拋出異常 - 再獲取用戶名密碼徘公,進行校驗牲证,校驗通過則把請求傳入下一個過濾器
經(jīng)過一系列的過濾器最終傳入到ExceptionTranslationFilter
,其源碼如下:
從圖中源碼可以看出,在該過濾器中主要是對異常進行處理关面,如果沒有異常坦袍,過濾器則直接放行到下一個過濾器FilterSecurityInterceptor
過濾器
FilterSecurityInterceptor
位于過濾器鏈的最底部,一個方法級別的過濾器等太,其源碼如下:
從源碼可以看出在請求放行之前需要先執(zhí)行之前所有的過濾器捂齐,才會進行放行。
總結如下:
5.加載
從上述中缩抡,知道Spring Security
本質就是一個過濾器鏈奠宜,通過不同的過濾器組合使用從而實現(xiàn)認證與授權。
==那么這些過濾器是如何被加載的呢,與Spring容器又存在什么關系呢压真?==
Spring Security
主要是用過DelegatingFilterProxy
去管理過濾器實例娩嚼。
當然該類也是一個過濾器,使用該類最大的好處就是可以通過Spring
容器來管理 Servler Filter
的生命周期
如果過濾器需要Spring容器中的實例滴肿,也可以直接注入
該類部分源碼如下:
在該類的源碼中岳悟,發(fā)現(xiàn)在doFilter
方法中會調用initDelegate
方法,該方法源碼如下:
該方法的主要作用就是從Spring
容器中拿到代理過濾器實例對象泼差,當該方法執(zhí)行完畢.
那么doFilter
方法緊接著就會調用invokeDelegate
,該方法的作用就是讓代理過濾器(FilterChainProxy
)去執(zhí)行doFilter
方法,其源碼如下:
從上圖源碼可知,在doFilter
方法中調用了doFilterInternal
方法竿音,該方法源碼如下:
在源碼中:
List<Filter> filters = getFilters(fwRequest);
該句代碼的意思就是將Spring Security
中所有的過濾器全部加載到過濾器鏈中。這樣就把所有的過濾器加載進來了
總結:
6 認證
6.1 簡介
==思考:==
通過之前的HelloWorld例子知道用戶名為user
,密碼則是啟動時隨機產(chǎn)生的一段加密字符串
但是在開發(fā)中拴驮,用戶名密碼都需要自定義或者從數(shù)據(jù)庫表查詢賬號跟密碼,那么這些在操作
在SpringSecurity
中如何實現(xiàn)柴信?
實現(xiàn)上述問題一共有三種方式:
- 通過配置文件
- 通過配置類
- 自定義實現(xiàn)類
接下來就讓挨個來實現(xiàn)這三種方式
6.2 配置文件
-
創(chuàng)建
SpringBoot
項目(spring-security-config-file
)套啤,并且導入Spring Security
依賴,pom.xml
部分內容如下:<dependencies> <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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
-
通過配置指定用戶名密碼
server.port=8081 spring.security.user.name=lisi spring.security.user.password=123456
-
創(chuàng)建Controller,內容如下:
package com.briup.security.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/file") public class ConfigFileController { @GetMapping("/hello") public String hello() { return "Hello Security"; } }
-
啟動并且進行測試
啟動后并沒有給我們產(chǎn)生密碼,這是因為制定了用戶名密碼随常,所以就不會產(chǎn)生密碼
訪問地址: http://127.0.0.1:8081/config/hello
輸入配置文件配置的用戶名密碼即可看到返回的結果,如下:
注意:這種方式只能用在學習階段潜沦,真正開發(fā)項目不會用這個
6.3 配置類
-
創(chuàng)建
SpringBoot
項目(spring-security-config-class
),并且導入Spring Security
依賴,pom.xml
部分內容如下:<dependencies> <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>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
-
創(chuàng)建配置類
SecurityConfig
绪氛,內容如下:package com.briup.security.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * Security 配置類 必須繼承 WebSecurityConfigurerAdapter * 同時必須加上 @Configuration注解 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 重寫該方法唆鸡,并且通過 auth 參數(shù)設置用戶名密碼 * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 對密碼進行加密 String password = passwordEncoder().encode("123456"); // 設置用戶名與密碼 以及 角色 由于這里只是學習,沒有用戶名和密碼枣察, // 因此直接寫死為admin auth.inMemoryAuthentication() .withUser("lisi") .password(password) .roles("adimin"); } /** * 配置加密 解密實例 * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
了解內容(start)
PasswordEncoder
接口 是Security
提供用來對密碼進行加密的接口争占,源碼如下:BCryptPasswordEncoder
是該接口的實現(xiàn)類,使用算法將接口三個方法全部實現(xiàn)因此使用該實現(xiàn)類的實例就可以對密碼進行加密
了解內容(end)
-
創(chuàng)建Controller序目,內容如下:
package com.briup.security.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/class") public class ConfigFileController { @GetMapping("/hello") public String hello() { return "Hello Security"; } }
-
啟動測試
注意:將項目端口設置為8082
訪問地址:http://127.0.0.1:8082/class/hello
輸入配置類配置好的用戶名密碼即可看到返回的結果臂痕,如下:
注意:這種方式在實際開發(fā)項目中也不會用到,只用作學習階段
6.4 自定義
-
簡介
實際開發(fā)中猿涨,更多的是用戶名密碼甚至包括角色是從數(shù)據(jù)庫中查詢出來握童,而且在登錄的時候會有一些用戶自定義的邏輯存在,例如 判斷賬號的狀態(tài)等等
但是上述兩種方式叛赚,用戶均不可以添加自定義邏輯澡绩,認證走的都是
Security
本身的那一套邏輯,因此急需要一套用戶可以自己定義認證邏輯的流程俺附。在
Spring Security
中就要想自定義邏輯肥卡,只需要實現(xiàn)UserDetailsSerivice
接口即可 -
準備工作
-
新建賬號表,存儲賬號數(shù)據(jù)昙读,以便認證時用戶名密碼從表中查詢
-- ---------------------------- -- Table structure for account -- ---------------------------- DROP TABLE IF EXISTS `account`; CREATE TABLE `account` ( `id` bigint(20) NOT NULL COMMENT '主鍵', `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用戶名', `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密碼', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of account -- ---------------------------- INSERT INTO `account` VALUES (1, 'lisi', '123321');
-
創(chuàng)建
SpringBoot
項目(spring-security-config-account
),pom.xml內容如下:<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>mysql</groupId> <artifactId>mysql-connector-java</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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
數(shù)據(jù)操作框架為
Spring Data JPA
-
application.yml
在
src/main/resources
下新增application.yml
召调,內容如下:server: port: 9999 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver # 數(shù)據(jù)庫地址 url: jdbc:mysql://172.16.0.154:3306/test?characterEncoding=utf8&useUnicode=true&useSSL=false&serverTimezone=GMT%2B8 username: root password: root
-
啟動類內容如下:
@SpringBootApplication public class SpringSecurityConfigAccountApplication { public static void main(String[] args) { SpringApplication.run(SpringSecurityConfigAccountApplication.class, args); } /* 加密實例 */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
-
POJO類 內容如下:
package com.briup.security.bean; import lombok.Data; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import java.io.Serializable; @Data @Table(name = "account") @Entity public class Account implements Serializable { @Id private Long id; private String username; private String password; }
-
DAO層 內容如下
package com.briup.security.dao; import com.briup.security.bean.Account; import org.springframework.data.jpa.repository.JpaRepository; public interface AccountDao extends JpaRepository<Account,Long> { Account findByUsername(String username); }
-
-
服務層開發(fā)
要想讓
Spring Security
走自定義登錄邏輯流程,就只需要實現(xiàn)UserDetailsService
接口,然后通過配置類進行指定即可唠叛。創(chuàng)建
MyDetailService
類只嚣,內容如下:package com.briup.security.service; import com.briup.security.bean.Account; import com.briup.security.dao.AccountDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.Objects; @Service("myDetailService") public class MyDetailService implements UserDetailsService { @Autowired private AccountDao accountDao; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Account account = accountDao.findByUsername(username); if (Objects.isNull(account)) { throw new UsernameNotFoundException("用戶名不存在"); } User user = new User(account.getUsername(), passwordEncoder.encode(account.getPassword()), AuthorityUtils.createAuthorityList("admin")); return user; } }
代碼解釋:
之所以實現(xiàn)這個接口,是因為
SpringSecurity
默認走的登錄邏輯流程就是UserDetailsService
接口實現(xiàn)類對象的登錄邏輯艺沼,從下圖可以看出該接口的實現(xiàn)類有多個如果用戶不實現(xiàn)該接口册舞,那么登錄邏輯默認就是其他實現(xiàn)類實例的登錄邏輯
返回的
UserDetails
,該接口主要包含一些用戶信息障般,其部分源碼如下:public interface UserDetails extends Serializable {} /** * 返回獲取用戶的所有權限 */ Collection<? extends GrantedAuthority> getAuthorities(); /** * 返回取用戶密碼 */ String getPassword(); /** * 返回獲取 */ String getUsername(); /** * 判斷賬戶是否為過期 */ boolean isAccountNonExpired(); /** * 判斷賬戶是被否鎖定 */ boolean isAccountNonLocked(); /** * 憑證(密碼) 是否過期 */ boolean isCredentialsNonExpired(); /** * 賬戶是否禁用 */ boolean isEnabled(); }
這是一個接口调鲸,因此最后返回其實現(xiàn)類對象
User
如下圖:由于設計表時,并沒有設計賬戶權限和是否過期等等挽荡,因此全部設置為
null
,權限集合為admin
注意:雖然上圖中的User對象藐石,只設置了
User
對象的用戶名,密碼以及角色權限定拟,但是查看其構造器源碼于微,在源碼中設置了幫助用戶設置了其他權限,如下 -
配置
新建
SecurityConfig
配置類青自,內容如下:package com.briup.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired @Qualifier("myDetailService") private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { /** * 設置認證邏輯為用戶自定義認證邏輯 * 設置密碼加密處理器為 BCryptPasswordEncoder */ auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder); } }
-
web層開發(fā)
新建
Controller
株依,內容如下:package com.briup.security.web; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/test") public class TestController { @GetMapping("/hello") public String hello() { return "hello security"; } }
-
測試
當輸入的賬號與密碼錯誤時,則直接報錯