基于Spring boot + Spring Security實現第一版傳統架構
本文是實訓邦的權限管理SpringSecurity+JWT的一個講義,分享給粉絲學習。想要對應學習視頻可以聯系哈。
任務案例分析
權限管理是一個幾乎所有后臺系統的都會涉及的一個重要組成部分,可以說是后臺項目的基本功,主要目的是對整個后臺管理系統進行權限的控制,而針對的對象是員工,避免因權限控制缺失或操作不當引發的風險問題,如操作錯誤,數據泄露等問題。
權限管理主要是管控下面三個方面:
哪些頁面要設置權限哪些操作要設置權限哪些數據要設置權限下面的例子就是控制頁面的訪問權限:
權限管理核心就是兩方面:認證和授權。下面我們參考一下認證的演進過程,去深入了解一下:
需求用例圖
權限管理流程
講義內容
1.使用Spring Security的HttpBasic模式實現登錄認證
2.使用Spring Security的FormLogin模式實現登錄認證
3.基于JSON的前后端分離開發的登錄認證
4.將權限管理系統部署到阿里云的docker;
5.基于MySQL數據庫的認證和授權。
1使用SpringSecurity的HttpBasic模式實現登錄認證
1.使用Spring Initializr快速構建項目
具體步驟: 在Intellij IDEA中選擇 File -> New - > Project -> Spring Initializr -> 點擊Next 。
2.填寫Group,Artifact,Packing選擇Jar ,點擊Next。
3.選取依賴,這里我們選擇Developer Tools中的Lombok、 Web依賴中的Spring Web、Templates Engines中的Thymeleaf 以及Security中的Spring Security,點擊Next
4.核對Project name和Project location,默認不變,選擇Finish。
5.構建成功
2.使用Thymeleaf制作項目業務頁面
具體步驟: 在com.sxbang.fridaysecuritytask包上右鍵選擇 New - > Java Class,輸入名字‘controller.HomeController’,點擊回車確定。
2.根據業務需求編輯HomeController.java。
3.使用Thymeleaf制作下面頁面:
index.html
user.html
role.html
menu.html
order.html
3.啟動運行項目,實現Httpbasic模式的登錄認證
啟動運行項目之后,我們可以看到無需任何配置就實現了登錄認證功能,這個就是SpringSecurity的Httpbasic模式。
1.啟動運行項目后,可以在控制臺看到輸出的密碼,我們首先復制這個密碼:
2.使用瀏覽器訪問localhost:8080:
會彈出一個登錄框,這個登錄框不是我們編碼實現的,是由SpringSecurity來實現。
3.在登陸頁面的Username中輸入user,在Password中把剛剛在控制臺復制的密碼粘貼進去,點擊Signin,就可以成功訪問到我們的頁面啦。
SpringSecurity基本原理:
其中,表單登錄只是其中的一種過濾方式,httpBasic這種過濾方式是在表單登錄之后,類似于責任鏈模式,除了這兩種方式SpringSecurity還支持很多種過濾方式。當請求通過這些綠色的過濾器之后,請求會進入到FilterSecurityInterceptor適配器上,這個是整個SpringSecurity過濾器的最后一環,是最終的守門人,它會去決定請求最終能否去訪問到我們的Rest服務。
流程說明:
客戶端發起一個請求,進入 Security 過濾器鏈。
當到 LogoutFilter 的時候判斷是否是登出路徑,如果是登出路徑則到 logoutHandler ,如果登出成功則到 logoutSuccessHandler 登出成功處理,如果登出失敗則由 ExceptionTranslationFilter ;如果不是登出路徑則直接進入下一個過濾器。
當到 UsernamePasswordAuthenticationFilter 的時候判斷是否為登錄路徑,如果是,則進入該過濾器進行登錄操作,如果登錄失敗則到 AuthenticationFailureHandler 登錄失敗處理器處理,如果登錄成功則到 AuthenticationSuccessHandler 登錄成功處理器處理,如果不是登錄請求則不進入該過濾器。
當到 FilterSecurityInterceptor 的時候會拿到 uri ,根據 uri 去找對應的鑒權管理器,鑒權管理器做鑒權工作,鑒權成功則到 Controller 層否則到 AccessDeniedHandler 鑒權失敗處理器處理。
2.使用Spring Security的FormLogin模式實現登錄認證
相信大家看過上面HttpBasic模式后發現實際項目應用中它并不適合,因為我們往往都是自己開發一個自定義的登陸頁面,Spring Security的FormLogin模式就支持這種需求,下面我們使用FormLogin模式來改寫我們的登錄認證。
我們先來一起看下需求:
1.應用中的所有請求都需要用戶登錄之后才能訪問
2.我們需要自己開發一個登陸頁面(login.html)
3.我們要允許所有用戶有權訪問登錄頁
4.如果用戶沒有登陸,必須跳轉到登錄頁進行登錄
5.用戶成功登陸后,我們需要根據用戶不同的角色進行授權。
下面我們開始使用FormLogin模式,具體步驟:
1.編寫login.html。
2.創建一個繼承WebSecurityConfigurerAdapter的SecurityConfig類,重寫configure(HttpSecurity http) 方法,用來配置登錄驗證邏輯。
上圖代碼分三段理解:
1.配置認證,開啟formLogin模式
2.配置權限
3.禁用跨站csrf攻擊防御。
3.這里我們采用內存中身份認證的方法,在SecurityConfig類重寫configure(AuthenticationManagerBuilder auth)方法,增加user和admin兩個用戶的配置,后續我們會根據RBAC模型設計數據表,實現基于數據庫動態的配置。
官方推薦使用BCryptPasswordEncoder進行密碼加密。
bcrypt是一種跨平臺的文件加密工具。bcrypt 使用的是布魯斯·施內爾在1993年發布的 Blowfish 加密算法。由它加密的文件可在所有支持的操作系統和處理器上進行轉移。它的口令必須是8至56個字符,并將在內部被轉化為448位的密鑰。
4.運行驗證,使用瀏覽器訪問:localhost:8080。
根據權限配置,user用戶可以訪問訂單頁面,不能訪問用戶管理、角色管理和菜單管理,下面我們分別訪問訂單頁面和用戶管理頁面,看一下是不是和我們的代碼配置一致。
點擊訂單頁面,可以正常訪問:
點擊用戶管理頁面,提示我們被禁止訪問:
5.一行代碼實現登出功能。
Spring Security幫我們實現登出功能的大部分代碼,我們只需要在configure(HttpSecurity httpSecurity)方法內添加一行即可:
然后在index.html中增加‘<a href="/logout" >退出</a>’即可。
3,基于JSON的前后端分離開發的登錄認證
前面的例子,在發送登錄請求并認證成功之后,頁面會跳轉回原訪問頁,但在前后端分離開發、通過JSON數據完成交互的應用中,會在登錄時返回一段JSON數據,告知前端登錄成功與否,由前端決定如何處理后續邏輯,而非由服務器主動執行頁面跳轉,下面我們就看看這種情況如何實現。
Spring Security表單登錄配置模塊提供了successHandler()和failureHandler()兩個方法,分別處理登錄成功和登錄失敗的邏輯。其中,successHandler()方法帶有一個Authentication參數,攜帶當前登錄用戶名及其角色等信息;而failureHandler()方法攜帶一個AuthenticationException異常參數。具體處理方式需按照系統的情況自定義。
實現思路:1.修改login.html,使用JSON數據傳遞username和password;2.自定義登陸成功時的處理邏輯;3.自定義登錄失敗時的處理邏輯。
實現步驟:1.修改login.html
2.在com.sxbang.fridaysecuritytask.config.security.handler包下創建MyAuthenticationSuccessHandler類,使它實現AuthenticationSuccessHandler接口,重寫onAuthenticationSuccess(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,Authenticationauthentication)方法來實現登陸成功返回邏輯。
3.在com.sxbang.fridaysecuritytask.config.security.handler包下創建MyAuthenticationFailureHandler類,使它實現AuthenticationFailureHandler接口,重寫onAuthenticationFailure(HttpServletRequesthttpServletRequest,HttpServletResponsehttpServletResponse,AuthenticationExceptione)方法來實現登陸失敗返回邏輯。
4.在configure(HttpSecurityhttpSecurity)方法中分別調用successHandler(successHandler)和failureHandler(failureHandler)方法來實現自定義處理邏輯
5.運行檢驗效果
4,將權限管理系統部署到阿里云的docker
本節內容,我們使用IntelliJ IDEA的Docker插件幫助我們將當前權限管理應用制作成Docker鏡像、運行在指定的遠程機器(阿里云)上。
實現步驟:1.在CentSO系統上開啟Docker的遠程連接,如果你對docker安裝和基本的操作還不熟悉,請參考我的docker課程之后再繼續下面的內容。
編輯此文件:/lib/systemd/system/docker.service,把ExecStart=/usr/bin/dockerd-current \改為ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \,如下:
重新加載配并重啟docker:
確保端口2375已開啟,如果使用阿里云等云服務,記得在安全策略上配置端口2375
2.IntellijIDEA安裝Docker插件,打開Idea,從File->Settings->Plugins->InstallJetBrainsplugin進入插件安裝界面,在搜索框中輸入docker,可以看到Dockerintegration,點擊右邊的Install按鈕進行安裝。安裝后重啟Idea。
3.重啟后配置docker,連接到遠程docker服務。從File->Settings->Build,Execution,Deployment->Docker打開配置界面。在設置頁面,按照下圖的數字順序創建一個Dockerserver并進行設置,輸入Docker服務所在機器的IP地址,如果連接成功頁面上會立即提示"Connectionsuccessful"
4.在fridaysecuritytask項目目錄下創建Dockerfile,內容如下:
5.按照下圖操作,創建一個Dockerfile的配置
在個"RunMavenGoal"點擊后,輸入要執行的maven命令cleanpackage-U-DskipTests,表示每次在構建鏡像之前,都會將當前工程清理掉并且重新編譯構建:
6.點擊三角按鈕運行驗證
啟動運行成功,使用瀏覽器訪問:http://宿主機IP:8081,如果是阿里云等云服務,記得在安全組規則中增加8081端口.
5.基于MySQL數據庫的認證和授權
到目前為止,我們仍然只有一個可登錄的用戶,怎樣引入多用戶呢?非常簡單,我們只需實現一個自定義的UserDetailsService即可。
UserDetailsService僅定義了一個loadUserByUsername方法,用于獲取一個UserDetails對象。UserDetails對象包含了一系列在驗證時會用到的信息,包括用戶名、密碼、權限以及其他信息,Spring Security會根據這些信息判定驗證是否成功。
1.創建我們的數據表,并插入三條數據,這里要注意,對密碼123456我們使用的機密存儲。
-- ------------------------------ Table structure for users-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`user_id` bigint NOT NULL AUTO_INCREMENT,
`user_name` varchar(30) COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`status` char(1) COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '0正常1停用',
`roles` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '多個角色用逗號間隔',
PRIMARY KEY (`user_id`)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- ------------------------------ Records of users-- ----------------------------
INSERT INTO `users` VALUES ('1', 'admin', '$2a$10$nNQI9Ij1rU5NG9JFLQphweTOteCX6O211Nysrg2V5rRSGDRmRWtm.', '0', 'ROLE_ADMIN,ROLE_USER');
INSERT INTO `users` VALUES ('2', 'user', '$2a$10$nNQI9Ij1rU5NG9JFLQphweTOteCX6O211Nysrg2V5rRSGDRmRWtm.', '0', 'ROLE_USER');
INSERT INTO `users` VALUES ('3', 'alex', '$2a$10$nNQI9Ij1rU5NG9JFLQphweTOteCX6O211Nysrg2V5rRSGDRmRWtm.', '0', 'ROLE_ADMIN,ROLE_USER');
2.在pom.xml中配置MySQL數據庫以及Spring data jpa
<dependency> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope> </dependency>
<dependency> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
/dependency>
3.在application.yml中配置數據庫來連接參數
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:12345/friday?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: root password: 123456
4.構建Users實體
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users {
? ? /**
? ? * 用戶ID
? ? */
? ? @Id
? ? @GeneratedValue(strategy = GenerationType.IDENTITY)
? ? @Column
? ? private Long userId;
? ? /**
? ? * 用戶賬號
? ? */
? ? @Column(name = "user_name")
? ? private String userName;
? ? /**
? ? * 密碼
? ? */
? ? @Column(name = "password")
? ? private String password;
? ? /**
? ? * 帳號狀態(0正常 1停用)
? ? */
? ? @Column(name = "status")
? ? private String status;
? ? /**
? ? * 用戶角色(多角色用逗號間隔)
? ? */
? ? @Column(name = "roles")
? ? private String roles;
}
5.Users實體實現UserDetails接口,實現UserDetails定義的幾個方法: ◎ isAccountNonExpired、isAccountNonLocked 和 isCredentialsNonExpired 暫且用不到,統一返回 true,否則Spring Security會認為賬號異常。 ◎ isEnabled對應enable字段,將其代入即可。 ◎ getAuthorities方法本身對應的是roles字段,但由于結構不一致,所以此處新建一個,并在后續進行填充。
/**
* 用戶對象 users
*/
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Users implements UserDetails {
? ? /**
? ? * 用戶ID
? ? */
? ? @Id
? ? @GeneratedValue(strategy = GenerationType.IDENTITY)
? ? @Column
? ? private Long userId;
? ? /**
? ? * 用戶賬號
? ? */
? ? @Column(name = "user_name")
? ? private String userName;
? ? /**
? ? * 密碼
? ? */
? ? @Column(name = "password")
? ? private String password;
? ? /**
? ? * 帳號狀態(0正常 1停用)
? ? */
? ? @Column(name = "status")
? ? private String status;
? ? /**
? ? * 用戶角色(多角色用逗號間隔)
? ? */
? ? @Column(name = "roles")
? ? private String roles;
? ? //實體類中使想要添加表中不存在字段,就要使用@Transient這個注解了。
? ? @Transient
? ? private List<GrantedAuthority> authorities;
? ? public void setAuthorities(List<GrantedAuthority> authorities) {
? ? ? ? this.authorities = authorities;
? ? }
? ? @Override
? ? public Collection<? extends GrantedAuthority> getAuthorities() {
? ? ? ? return this.authorities;
? ? }
? ? @Override
? ? public String getUsername() {
? ? ? ? return this.userName;
? ? }
? ? /**
? ? * 賬戶是否未過期,過期無法驗證
? ? */
? ? @Override
? ? public boolean isAccountNonExpired() {
? ? ? ? return true;
? ? }
? ? /**
? ? * 指定用戶是否解鎖,鎖定的用戶無法進行身份驗證
? ? *
? ? * @return
? ? */
? ? @Override
? ? public boolean isAccountNonLocked() {
? ? ? ? return true;
? ? }
? ? /**
? ? * 指示是否已過期的用戶的憑據(密碼),過期的憑據防止認證
? ? *
? ? * @return
? ? */
? ? @Override
? ? public boolean isCredentialsNonExpired() {
? ? ? ? return true;
? ? }
? ? /**
? ? * 是否可用 ,禁用的用戶不能身份驗證
? ? *
? ? * @return
? ? */
? ? @Override
? ? public boolean isEnabled() {
? ? ? ? return true;
? ? }
6.編寫接口UserDAO,繼承JpaRepository<T, ID>,
@Repository public interface UserDAO extends JpaRepository<Users, Long> { }
7.我們需要根據輸入用戶名在數據庫中查詢User數據,然后和輸入的密碼作比較,現在我們來實現根據用戶名查找User數據的代碼,新增UserService以及它的實現類。
public interface UserService {
? public Users selectUserByUserName(String userName); }
@Service
public class UserServiceImpl implements UserService {
? @Autowired
? private UserDAO userDAO;
? @Override
? public Users selectUserByUserName(String userName) {
? ? Users user = new Users(); user.setUserName(userName);
? ? List<Users> list = userDAO.findAll(Example.of(user));
? ? return list.isEmpty() ? null : list.get(0);
? }
}
8.實現UserDetailService邏輯
@Service
public class UserDetailServiceImpl implements UserDetailsService {
? @Autowired
? private UserService userService;
? @Override
? public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
? ? Users users = userService.selectUserByUserName(username);
? ? if (users == null){
? ? ? throw new UsernameNotFoundException("登錄用戶:" + username + " 不存在");
? ? }
? ? //將數據庫的roles解析為UserDetails的權限集
? ? //AuthorityUtils.commaSeparatedStringToAuthorityList將逗號分隔的字符集轉成權限對象列表
? ? users.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(users.getRoles()));
? ? return users;
? }
}
9.修改SecurityConfig文件,將之前的內容認證方式注銷掉,使用UserDetailService邏輯來實現登錄邏輯認證。
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
? auth.userDetailsService(myUserDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
// @Override
// protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.inMemoryAuthentication()
// .withUser("user")
// .password(bCryptPasswordEncoder().encode("123456"))
// .roles("USER")
// .and() // .withUser("admin")
// .password(bCryptPasswordEncoder().encode("123456"))
// .roles("ADMIN")
// .and()
// .passwordEncoder(bCryptPasswordEncoder());
//配置BCrypt加密
// }
10.啟動運行
登錄成功
Spring Security提供4種方式精確的控制會話的創建:
理解會話
會話(session)就是無狀態的 HTTP 實現用戶狀態可維持的一種解決方案。HTTP 本身的無狀態使得用戶在與服務器的交互過程中,每個請求之間都沒有關聯性。這意味著用戶的訪問沒有身份記錄,站點也無法為用戶提供個性化的服務。session的誕生解決了這個難題,服務器通過與用戶約定每個請求都攜帶一個id類的信息,從而讓不同請求之間有了關聯,而id又可以很方便地綁定具體用戶,所以我們可以把不同請求歸類到同一用戶。基于這個方案,為了讓用戶每個請求都攜帶同一個id,在不妨礙體驗的情況下,cookie是很好的載體。當用戶首次訪問系統時,系統會為該用戶生成一個sessionId,并添加到cookie中。在該用戶的會話期內,每個請求都自動攜帶該cookie,因此系統可以很輕易地識別出這是來自哪個用戶的請求。
4種方式控制會話
always:如果當前請求沒有session存在,Spring Security創建一個session;
ifRequired(默認): Spring Security在需要時才創建;
sessionnever: Spring Security將永遠不會主動創建session,但是如果session已經存在,它將使用該session;
stateless:Spring Security不會創建或使用任何session。適合于接口型的無狀態應用,該方式節省資源。