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 是一款非常強大的安全控制框架,本課內容只是演示了常見的使用場景,若大家感興趣可以線下繼續學習瞭解。