Spring Security 上

zc發表於2021-05-06

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.執行測試

執行網址為:

http://localhost:8081/hello

這時候會發現,網址會自動變為:

http://localhost:8081/login

1

6.登入

能看到,在該頁面中有賬號密碼

預設賬號:user

預設密碼:

2

登入之後:

3

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

4

初始化為 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() 方法 可以返回 所有要進行載入的過濾器


總結:

  1. 配置過濾器 DelegatingFilterProxy
  2. 在其中進行初始化 initDelegate
  3. 在初始化中得到 FilterChainProxy 物件
  4. 在其中執行的就是 doFilterInternal() 方法,該方法返回的就是 所有要進行載入的過濾器

UserDetailsService 介面

UserDetailsService介面 : 查詢資料庫使用者名稱和密碼過程

步驟:

  1. 建立類繼承UsernamePasswordAuthenticationFilter,重寫三個方法: attemptAuthentication() 、successfulAuthentication()、unsuccessfulAuthentication()
  2. 如果成功呼叫successfulAuthentication(),反之呼叫unsuccessfulAuthentication()
  3. 建立類實現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. 通過配置檔案
  2. 通過配置類
  3. 自定義編寫實現類

1.通過配置檔案

spring.security.user.name=root
spring.security.user.password=root

這個時候再執行,會發現控制檯不會出現密碼,可以直接通過設定的賬號密碼登入

2.通過配置類

  1. 建立一個 SecurityConfig 配置類
  2. 重寫configure()方法,注意看清引數,不要選錯方法
  3. 很重要的一點:需要注入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.自定義編寫實現類

  1. 編寫userDetailsService實現類,返回User物件
  2. 建立一個 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 介面

5

在原始碼中可以看出,實際上 UserDetails 介面,返回的是一個 User 物件

6

而在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.連線資料庫完成使用者認證

(該方法是在第三種方法程式碼基礎上完成)

  1. 建立資料庫
  2. 整合Mybatis-Plus完成資料庫操作
  3. 配置JDBC資訊
  4. 建立實體類、Mapper介面
  5. 建立UserDetailsService類

建立資料庫

建立了一個 mybatis-plus 資料庫 ,其中建立了一個users表,記得建立後,加入資料

7

引入依賴

<!-- 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
}

這時候可以分別測試進入以下兩個路徑:

http://localhost:8081/hello

http://localhost:8081/index

會發現,第一個 hello 路徑 ,不會攔截了,可以直接進入頁面

第二個index,會進入自定義的登陸頁面,登陸成功後,才可以進入

基於角色或許可權的訪問控制

1.hasAuthority方法

如果當前的主體具有指定的許可權,則返回true,否則返回false

  1. 修改配置類
  2. 在 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);
    }

進行測試,路徑為:

http://localhost:8081/index

  • 如果許可權不通過 , 403 無許可權

8

  • 如果許可權通過 ,正常執行

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");
}

9

個人部落格為:
MoYu's Github Blog
MoYu's Gitee Blog

相關文章