spring boot 學習筆記 (18)使用 Security 進行安全控制

Spring Security 介紹

Spring Security 是一個能夠基於 Spring 的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了 Spring IoC、DI(控制反轉 Inversion of Control,DI:Dependency Injection 依賴注入)和 AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減少了爲企業系統安全控制編寫大量重複代碼的工作。

Spring Security 的前身是 Acegi Security,它是一個基於 Spring AOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時在 Web 請求級和方法調用級處理身份確認和授權,爲基於 J2EE 企業應用軟件提供了全面安全服務。

Spring Boot 提供了集成 Spring Security 的組件包 spring-boot-starter-security,方便我們在 Spring Boot 項目中使用 Spring Security。

快速上手

先來做一個 Web 系統。

(1)添加依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

(2)配置文件

配置文件中將 Thymeleaf 的緩存先去掉。

spring.thymeleaf.cache=false

(3)創建頁面

在 resources/templates 目錄下創建頁面 index.html,在頁面簡單寫兩句話。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>index</title>
</head>
<body>
<h1>Hello!</h1>
<p>今天天氣很好,來一個純潔的微笑吧!</p>
</body>
</html>

(4)添加訪問入口

創建 SecurityController 類,在類中添加訪問頁面的入口:

@Controller
public class SecurityController {
    @RequestMapping("/")
    public String index() {
        return "index";
    }
}

添加完成後啓動項目,在瀏覽器中訪問地址:http://localhost:8080/,頁面展示結果如下:

Hello!

今天天氣很好,來一個純潔的微笑吧!

以上完成了一個特別簡單的 Web 頁面請求、展示信息。

(5)添加 Spring Security 依賴

現在在項目中添加 spring-boot-starter-security 的依賴包。

在 pom.xml 添加:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

添加完成後重啓項目,再次訪問地址:http://localhost:8080/,頁面會自動彈出了一個登錄框,如下:

說明 Spring Security 自動給所有訪問請求做了登錄保護,那麼這個登錄名和密碼是什麼呢,如果觀察比較仔細的話,會發現添加了 spring-boot-starter-security 依賴包重啓後的項目,在控制檯打印了一長串字符,如下:

2018-11-09 12:27:46.052  INFO 26240 --- [  restartedMain] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: d2c87183-ada6-4f26-b803-db2e60b01079

根據打印信息可以看出,這應該就是登錄的密碼了。

(6)進行分析

根據上面的打印信息,可以看出密碼是由 UserDetailsServiceAutoConfiguration 類打印出的,在 IEDA 連續按兩次 Shift 鍵,調出 IEDA 的類搜索框,輸出類名 UserDetailsServiceAutoConfiguration,查看它的源碼,具體打印代碼如下:

private String getOrDeducePassword(User user, PasswordEncoder encoder) {
    String password = user.getPassword();
    if (user.isPasswordGenerated()) {
        logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
    }

    return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password;
}

可以看出 User 就是我們需要的登錄用戶信息,打開 User 其源碼如下:

public static class User {
    private String name = "user";
    private String password = UUID.randomUUID().toString();
    private List<String> roles = new ArrayList<>();
    private boolean passwordGenerated = true;
    //省略一部分
    public void setPassword(String password) {
            if (!StringUtils.hasLength(password)) {
                return;
            }
            this.passwordGenerated = false;
            this.password = password;
        }
    //省略一部分
}

根據 User 類的信息發現,passwordGenerated 默認值爲 true ,當用戶被設置密碼時更新爲 false;也就是說如果沒有設置密碼 passwordGenerated 的值爲 true。 password 的值默認由 UUID 生產的一段隨機字符串,用戶名默認爲 user。綜上,用戶名 user 和控制檯打印的密碼便是系統默認的登錄和密碼,登錄成功後跳轉到首頁。

當然,如果想修改用戶名和密碼,可以在 application.properties 重新進行配置,例如:

# security
spring.security.user.name=admin
spring.security.user.password=admin

配置完成之後重啓項目,再次訪問 http://localhost:8080/,在跳轉出來的登錄頁面輸入上述用戶名和密碼,可以登錄成功。

登錄認證

上述是 Spring Security 最簡單的集成演示,在實際項目使用過程中,有的頁面不需要進行驗證,有的頁面需要進行驗證,賬戶密碼需要存儲到數據庫、角色權限相關聯等,其實這些 Spring Security 輕鬆可實現。

創建頁面 content.html,此頁面只有登錄用戶纔可查看,否則會跳轉到登錄頁面,登錄成功後才能訪問。可以自定義登錄頁面,當用戶未登錄時跳轉到自定義登錄頁面。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<body>
<h1>content</h1>
<p>我是登錄後纔可以看的頁面</p>
</body>
</html>

登錄頁面:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>login</title>
</head>
<body>
<div th:if="${param.error}">
    用戶名或密碼錯
</div>
<div th:if="${param.logout}">
    您已註銷成功
</div>
<form th:action="@{/login}" method="post">
    <div><label> 用戶名 : <input type="text" name="username"/> </label></div>
    <div><label> 密 碼 : <input type="password" name="password"/> </label></div>
    <div><input type="submit" value="登錄"/></div>
</form>
</body>
</html>

後臺添加訪問入口:

@RequestMapping("/content")
public String content() {
    return "content";
}

@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
    return "login";
}

進行配置 index.html 可以直接訪問,但 content.html 需要登錄後纔可查看,沒有登錄自動調整到 login.html,創建 SecurityConfig 類繼承於 WebSecurityConfigurerAdapter。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
            .formLogin()
                // .loginPage("/login")
                .permitAll()
                .and()
            .logout()
                .permitAll()
                .and()
            .csrf()
                .ignoringAntMatchers("/logout");

    }
}
  • @EnableWebSecurity,開啓 Spring Security 權限控制和認證功能。
  • antMatchers("/", "/home").permitAll(),配置不用登錄可以訪問的請求。
  • anyRequest().authenticated(),表示其他的請求都必須要有權限認證。
  • formLogin(),定製登錄信息。
  • loginPage("/login"),自定義登錄地址,若註釋掉則使用默認登錄頁面。
  • logout(),退出功能,Spring Security 自動監控了 /logout。
  • ignoringAntMatchers("/logout"),Spring Security 默認啓用了同源請求控制,在這裏選擇忽略退出請求的同源限制。

我們在 index 頁面添加一個挑戰 content 頁面的鏈接,同時在 content 頁面添加一個退出的鏈接。

index 頁面:

<p>點擊 <a th:href="@{/content}">這裏</a> 進入受限頁面</p>

content 頁面:

<form method="post" action="/logout">
    <button  type="submit">退出</button>
</form>

退出請求默認只支持 post 請求,修改完成之後重啓項目,訪問地址 http://localhost:8080/ 可以看到 index 頁面內容,點擊鏈接跳轉到 content 頁面時,會自動跳轉到 http://localhost:8080/login 登錄頁面,登錄成功後會自動跳轉到 http://localhost:8080/content,在 content 頁面單擊“退出”按鈕,會退出登錄狀態,跳轉到登錄頁面並提示已經退出。

登錄、退出、請求受限頁面,退出後跳轉到登錄頁面,是最常見的安全控制案例,是賬戶系統最基本的安全保障,接下來介紹如何通過角色來控制權限。

角色權限

也可以在 Java 代碼中配置用戶登錄名和密碼,在上面創建的 SecurityConfig 類中添加方法 configureGlobal()。

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
            .withUser("user")
                .password(new BCryptPasswordEncoder()
                    .encode("123456")).roles("USER");
}

在 Spring Boot 2.x 中配置密碼需要指明密碼的加密方式。當面配置文件和 SecurityConfig 類中都配置了用戶名和密碼時,會使用代碼中的用戶名和密碼。添加完上述代碼,重啓項目後,即可用最新的用戶名和密碼登錄系統。

在上述代碼中有這麼一段 roles("USER") 指明瞭用戶角色,角色就是 Spring Security 最重要的概念之一,往往通過用戶來控制權限比較繁瑣,在實際項目中,往往都是將用戶關聯到角色,給角色賦予一定的權限,通過角色來控制用戶訪問請求。

爲了演示不同角色擁有不同權限,再添加一個管理員 admin 和 角色 ADMIN。

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
            .withUser("user")
                 .password(new BCryptPasswordEncoder()
                    .encode("123456")).roles("USER")
            .and()
            .withUser("admin")
                 .password(new BCryptPasswordEncoder()
                    .encode("admin")).roles("ADMIN", "USER");
}

admin 用戶擁有 USER 和 ADMIN 的角色,user 用戶擁有 USER 角色,添加 admin.html 頁面設置只有 ADMIN 角色的用戶纔可以訪問。

admin.html:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <title>admin</title>
</head>
<body>
    <h1>admin</h1>
    <p>管理員頁面</p>
    <p>點擊 <a th:href="@{/}">這裏</a> 返回首頁</p>
</body>
</html>

添加後端訪問:

@RequestMapping("/admin")
public String admin() {
    return "admin";
}

我們再將上述的 configure() 方法修改如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/resources/**", "/").permitAll()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/content/**").access("hasRole('ADMIN') or hasRole('USER')")
            .anyRequest().authenticated()
            .and()
        .formLogin()
//                .loginPage("/login")
            .permitAll()
            .and()
        .logout()
            .permitAll()
            .and()
        .csrf()
            .ignoringAntMatchers("/logout");
}

重點看這些:

  • antMatchers("/resources/** ", "/").permitAll(),地址 "/resources/ **" 和 "/" 所有用戶都可訪問,permitAll 表示該請求任何人都可以訪問;
  • antMatchers("/admin/** ").hasRole("ADMIN"),地址 "/admin/**" 開頭的請求地址,只有擁有 ADMIN 角色的用戶纔可以訪問;
  • antMatchers("/content/** ").access("hasRole('ADMIN') or hasRole('USER')"),地址 "/content/**" 開頭的請求地址,可以給角色 ADMIN 或者 USER 的用戶來使用;
  • antMatchers("/admin/**").hasIpAddress("192.168.11.11"),只有固定 IP 地址的用戶可以訪問。

更多的權限控制方式參看下錶:

方法名 解釋
access(String) Spring EL 表達式結果爲 true 時可訪問
anonymous() 匿名可訪問
denyAll() 用戶不可以訪問
fullyAuthenticated() 用戶完全認證可訪問(非 remember me 下自動登錄)
hasAnyAuthority(String...) 參數中任意權限的用戶可訪問
hasAnyRole(String...) 參數中任意角色的用戶可訪問
hasAuthority(String) 某一權限的用戶可訪問
hasRole(String) 某一角色的用戶可訪問
permitAll() 所有用戶可訪問
rememberMe() 允許通過 remember me 登錄的用戶訪問
authenticated() 用戶登錄後可訪問
hasIpAddress(String) 用戶來自參數中的 IP 時可訪問

配置完成重新啓動項目,使用用戶 admin 登錄系統,所有頁面都可以訪問,使用 user 登錄系統,只可訪問不受限地址和 以 "/content/**" 開頭的請求,說明權限配置成功。

值得注意的是 hasRole() 和 access() 雖然都可以給角色賦予權限,但有所區別,比如 hasRole() 修飾的角色 "/admin/** ",那麼擁有 ADMIN 權限的用戶訪問地址 xxx/admin 和 xxx/admin/* 均可,如果使用 access() 修飾的角色,那麼訪問地址 xxx/admin 權限受限,請求 xxx/admin/ 可以通過。

方法級別的安全

上面是通過請求路徑來控制權限,也可以在方法上添加註解來限制控制訪問權限。

@PreAuthorize / @PostAuthorize

Spring 的 @PreAuthorize/@PostAuthorize 註解更適合方法級的安全,也支持 Spring EL 表達式語言,提供了基於表達式的訪問控制。

  • @PreAuthorize 註解:適合進入方法前的權限驗證,@PreAuthorize 可以將登錄用戶的角色 / 權限參數傳到方法中。
  • @PostAuthorize 註解:使用並不多,在方法執行後再進行權限驗證。
@PreAuthorize("hasAuthority('ADMIN')")
@RequestMapping("/admin")
public String admin() {
    return "admin";
}

這樣只要擁有角色 ADMIN 的用戶纔可以訪問此方法。

@Secured

此註釋是用來定義業務方法的安全配置屬性的列表,可以在需要安全 [ 角色 / 權限等 ] 的方法上指定 @Secured,並且只有那些角色 / 權限的用戶纔可以調用該方法。如果有人不具備要求的角色 / 權限但試圖調用此方法,將會拋出 AccessDenied 異常。

示例:

public interface UserService {

    List<User> findAllUsers();

    @Secured("ADMIN")
    void updateUser(User user);

    @Secured({ "USER", "ADMIN" })
    void deleteUser();
}

如此項目中便可根據角色來控制用戶擁有不同的權限。爲了方便演示,內容中所有用戶和角色信息均寫死在代碼中,在實際項目使用中,會將用戶、角色、權限控制等信息存儲到數據庫中,以更加方便靈活的方式去配置整個項目的權限。

總結

通過本課內容的學習,我們瞭解到 Spring Security 是一個專注認證和權限控制的一套安全框架。Spring Boot 有對應的組件包幫助集成,在 Spring Boot 項目中,可以通過不同的註解和配置來控制不同用戶、不同角色的訪問權限。Spring Security 是一款非常強大的安全控制框架,本課內容只是演示了常見的使用場景,若大家感興趣可以線下繼續學習瞭解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章