菜鸟的spring security学习教程
菜鳥的spring security學習教程
- 說明
- 一、Spring Security簡介
- 二、Spring Security入門系列
- (1)默認登錄與注銷
- (2)自定義表單登錄
- (3)自定義表單用戶授權
- (4)基于數據庫的自定義表單認證
- (5)基于數據庫的自定義表單授權
- (6)獲取當前登錄用戶的信息
- (7)前后端分離下的基于表單數據的登錄驗證
- (8)前后端分離下的基于json數據的登錄驗證
- 三、SpringSecurity核心組件
- (1)Authentication
- (2)SecurityContext
- (3)SecurityContextHolder
- (4)UserDetails
- (5)UserDetailsService
- (6)AuthenticationManager
- 四、部分源碼解析
- (1)用戶認證流程
- 認證大致流程
- 認證具體流程
- (2)默認登錄用戶名與密碼配置
說明
更新時間:2020/5/31 22:50,更新了基于數據庫的認證與授權
更新時間:2020/6/6 17:45,更新了SpringSecurity核心組件
近期要用到spring security這個框架,由于spring security是之前學的,而且當時也沒有深入的學習,對于該框架的用法有點陌生了,現重新學習spring security并在此做好筆記,本文會持續更新,不斷地擴充
本文僅為記錄學習軌跡,如有侵權,聯系刪除
一、Spring Security簡介
Spring Security 是 Spring 家族中的一個安全管理框架,主要用于 Spring 項目組中提供安全認證服務,該框架主要的核心功能有認證、授權和攻擊防護。
二、Spring Security入門系列
(1)默認登錄與注銷
文件名:springboot_security2
pom配置
<dependencies><!--thymeleaf--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</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><!--SpringSecurity框架整合--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- thymeleaf和springsecurity5的整合 --><dependency><groupId>org.thymeleaf.extras</groupId><artifactId>thymeleaf-extras-springsecurity5</artifactId><version>3.0.4.RELEASE</version></dependency></dependencies>經過測試,發現一般只要配置了SpringSecurity之后,即pom導入配置后,只要一訪問控制器的接口,都會被攔截,自動跳轉到SpringSecurity自定義的登錄界面,界面如下:
Security自定義的賬號是user,密碼則是由控制臺生成
輸入賬號和密碼即可登錄成功,并跳轉到一開始輸入要訪問的頁面
在url后面輸入logout即可退出登錄,logout接口也是Security自己內部的接口。
后面真正使用的時候會自己重寫配置,配置自己寫的登錄頁面,以及做一些用戶權限處理。
(2)自定義表單登錄
文件名:springboot_security2
可以看到如果配置了SpringSecurity,Security會有自己的登錄頁面,并且會攔截任何頁面,Security會有自己的內部接口login和logout。當然,很多時候是不會用它自己內部的登陸頁面,更多的是用自己自定義的登錄頁面,用自定義的表單,只需要自己做一下配置即可。
首先自己新建一個配置類,并且繼承WebSecurityConfigurerAdapter,這是官方要求的,官方說明如下:
/*** Provides a convenient base class for creating a {@link WebSecurityConfigurer}* instance. The implementation allows customization by overriding methods.** <p>* Will automatically apply the result of looking up* {@link AbstractHttpConfigurer} from {@link SpringFactoriesLoader} to allow* developers to extend the defaults.* To do this, you must create a class that extends AbstractHttpConfigurer and then create a file in the classpath at "META-INF/spring.factories" that looks something like:* </p>* <pre>* org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer* </pre>* If you have multiple classes that should be added you can use "," to separate the values. For example:** <pre>* org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer, sample.OtherThatExtendsAbstractHttpConfigurer* </pre>**/意思是說 WebSecurityConfigurerAdapter 提供了一種便利的方式去創建 WebSecurityConfigurer的實例,只需要重寫 WebSecurityConfigurerAdapter 的方法,即可配置攔截什么URL、設置什么權限等安全控制。創建配置類
package com.zsc.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.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Objects;/*** 沒有添加改配置,頁面會強制跳轉到springsecurity自己的登錄頁面* 參考鏈接:https://www.cnblogs.com/dw3306/p/12751373.html*/ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {// 指定密碼的加密方式,不然定義認證規則那里會報錯@Beanpublic PasswordEncoder passwordEncoder() {return new PasswordEncoder() {@Overridepublic String encode(CharSequence charSequence) {return charSequence.toString();}@Overridepublic boolean matches(CharSequence charSequence, String s) {return Objects.equals(charSequence.toString(), s);}};}//配置忽略掉的 URL 地址,一般用于js,css,圖片等靜態資源@Overridepublic void configure(WebSecurity web) throws Exception {//web.ignoring() 用來配置忽略掉的 URL 地址,一般用于靜態文件web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");}// (認證)配置用戶及其對應的角色@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//數據在內存中定義,一般要去數據庫取,jdbc中去拿,/*** 懶羊羊,灰太狼,喜羊羊,小灰灰分別具有vip0,vip1,vip2,vip3的權限* root則同時又vip0到vip3的所有權限*///Spring security 5.0中新增了多種加密方式,也改變了密碼的格式。//要想我們的項目還能夠正常登陸,需要修改一下configure中的代碼。我們要將前端傳過來的密碼進行某種方式加密//spring security 官方推薦的是使用bcrypt加密方式。auth.inMemoryAuthentication().withUser("懶羊羊").password("123").roles("vip0").and().withUser("灰太狼").password("123").roles("vip1").and().withUser("喜羊羊").password("123").roles("vip2").and().withUser("小灰灰").password("123").roles("vip3").and().withUser("root").password("123").roles("vip1","vip2","vip3");}// (授權)配置 URL 訪問權限,對應用戶的權限@Overrideprotected void configure(HttpSecurity http) throws Exception {http.headers().frameOptions().disable();//開啟運行iframe嵌套頁面//任何請求都必須經過身份驗證http.authorizeRequests().anyRequest().authenticated();//任何請求都必須經過身份驗證//開啟表單驗證http.formLogin().and().formLogin()//開啟表單驗證.loginPage("/toLogin")//跳轉到自定義的登錄頁面.usernameParameter("name")//自定義表單的用戶名的name,默認為username.passwordParameter("pwd")//自定義表單的密碼的name,默認為password.loginProcessingUrl("/doLogin")//表單請求的地址,一般與form的action屬性一致,注意:不用自己寫doLogin接口,只要與form的action屬性一致即可.successForwardUrl("/index")//登錄成功后跳轉的頁面(重定向).failureForwardUrl("/toLogin")//登錄失敗后跳轉的頁面(重定向).and().logout()//開啟注銷功能.logoutSuccessUrl("/toLogin")//注銷后跳轉到哪一個頁面.logoutUrl("/logout") // 配置注銷登錄請求URL為"/logout"(默認也就是 /logout).clearAuthentication(true) // 清除身份認證信息.invalidateHttpSession(true) //使Http會話無效.permitAll() // 允許訪問登錄表單、登錄接口.and().csrf().disable(); // 關閉csrf} }這里配置了幾個用戶懶羊羊,灰太狼,喜羊羊,小灰灰等用于登錄,一般這些用戶要從數據庫獲取,另外這里給他們設置了對應的權限,vip0到vip3的權限,都是自己自定義的權限,主要是為了下一節做授權操作(這里還沒做授權操作)
除此之外,這里有一個自己之前一直搞錯的重點如下
運行并訪問主頁index,會被攔截并且跳到自定義表單
隨便輸入自己上面定義好的用戶,跳轉到首頁,并且所有的頁面都可以訪問,vip0到vip3對應所有頁面均可以訪問
注銷登錄(退出)
以上就完成了自定義表單的登錄與注銷,下面開始做用戶授權。
(3)自定義表單用戶授權
文件名:springboot_security2
用戶授權簡單理解就是什么用戶可以訪問什么頁面,不同的用戶可以訪問不同的頁面,上一節已經給不同的用戶設置了的權限,下面給不同用戶做授權,配置類如下
package com.zsc.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.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.PasswordEncoder; import java.util.Objects;/*** 沒有添加改配置,頁面會強制跳轉到springsecurity自己的登錄頁面* 參考鏈接:https://www.cnblogs.com/dw3306/p/12751373.html*/ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {// 指定密碼的加密方式,不然定義認證規則那里會報錯@Beanpublic PasswordEncoder passwordEncoder() {return new PasswordEncoder() {@Overridepublic String encode(CharSequence charSequence) {return charSequence.toString();}@Overridepublic boolean matches(CharSequence charSequence, String s) {return Objects.equals(charSequence.toString(), s);}};}//配置忽略掉的 URL 地址,一般用于js,css,圖片等靜態資源@Overridepublic void configure(WebSecurity web) throws Exception {//web.ignoring() 用來配置忽略掉的 URL 地址,一般用于靜態文件web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");}// (認證)配置用戶及其對應的角色@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//數據在內存中定義,一般要去數據庫取,jdbc中去拿,/*** 懶羊羊,灰太狼,喜羊羊,小灰灰分別具有vip0,vip1,vip2,vip3的權限* root則同時又vip0到vip3的所有權限*///Spring security 5.0中新增了多種加密方式,也改變了密碼的格式。//要想我們的項目還能夠正常登陸,需要修改一下configure中的代碼。我們要將前端傳過來的密碼進行某種方式加密//spring security 官方推薦的是使用bcrypt加密方式。auth.inMemoryAuthentication().withUser("懶羊羊").password("123").roles("vip0").and().withUser("灰太狼").password("123").roles("vip1").and().withUser("喜羊羊").password("123").roles("vip2").and().withUser("小灰灰").password("123").roles("vip3").and().withUser("root").password("123").roles("vip1","vip2","vip3");}// (授權)配置 URL 訪問權限,對應用戶的權限@Overrideprotected void configure(HttpSecurity http) throws Exception {http.headers().frameOptions().disable();//開啟運行iframe嵌套頁面//任何請求都必須經過身份驗證http.authorizeRequests() // .anyRequest().authenticated()//任何請求都必須經過身份驗證.antMatchers("/vip/vip0/**").hasRole("vip0")//vip1具有的權限:只有vip1用戶才可以訪問包含url路徑"/vip/vip0/**".antMatchers("/vip/vip1/**").hasRole("vip1")//vip1具有的權限:只有vip1用戶才可以訪問包含url路徑"/vip/vip1/**".antMatchers("/vip/vip2/**").hasRole("vip2")//vip2具有的權限:只有vip2用戶才可以訪問url路徑"/vip/vip2/**".antMatchers("/vip/vip3/**").hasRole("vip3");//vip3具有的權限:只有vip3用戶才可以訪問url路徑"/vip/vip3/**"//開啟表單驗證http.formLogin().and().formLogin()//開啟表單驗證.loginPage("/toLogin")//跳轉到自定義的登錄頁面.usernameParameter("name")//自定義表單的用戶名的name,默認為username.passwordParameter("pwd")//自定義表單的密碼的name,默認為password.loginProcessingUrl("/doLogin")//表單請求的地址,一般與form的action屬性一致,注意:不用自己寫doLogin接口,只要與form的action屬性一致即可.successForwardUrl("/index")//登錄成功后跳轉的頁面(重定向).failureForwardUrl("/toLogin")//登錄失敗后跳轉的頁面(重定向).and().logout()//開啟注銷功能.logoutSuccessUrl("/toLogin")//注銷后跳轉到哪一個頁面.logoutUrl("/logout") // 配置注銷登錄請求URL為"/logout"(默認也就是 /logout).clearAuthentication(true) // 清除身份認證信息.invalidateHttpSession(true) //使Http會話無效.permitAll() // 允許訪問登錄表單、登錄接口.and().csrf().disable(); // 關閉csrf} }主要增加了用戶權限的配置,具體如下圖
不需要登錄直接進首頁,因為沒對首頁index做限制,但是點擊vip0到vip3的任意頁面都會被攔截,并且自動跳轉到登錄頁面進行登錄
點擊vip0下對應的頁面、
灰太狼賬號登錄后,可以訪問vip1權限的頁面,其余權限的頁面不可以訪問,如果訪問會拋出異常,因為沒做相應異常的處理,所以異常會顯示在頁面
登錄具有不同權限的用戶,可以訪問對應權限的頁面,以上就是用戶授權的最基本的用戶。
(4)基于數據庫的自定義表單認證
文件名:springboot_security3
首先是數據庫的創建,實際上登錄認證至少要有5張表,用戶表、角色表、權限表,角色權限中間表和用戶角色中間表,這里按照上面的例子,將權限直接寫死在自定義的SecurityConfig配置類中。
所以這里的登錄認證只涉及到三張表:用戶表(user)、角色表(role)、用戶角色中間表(user_role)。
具體數據表截圖
注意:這里的role跟上面的例子相比多加了ROLE_前綴。這是因為之前的role都是通過springsecurity的api賦值過去的,他會自行幫我們加上這個前綴。但是現在我們使用的是自己的數據庫里面讀取出來的權限,然后封裝到自己的實體類中。所以這時候需要我們自己手動添加這個ROLE_前綴。經過測試如果不加ROLE_前綴的話,可以做數據庫的認證,但無法做授權
創建實體類User,注意User需要實現UserDetails接口,并且實現該接口下的7個接口
package com.zsc.po;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;import java.util.ArrayList; import java.util.Collection; import java.util.List;@Data @NoArgsConstructor @AllArgsConstructor public class User implements UserDetails {private Integer id;private String userName;//用戶名private String passWord;//密碼private List<Role> roles;//該用戶對應的角色/*** 返回用戶的權限集合。* @return*/@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<SimpleGrantedAuthority> authorities = new ArrayList<>();for (Role role : roles) {authorities.add(new SimpleGrantedAuthority(role.getName()));}return authorities;}/*** 返回賬號的密碼* @return*/@Overridepublic String getPassword() {return passWord;}/*** 返回賬號的用戶名* @return*/@Overridepublic String getUsername() {return userName;}/*** 賬號是否失效,true:賬號有效,false賬號失效。* @return*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 賬號是否被鎖,true:賬號沒被鎖,可用;false:賬號被鎖,不可用* @return*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 賬號認證是否過期,true:沒過期,可用;false:過期,不可用* @return*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 賬號是否可用,true:可用,false:不可用* @return*/@Overridepublic boolean isEnabled() {return true;}}角色表實體類Role,這個類不用實現上述接口
package com.zsc.po;import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor;@Data @NoArgsConstructor @AllArgsConstructor public class Role {private Integer id;private String name;//角色的名字 }接下來做數據庫的查詢,創建持久層接口(UserMapper和RoleMapper)
package com.zsc.mapper;import com.zsc.po.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List;@Mapper @Repository public interface UserMapper {/*** 通過用戶名獲取用戶信息** @param username 用戶名* @return User 用戶信息*/List<User> getUserByUsername(String username);} package com.zsc.mapper;import com.zsc.po.Role; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List;@Mapper @Repository public interface RoleMapper {/*** 通過用戶id獲取用戶角色集合** @param userId 用戶id* @return List<Role> 角色集合*/List<Role> getRolesByUserId(Integer userId); }持久層接口對應配置文件(UserMapper.xml和RoleMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zsc.mapper.UserMapper"><resultMap id = "userMap" type = "com.zsc.po.User"><id column="id" property="id"></id><result column="username" property="userName"></result><result column="password" property="passWord"></result><collection property="roles" ofType="com.zsc.po.Role"><id property="id" column="rid"></id><result column="rname" property="name"></result></collection></resultMap><select id="getUserByUsername" resultMap="userMap">select * from user where username = #{username}</select></mapper> <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.zsc.mapper.RoleMapper"><resultMap id = "roleMap" type = "com.zsc.po.Role"><id column="id" property="id"></id><result column="name" property="name"></result></resultMap><select id="getRolesByUserId" resultMap="roleMap">select * from role r,user_role ur where r.id = ur.rid and ur.uid = #{userId}</select></mapper>創建服務層(UserService),該層獲取數據庫數據,將數據交給SpringSecurity做用戶的認證與授權,為此,需要實現接口UserDetailsService,并且實現該接口下的loadUserByUsername方法,該方法獲取數據庫數據
package com.zsc.service;import com.zsc.mapper.RoleMapper; import com.zsc.mapper.UserMapper; import com.zsc.po.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service;import java.util.List;@Service public class UserService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate RoleMapper roleMapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {List<User> users = userMapper.getUserByUsername(s);if (null == users || users.size() ==0) {throw new UsernameNotFoundException("該用戶不存在!");}else{users.get(0).setRoles(roleMapper.getRolesByUserId(users.get(0).getId()));System.out.println("***********************"+users.get(0).getAuthorities());return users.get(0);}} }最后修改一下自定義的SecurityConfig配置類即可
package com.zsc.config;import com.zsc.service.UserService; 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.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;/*** 沒有添加改配置,頁面會強制跳轉到springsecurity自己的登錄頁面* 參考鏈接:https://www.cnblogs.com/dw3306/p/12751373.html*/ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserService userService;//配置忽略掉的 URL 地址,一般用于js,css,圖片等靜態資源@Overridepublic void configure(WebSecurity web) throws Exception {//web.ignoring() 用來配置忽略掉的 URL 地址,一般用于靜態文件web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");}// (認證)配置用戶及其對應的角色@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());}// (授權)配置 URL 訪問權限,對應用戶的權限@Overrideprotected void configure(HttpSecurity http) throws Exception {http.headers().frameOptions().disable();//開啟運行iframe嵌套頁面//身份驗證http.authorizeRequests().anyRequest().authenticated();//任何請求都必須經過身份驗證//開啟表單驗證http.formLogin().and().formLogin()//開啟表單驗證.loginPage("/toLogin")//跳轉到自定義的登錄頁面.usernameParameter("name")//自定義表單的用戶名的name,默認為username.passwordParameter("pwd")//自定義表單的密碼的name,默認為password.loginProcessingUrl("/doLogin")//表單請求的地址,一般與form的action屬性一致,注意:不用自己寫doLogin接口,只要與form的action屬性一致即可.successForwardUrl("/index")//登錄成功后跳轉的頁面(重定向).failureForwardUrl("/toLogin")//登錄失敗后跳轉的頁面(重定向).and().logout()//開啟注銷功能.logoutSuccessUrl("/toLogin")//注銷后跳轉到哪一個頁面.logoutUrl("/logout") // 配置注銷登錄請求URL為"/logout"(默認也就是 /logout).clearAuthentication(true) // 清除身份認證信息.invalidateHttpSession(true) //使Http會話無效.permitAll() // 允許訪問登錄表單、登錄接口.and().csrf().disable(); // 關閉csrf} }大功告成,運行測試,訪問首頁index,被攔截重定向到登錄頁面進行用戶認證,即登錄認證
隨便輸入數據庫中存在的任一用戶,密碼是123,數據庫存儲的密碼是經過加密的,登錄成功,因為沒做任何用戶的授權,所以可訪問任意頁面
這里再重點記錄一下,關于數據庫存儲用戶權限必須要有ROLE_前綴,但在SecurityConfig中設置權限時可以不用加ROLE_前綴
這其實是授權部分的內容,下一節就是數據庫用戶的授權操作
(5)基于數據庫的自定義表單授權
文件名:springboot_security3
關于授權部分,上一節其實已經有講到一點了,實現起來頁簡單,基本的配置跟上一節一樣保持不變,唯一變的就是將攔截所有請求改為對應權限的攔截,具體只要修改SecurityConfig配置類的部分內容即可
這樣就完成了數據庫用戶的授權,測試運行,訪問主頁,跟之前一樣任何人可以登錄,因為沒有對主頁做限制,但是訪問主頁里面的vip0到vip3任意頁面都需要相應的權限,如果沒有會跳到登錄頁面進行登錄認證
(6)獲取當前登錄用戶的信息
登錄授權后,很多時候都需要用戶登錄的用戶的基本信息,比如判斷用戶是在線,獲取當前登錄用戶的關聯的信息等。都需要用到當前登錄用戶的信息,下面是獲取當前登錄用戶信息的一種方法,主要是在控制層獲取。
@GetMapping("/isLogin")@ResponseBodypublic Object getUserInfo(){if(!SecurityContextHolder.getContext().getAuthentication().getName().equals("anonymousUser")){//已登錄Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//獲取用戶信息//獲取登錄的用戶名String username = authentication.getName();System.out.println("username : "+username);//用戶的所有權限Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();System.out.println("authorities : "+authorities);/*** 如果要獲取更詳細的用戶信息可以采用下面這種方法*///用戶的基本信息User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();System.out.println("user : "+user);//用戶的idInteger userId = user.getId();System.out.println("userId: "+userId);//User其余信息可以用這種方式獲取//List<Role> roles = user.getRoles();//String password = user.getPassword();//String username1 = user.getUsername();return "已登錄賬號:"+username;}else{//未登錄return "請先登錄";}在沒有登錄的狀態下訪問上面的接口
登錄灰太狼賬號之后,查看頁面,同時觀察控制臺輸出
以上就是獲取當前登錄賬號的個人信息的全部內容。
(7)前后端分離下的基于表單數據的登錄驗證
sql
這里新建了一個項目springboot_security4,采用springboot+mybatis-plus+security技術棧,實體類如下
@Data @TableName("user") public class User implements Serializable, UserDetails {private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)private Integer id;private String username;private String nickname;private String password;private Boolean enabled;private String email;private String userface;@TableField(value = "create_time")//字段名與數據庫字段名不一致時采用該形式進行映射private Date createTime;@TableField(value = "update_time")//字段名與數據庫字段名不一致時采用該形式進行映射private Date updateTime;/*** 賬號是否失效,true:賬號有效,false賬號失效。* @return*/@Override@JsonIgnore//在json序列化時將pojo中的一些屬性忽略掉,標記在屬性或者方法上,返回的json數據即不包含該屬性。public boolean isAccountNonExpired() {return true;}/*** 號是否被鎖,true:賬號沒被鎖,可用;false:賬號被鎖,不可用* @return*/@Override@JsonIgnore//在json序列化時將pojo中的一些屬性忽略掉,標記在屬性或者方法上,返回的json數據即不包含該屬性。public boolean isAccountNonLocked() {return true;}/*** 號認證是否過期,true:沒過期,可用;false:過期,不可用* @return*/@Override@JsonIgnore//在json序列化時將pojo中的一些屬性忽略掉,標記在屬性或者方法上,返回的json數據即不包含該屬性。public boolean isCredentialsNonExpired() {return true;}/*** 賬號是否可用,true:可用,false:不可用* @return*/@Override@JsonIgnore//在json序列化時將pojo中的一些屬性忽略掉,標記在屬性或者方法上,返回的json數據即不包含該屬性。public boolean isEnabled() {return this.enabled;}//如果沒有設置權限的話,這里直接返回null即可@Override@JsonIgnore//在json序列化時將pojo中的一些屬性忽略掉,標記在屬性或者方法上,返回的json數據即不包含該屬性。public Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Override@JsonIgnore//在json序列化時將pojo中的一些屬性忽略掉,標記在屬性或者方法上,返回的json數據即不包含該屬性。public String getPassword() {return this.password;}@Override@JsonIgnore//在json序列化時將pojo中的一些屬性忽略掉,標記在屬性或者方法上,返回的json數據即不包含該屬性。public String getUsername() {return this.username;} }持久層mapper
@Mapper public interface UserMapper extends BaseMapper<User> { }user服務層,需要繼承UserDetailsService類,并且實現里面的loadUserByUsername方法
接口
實現類
@Service @Transactional public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService, UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {Map<String,Object> map = new HashMap<>();map.put("username",s);List<User> users = userMapper.selectByMap(map);users.forEach(System.out::println);//注意這里必須保證數據庫的用戶名唯一if(users.size() == 0 || users.get(0) == null){System.out.println("用戶為null");//避免返回null,這里返回一個不含有任何值的User對象,在后期的密碼比對過程中一樣會驗證失敗return new User();}return users.get(0);}//根據id查詢用戶public User selectById(Integer id){return userMapper.selectById(id);} }接下來是配置類WebSecurityConfig ,這個是核心
/*** @ClassName : WebSecurityConfig* @Description : security配置類* @Author : CJH* @Date: 2020-08-31 16:18*/ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserServiceImpl userService;/*** 用戶認證** @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());//使用BCryptPasswordEncoder進行加密}/*** 用戶登錄判斷及響應** @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated()//所有的路徑都是登錄后即可訪問.and().formLogin().loginPage("/doLogin")//如果是未登錄的會自動跳到該接口(根據需要自己實現,返回頁面或返回json).successHandler(new AuthenticationSuccessHandler() {@Overridepublic void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=utf-8");PrintWriter out = httpServletResponse.getWriter();out.write("{\"status\":\"success\",\"msg\":\"登錄成功\"}");out.flush();out.close();}}).failureHandler(new AuthenticationFailureHandler() {@Overridepublic void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {httpServletResponse.setContentType("application/json;charset=utf-8");PrintWriter out = httpServletResponse.getWriter();out.write("{\"status\":\"error\",\"msg\":\"登錄失敗!!\"}");out.flush();out.close();}}).loginProcessingUrl("/login")//發起登錄請求的接口.usernameParameter("username")//設置登錄請求接口的參數(用戶名).passwordParameter("password")//設置登錄請求接口的參數(密碼).permitAll().and().logout()//注銷登錄接口(/logout).logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler() {//注銷成功時的處理@Overridepublic void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();out.write("注銷成功");out.flush();out.close();}}).permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(getAccessDeniedHandler());//用戶權限不足時的處理}/*** 用戶權限不足時的處理** @return*/@BeanAccessDeniedHandler getAccessDeniedHandler() {return new AuthenticationAccessDeniedHandler();} }配置類中對應的用戶權限不足時的處理器
/*** @ClassName : AuthenticationAccessDeniedHandler* @Description : security用戶權限不足時的處理* @Author : CJH* @Date: 2020-08-31 16:59*/ public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest req, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {resp.setStatus(HttpServletResponse.SC_FORBIDDEN);resp.setCharacterEncoding("UTF-8");PrintWriter out = resp.getWriter();out.write("權限不足,請聯系管理員!");out.flush();out.close();} }控制器LoginController里面的doLogin接口對應上面的WebSecurityConfig里面配置的doLogin
@RestController public class LoginController {/*** 如果自動跳轉到這個頁面,說明用戶未登錄,返回相應的提示即可* 如果要支持表單登錄,可以在這個方法中判斷請求的類型,進而決定返回JSON還是HTML頁面* @return*/@RequestMapping("/doLogin")public Map<String,String> doLogin(){Map<String,String> map = new HashMap<>();map.put("msg","尚未登錄,請先登錄");map.put("code","10001");return map;} } /*** <p>* 前端控制器* </p>** @author 最強菜鳥* @since 2020-08-31*/ @RestController @RequestMapping("/user") public class UserController {@GetMapping("/hello")public String hello(){return "hello world";}@GetMapping("/test")public void test(){System.out.println("this is test");} }上面所做的用戶驗證都是基于表單數據的驗證,只有用表單數據(非json格式)發生post請求才有效,下面開始測試
注意:如果用json格式發請求會驗證失敗,如下
注銷接口
(8)前后端分離下的基于json數據的登錄驗證
基本的配置跟上面的(7)一樣,不同的是WebSecurityConfig配置類有部分修改,以及增加了一個過濾器
/*** @ClassName : WebSecurityConfig* @Description : security配置類* @Author : CJH* @Date: 2020-08-31 16:18*/ @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserServiceImpl userService;/*** 用戶認證** @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());//使用BCryptPasswordEncoder進行加密}/*** 基于json用戶登錄判斷及響應** @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated()//所有的路徑都是登錄后即可訪問.and().formLogin().loginPage("/doLogin")//如果是未登錄的會自動跳到該接口(根據需要自己實現,返回頁面或返回json).loginProcessingUrl("/login")//發起登錄請求的接口.usernameParameter("username")//設置登錄請求接口的參數(用戶名).passwordParameter("password")//設置登錄請求接口的參數(密碼).permitAll().and().logout()//注銷登錄接口(/logout).logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler() {//注銷成功時的處理@Overridepublic void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();out.write("注銷成功");out.flush();out.close();}}).permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(getAccessDeniedHandler());//用戶權限不足時的處理http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);}/*** 自定義security過濾器,以實現用post發起登錄請求時,參數用json傳遞* @return* @throws Exception*/@BeanCustomAuthenticationFilter customAuthenticationFilter() throws Exception {CustomAuthenticationFilter filter = new CustomAuthenticationFilter();/**登錄成功**/filter.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {@Overridepublic void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException {resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();out.write("{\"status\":\"success\",\"msg\":\"登錄成功\"}");out.flush();out.close();}});/**登錄失敗**/filter.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {@Overridepublic void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {resp.setContentType("application/json;charset=utf-8");PrintWriter out = resp.getWriter();out.write("{\"status\":\"error\",\"msg\":\"登錄失敗!!\"}");out.flush();out.close();}});filter.setAuthenticationManager(authenticationManagerBean());return filter;}/*** 用戶權限不足時的處理** @return*/@BeanAccessDeniedHandler getAccessDeniedHandler() {return new AuthenticationAccessDeniedHandler();}}過濾器
/*** @ClassName : CustomAuthenticationFilter* @Description : 自定義security過濾器,以實現用post發起登錄請求時,參數用json傳遞* @Author : CJH* @Date: 2020-08-31 22:18*/ public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//攔截請求頭,可以自定義配置,如果想用表單數據也可同時用json也可以用MediaType類型配置,這里只配置了jsonif (request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE) || request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {ObjectMapper mapper = new ObjectMapper();UsernamePasswordAuthenticationToken authRequest = null;try (InputStream is = request.getInputStream()) {Map<String, String> authenticationBean = mapper.readValue(is, Map.class);authRequest = new UsernamePasswordAuthenticationToken(authenticationBean.get("username"),authenticationBean.get("password"));} catch (IOException e) {e.printStackTrace();authRequest = new UsernamePasswordAuthenticationToken("", "");} finally {setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}} else {return super.attemptAuthentication(request, response);}} }其余的跟上面(7)一樣即可,下面開始測試
訪問未登錄時的接口
訪問登錄接口
注銷接口
注意,使用過濾器進行攔截可以自己根據需求配置表單數據接受或json數據接收或兩者都可以接受等
三、SpringSecurity核心組件
這里列舉出以下核心組件:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager,下面開始對這些核心組件的詳細介紹。
(1)Authentication
authentication 直譯過來是“認證”的意思,在Spring Security 中Authentication用來表示當前用戶是誰,一般來講你可以理解為authentication就是一組用戶名密碼信息。Authentication也是一個接口,其定義如下:
我們獲取當前登錄用戶信息就是用的這個接口,如果有看上面入門系列的獲取用戶那一段,就可以知道獲取用戶信息也就是用的Authentication
(2)SecurityContext
安全上下文,用戶通過Spring Security 的校驗之后,驗證信息存儲在SecurityContext中,SecurityContext的接口定義如下:
可以看到這里只定義了兩個方法,主要都是用來獲取或修改認證信息(Authentication)的,Authentication是用來存儲著認證用戶的信息,所以這個接口可以間接獲取到用戶的認證信息。還是以上面的入門系列的獲取用戶那一段來進行解析
(3)SecurityContextHolder
SecurityContextHolder看名字就知道跟SecurityContext實例相關的。在典型的web應用程序中,用戶登錄一次,然后由其會話ID標識。服務器緩存持續時間會話的主體信息。
但是在Spring Security中,在請求之間存儲SecurityContext的責任落在SecurityContextPersistenceFilter上,默認情況下,該上下文將上下文存儲為HTTP請求之間的HttpSession屬性。它會為每個請求恢復上下文SecurityContextHolder,并且最重要的是,在請求完成時清除SecurityContextHolder
說到SecurityContextHolder就必須要說到一個過濾器,SecurityContextPersistenceFilter
SecurityContextPersistenceFilter:這個Filter是整個攔截過程的入口和出口 ,在請求開始時從配置好的SecurityContextRepository中獲取SecurityContext,然后把它設置給 SecurityContextHolder。在請求完成后將 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同時清除 SecurityContextHolder 所持有的SecurityContext ;
進入源碼查看
同樣的可以參考上面的入門系列的獲取用戶那一段來進行解析
可以看整個登錄用戶的信息獲取流程就十分清晰了。
(4)UserDetails
這個看著有點熟悉,在上面入門系列的基于數據庫認證中,實體類User就必須實現這個接口
UserDetails存儲的就是用戶信息,其定義如下:
(5)UserDetailsService
在上面入門系列的基于數據庫認證中,用戶類必須要實現UserDetails接口,還需要實現UserDetailsService接口,與實體類User(實現了UserDetails接口)
相對應的還要在應用層中實現UserDetailsService接口
之前在入門系列中的基于數據庫的認證中沒有去深究其原理,到現在基本就可以知道其認證的流程,包括數據庫用戶的獲取。
通常在spring security應用中,我們會自定義一個UserDetailsService來實現UserDetailsService接口,并實現其loadUserByUsername(final String login);方法。我們在實現loadUserByUsername方法的時候,就可以通過查詢數據庫(或者是緩存、或者是其他的存儲形式)來獲取用戶信息,然后組裝成一個UserDetails,(通常是一個org.springframework.security.core.userdetails.User,它繼承自UserDetails) 并返回。
在實現loadUserByUsername方法的時候,如果我們通過查庫沒有查到相關記錄,需要拋出一個異常來告訴spring security來“善后”。這個異常是org.springframework.security.core.userdetails.UsernameNotFoundException。
關于其源碼估計能猜到,肯定有一個loadUserByUsername方法等著我們去實現
通常基于數據庫的認證,就要從數據庫中獲取要認證的用戶信息,從數據庫中獲取用戶信息就是通過服務處實現UserDetailsService接口,并重寫其loadUserByUsername方法,這個方法用來獲取數據庫用戶信息。
(6)AuthenticationManager
AuthenticationManager 是一個接口,它只有一個方法,接收參數為Authentication,其定義如下:
public interface AuthenticationManager {Authentication authenticate(Authentication authentication)throws AuthenticationException; }AuthenticationManager 的作用就是校驗Authentication,如果驗證失敗會拋出AuthenticationException異常。AuthenticationException是一個抽象類,因此代碼邏輯并不能實例化一個AuthenticationException異常并拋出,實際上拋出的異常通常是其實現類,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能會比較常見,即密碼錯誤的時候。
四、部分源碼解析
(1)用戶認證流程
認證大致流程
關于SpringSecurity的用戶認證流程,個人覺得是十分有必要了解的,盡管框架已經封裝好,只要按照它定好的規則來做就好了。在查詢了大量的博客和網上的大量視頻講解后,發現其實講的基本都一樣,當然有些自己還沒搞懂,個人覺得任何東西如果自己不動手試一下是不可能真正懂的。
首先是大致流程,之前我想的是它可能是通過過濾器的方式去實現的攔截并且重定向到登錄頁面的方式進行認證的,在查閱了大量的資料發現,實現的方式確實是過濾器的方式,只不過它有很多個過濾器,形成一條過濾鏈,只有通過這條過濾鏈后才可以訪問API
具體的驗證流程可以用下圖來表示
下面介紹過濾器鏈中主要的幾個過濾器及其作用:
SecurityContextPersistenceFilter:這個Filter是整個攔截過程的入口和出口 ,在請求開始時從配置好的SecurityContextRepository中獲取SecurityContext,然后把它設置給 SecurityContextHolder。在請求完成后將 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同時清除 SecurityContextHolder 所持有的SecurityContext ;
UsernamePasswordAuthenticationFilter:用于處理來自表單提交的認證。該表單必須提供對應的用戶名和密碼,其內部還有登錄成功或失敗后進行處理的AuthenticationSuccessHandler和 AuthenticationFailureHandler,這兩個接口可以字配置,在上面入門系列的自定義的SecurityConfig配置類中可以自己配置
FilterSecuritylnterceptor:是用于保護web資源的,使用AccessDecisionManager對當前用戶進行授權訪問
ExceptionTranslationFilter:捕獲來自FilterChain所有的異常并進行處理。但是它只會處理兩類異 常:Authentication Exception 和 AccessDeniedException ,其它的異常它會繼續拋出。
認證具體流程
這里推薦一篇個人覺得簡單易懂認證流程的博客:https://www.cnblogs.com/ymstars/p/10626786.html
具體的認證流程需要看源碼才能知道,這里引用一下之前看的視頻的一張認證的圖片,圖片如下
從這里看到請求進來會經過UsernamePasswordAuthenticationFilter 過濾器,所以先用全局搜索(CTRL+N)找到該過濾器,并且打上斷點,這里用的默認登錄頁面,密碼用的控制臺隨機生成的密碼
就像上面認證大致流程里面說的一樣,用戶身份的認證交給AuthenticationManager處理,AuthenticationManager又委托給DaoAuthenticationProvider 認證,所以在全局搜索找到DaoAuthenticationProvider并且打上斷點進行調試
注意這行代碼:
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
它是通過UserDetailsService來加載要驗證的用戶
獲取用戶名后,將用戶名傳給preAuthenticationChecks.check()方法驗證
進入preAuthenticationChecks.check(user);內部方法,可以看到有一些驗證,如賬號是否可用,是否被鎖等等,這些參數就是上面數據庫用戶認證User類要認證的參數
用戶賬號密碼的驗證則是由斷點下面的additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);負責驗證,進入該方法
總結:
(1)UsernamePasswordAuthenticationFilter獲取表單輸入的用戶名和密碼等請求信息,并封裝成Token
(2)AuthenticationManager負責將Token委托給DaoAuthenticationProvider進行認證
(3)DaoAuthenticationProvider通過UserDetailsService來加載要驗證的用戶
(4)最后先校驗用戶的賬號是否被鎖了等信息,再校驗用戶賬號和密碼
(5)校驗成功則可以訪問接口,失敗則拋出異常
以上就是用戶認證具體流程的全部內容,涉及到一些源碼解讀,有點累人,剛開始以為很復雜,但自己試著調試了一下,基本還是可以理解的。
(2)默認登錄用戶名與密碼配置
如果成功引入Security依賴,MVC Security安全管理功能就行會自動生效,默認的安全配置是在UserDetailsServiceAutoConfiguration和SecurityAutoConfiguration中實現的,其中SecurityAutoConfiguration會導入并且自動配置,SpringBootWebSecurityConfiguration用于啟動Web安全管理,UserDetailsServiceAutoConfiguration用于配置用戶信息。
關于Security內部配置的用戶名和密碼可以進入源碼查看它的配置,讀源碼真的是一種難受的事情。
總結
以上是生活随笔為你收集整理的菜鸟的spring security学习教程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 只有搞Java开发的才知道!javasp
- 下一篇: (一)spring Boot菜鸟教程-搭