Spring Security 上
Security-dome
1.創建項目
創建一個Spring Boot項目,不用加入什么依賴
2.導入依賴
<dependencies>
<!--啟動器變為 web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--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>
</dependency>
</dependencies>
3.創建控制層
@RestController
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello Security";
}
}
4.配置文件修改端口號
server.port=8081
5.運行測試
運行網址為:
這時候會發現,網址會自動變為:
6.登錄
能看到,在該頁面中有賬號
和密碼
默認賬號:user
默認密碼:
登錄之后:
Security 原理
Spring Security 本質是一個過濾器鏈
FilterSecurityInterceptor:是一個方法級的 權限過濾器
,基本位於過濾鏈的最底部
ExceptionTranslationFilter:是個異常過濾器
,用來處理在認證授權過程中拋出的異常
UsernamePasswordAuthenticationFilter:對 /login
的POST請求做攔截,校驗表單中用戶名,密碼
過濾器加載步驟
步驟流程
使用Spring Security配置過濾器 : DelegatingFilterProxy
源代碼如下
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized(this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = this.findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = this.initDelegate(wac);
}
this.delegate = delegateToUse;
}
}
this.invokeDelegate(delegateToUse, request, response, filterChain);
}
即為:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//進行判斷
//初始化
delegateToUse = this.initDelegate(wac);
//其余部分
}
然后我們查看 initDelegate:
初始化為 FilterChainProxy 對象
進入 FilterChainProxy:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (!clearContext) {
//滿足條件 運行該方法
this.doFilterInternal(request, response, chain);
} else {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
//不滿足 最終還是需要運行該方法
this.doFilterInternal(request, response, chain);
} catch (RequestRejectedException var9) {
this.requestRejectedHandler.handle((HttpServletRequest)request, (HttpServletResponse)response, var9);
} finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
}
可以看出,無論滿不滿足條件,最終都需要運行 doFilterInternal()方法
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/*
* 部分代碼。。。
*/
List<Filter> filters = this.getFilters((HttpServletRequest)firewallRequest);
/*
* 部分代碼。。。
*/
private List<Filter> getFilters(HttpServletRequest request) {
int count = 0;
Iterator var3 = this.filterChains.iterator();
SecurityFilterChain chain;
do {
if (!var3.hasNext()) {
return null;
}
chain = (SecurityFilterChain)var3.next();
if (logger.isTraceEnabled()) {
++count;
logger.trace(LogMessage.format("Trying to match request against %s (%d/%d)", chain, count, this.filterChains.size()));
}
} while(!chain.matches(request));
//返回所有過濾器
return chain.getFilters();
}
所以 doFilterInternal() 方法 可以返回 所有要進行加載的過濾器
總結:
- 配置過濾器 DelegatingFilterProxy
- 在其中進行初始化 initDelegate
- 在初始化中得到 FilterChainProxy 對象
- 在其中運行的就是 doFilterInternal() 方法,該方法返回的就是 所有要進行加載的過濾器
UserDetailsService 接口
UserDetailsService接口 : 查詢數據庫用戶名和密碼過程
步驟:
- 創建類繼承UsernamePasswordAuthenticationFilter,重寫三個方法: attemptAuthentication() 、successfulAuthentication()、unsuccessfulAuthentication()
- 如果成功調用successfulAuthentication(),反之調用unsuccessfulAuthentication()
- 創建類實現UserDetailService,編寫查詢數據過程,返回User對象,這個User對象是安全框架提供對象
PasswordEncoder接口
PasswordEncoder接口 : 數據加密接口,用於返回User對象里面密碼加密
加密方法:
BCryptPasswordEncoder是Spring Security官方推薦的密碼解析器,平時多使用這個解析器。
BCryptPasswordEncoder是對bcrypt強散列方法的具體實現。是基於Hash算法實現的單向加密。可以通過strength控制加密強度,默認10.
BCryptPasswordEncoder b = new BCryptPasswordEncoder();
String zc = b.encode("zc"); //加密成功
Web權限
在 Security-dome
中可以看到,如果想要進入頁面,還需要輸入賬號密碼
而對於登陸時候的賬號密碼可以進行自定義設置:
- 通過配置文件
- 通過配置類
- 自定義編寫實現類
1.通過配置文件
spring.security.user.name=root
spring.security.user.password=root
這個時候再運行,會發現控制台不會出現密碼
,可以直接通過設置的賬號密碼登錄
2.通過配置類
- 創建一個 SecurityConfig 配置類
- 重寫configure()方法,注意看清參數,不要選錯方法
- 很重要的一點:需要注入PasswordEncoder接口
如果不注入該接口,可能報 Encoded password does not look like BCrypt
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("root");
auth.inMemoryAuthentication()
.withUser("root") //賬號
.password(password) //加密的密碼
.roles("admin"); //權限
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
這時候,也可以直接使用你設置的賬號密碼登錄頁面
3.自定義編寫實現類
- 編寫userDetailsService實現類,返回User對象
- 創建一個 SecurityConfig 配置類
編寫一個UserDetailsService實現類
在其中需要重寫 loadUserByUsername() 方法,該方法用於登錄
@Service("userDetailsService")
public class MyuserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//返回的實際上是一個User對象,參數解析可以看下面
return new User("root",
new BCryptPasswordEncoder().encode("root"),auths);
}
}
UserDetailsService 解析
對於該實現類中重寫的 loadUserByUsername() 方法,返回的是 UserDetails 接口
在源代碼中可以看出,實際上 UserDetails 接口,返回的是一個 User 對象
而在User對象中,需要返回三個參數:
String、String、Collection;
賬號 、 密碼 、集合(權限等信息)
創建一個 SecurityConfig 配置類
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService; // 這里應和 @Service("userDetailsService") 中內容相同
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用該方法
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
這時候測試,也可以直接使用設置的賬號密碼登錄
之后,如果連接數據庫,一般都是用第三種方式
4.連接數據庫完成用戶認證
(該方法是在第三種方法代碼基礎上完成)
- 創建數據庫
- 整合Mybatis-Plus完成數據庫操作
- 配置JDBC信息
- 創建實體類、Mapper接口
- 創建UserDetailsService類
創建數據庫
創建了一個 mybatis-plus
數據庫 ,其中創建了一個users
表,記得創建后,加入數據
引入依賴
<!-- Mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
配置JDBC信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis-plus?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
創建實體類、Mapper接口
@Data // 引入了Lombok才可以使用
public class Users {
private Integer id;
private String username;
private String password;
}
@Repository
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}
創建UserDetailsService類
@Service("userDetailsService")
public class MyuserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//調用usersMapper方法
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
if (users == null){
//數據庫沒有用戶名,認證失敗
throw new UsernameNotFoundException("用戶名不存在");
}
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}
這時候就可以正常運行了
5.自定義登錄頁面
在上面代碼的基礎上完成該部分代碼
1.創建前端頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form method="post" action="/user/login">
用戶名:<input type="text" name="username">
<br>
密碼:<input type="text" name="password">
<br>
<input type="submit" value="login">
</form>
</body>
</html>
2.書寫Controller層代碼
@GetMapping("/index")
public String index(){
return "index";
}
3.在創建的配置類中重寫 configure(HttpSecurity http) 方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //自定義自己編寫的登錄頁面
.loginPage("/login.html") //設置登陸頁面
.loginProcessingUrl("/login") //成功登錄訪問路徑 , 該處路徑和from表單中的action路徑統一
.defaultSuccessUrl("/index").permitAll() //登錄成功之后跳轉路徑
.and().authorizeRequests()
.antMatchers("/","/hello","/login") //可以直接訪問的路徑,不需要認證
.permitAll()
.anyRequest().authenticated()
.and().csrf().disable(); //關閉csrf
}
這時候可以分別測試進入以下兩個路徑:
會發現,第一個 hello 路徑 ,不會攔截了,可以直接進入頁面
第二個index,會進入自定義的登陸頁面,登陸成功后,才可以進入
基於角色或權限的訪問控制
1.hasAuthority方法
如果當前的主體具有指定的權限,則返回true,否則返回false
- 修改配置類
- 在 UserDetailsService 實現類中添加權限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/index").permitAll()
.and().authorizeRequests()
.antMatchers("/","/hello","/user/login")
.permitAll()
//當前登錄用戶,只有具有admins權限才可以訪問這個路徑
.antMatchers("/index").hasAuthority("admins")
.anyRequest().authenticated()
.and().csrf().disable();
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
if (users == null){
throw new UsernameNotFoundException("用戶名不存在");
}
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admins"); //這里添加權限
return new User(users.getUsername(),
new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
進行測試,路徑為:
- 如果權限不通過 , 403 無權限
- 如果權限通過 ,正常運行
2.hasAnyAuthority方法
如果當前的主體有任何提供的角色(給定的作為一個逗號分隔的字符串列表)的話,返回 true
與 hasAuthority() 的區別是
hasAuthority() 參數唯一,只能滿足這一個權限才可以
而該方法,參數可以多個,滿足其中一個權限 即為通過
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/index").permitAll()
.and().authorizeRequests()
.antMatchers("/","/hello","/user/login")
.permitAll()
//當前登錄用戶,具有admins或者user權限才可以訪問這個路徑
.antMatchers("/index").hasAnyAuthority("admins","user")
.anyRequest().authenticated()
.and().csrf().disable();
}
3.hasRole方法
如果用戶具備給定角色就 允許訪問,否則出現 403
如果當前主體具有指定的角色,則返回 true該方法與 hasAuthority 方法,使用方法基本相同,區別就是 他需要在權限前加上
ROLE_
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/user/login")
.defaultSuccessUrl("/index").permitAll()
.and().authorizeRequests()
.antMatchers("/","/hello","/user/login")
.permitAll()
//當前登錄用戶,只有具有 ROLE_user 權限才可以訪問這個路徑
.antMatchers("/index").hasRole("user")
.anyRequest().authenticated()
.and().csrf().disable();
}
// UserDetailsService 實現類中添加權限
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_user");
這時候可以正常運行
4.hasAnyRole方法
表示用戶具備任何一個條件都可以訪問
該方法與 hasRole() 的區別 與1 2 兩種方法相同,大家可以自行測試
5.自定義403頁面
1.創建自定義403頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>沒有權限訪問!!!</h1>
</body>
</html>
2.修改配置類
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling().accessDeniedPage("/uuauth.html");
}