由於有一個項目需採用MVC構架,因此學習了Spring Security並記錄下來,但願你們一塊兒學習提供意見php
GitHub地址:github.com/Smith-Cruis…。html
原文地址:www.inlighting.org/2019/spring…。前端
若是有疑問,請在 GitHub 中發佈 issue,我有空會爲你們解答的java
本項目基於Spring Boot 2 + Spring Security 5 + Thymeleaf 2 + JDK11(你也能夠用8,應該區別不大)git
實現瞭如下功能:github
若是須要先後端分離的安全框架搭建教程能夠參考:Spring Boot 2 + Spring Security 5 + JWT 的單頁應用Restful解決方案web
若是想要直接體驗,直接 clone
項目,運行 mvn spring-boot:run
命令便可進行訪問,網址規則自行看教程後面算法
首頁spring
登入數據庫
登出
Home頁面
Admin頁面
403無權限頁面
Spring Security 過濾器鏈
Spring Security實現了一系列的過濾器鏈,就按照下面順序一個一個執行下去。
....class
一些自定義過濾器(在配置的時候你能夠本身選擇插到哪一個過濾器以前),由於這個需求因人而異,本文不探討,你們能夠本身研究UsernamePasswordAithenticationFilter.class
Spring Security 自帶的表單登入驗證過濾器,也是本文主要使用的過濾器BasicAuthenticationFilter.class
ExceptionTranslation.class
異常解釋器FilterSecurityInterceptor.class
攔截器最終決定請求可否經過Controller
咱們最後本身編寫的控制器相關類說明
User.class
:注意這個類不是咱們本身寫的,而是Spring Security官方提供的,他提供了一些基礎的功能,咱們能夠經過繼承這個類來擴充方法。詳見代碼中的 CustomUser.java
UserDetailsService.class
: Spring Security官方提供的一個接口,裏面只有一個方法loadUserByUsername()
,Spring Security會調用這個方法來獲取數據庫中存在的數據,而後和用戶POST過來的用戶名密碼進行比對,從而判斷用戶的用戶名密碼是否正確。因此咱們須要本身實現loadUserByUsername()
這個方法。詳見代碼中的 CustomUserDetailsService.java
。爲了體現權限區別,咱們經過HashMap構造了一個數據庫,裏面包含了4個用戶
ID | 用戶名 | 密碼 | 權限 |
---|---|---|---|
1 | jack | jack123 | user |
2 | danny | danny123 | editor |
3 | alice | alice123 | reviewer |
4 | smith | smith123 | admin |
說明下權限
user
:最基礎的權限,只要是登入用戶就有 user
權限
editor
:在 user
權限上面增長了 editor
的權限
reviewer
:與上同理,editor
和 reviewer
屬於同一級的權限
admin
:包含全部權限
爲了檢驗權限,咱們提供若干個頁面
網址 | 說明 | 可訪問權限 |
---|---|---|
/ | 首頁 | 全部人都可訪問(anonymous) |
/login | 登入頁面 | 全部人都可訪問(anonymous) |
/logout | 退出頁面 | 全部人都可訪問(anonymous) |
/user/home | 用戶中心 | user |
/user/editor | editor, admin | |
/user/reviewer | reviewer, admin | |
/user/admin | admin | |
/403 | 403錯誤頁面,美化過,你們能夠直接用 | 全部人都可訪問(anonymous) |
/404 | 404錯誤頁面,美化過,你們能夠直接用 | 全部人都可訪問(anonymous) |
/500 | 500錯誤頁面,美化過,你們能夠直接用 | 全部人都可訪問(anonymous) |
Maven 配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.inlighting</groupId>
<artifactId>security-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>security-demo</name>
<description>Demo project for Spring Boot & Spring Security</description>
<!--指定JDK版本,你們能夠改爲本身的-->
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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>
<!--對Thymeleaf添加Spring Security標籤支持-->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--開發的熱加載配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
複製代碼
application.properties配置
爲了使熱加載(這樣修改模板後無需重啓 Tomcat )生效,咱們須要在Spring Boot的配置文件上面加上一段話
spring.thymeleaf.cache=false
複製代碼
若是須要詳細瞭解熱加載,請看官方文檔:docs.spring.io/spring-boot…
首先咱們開啓方法註解支持:只須要在類上添加 @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
註解,咱們設置 prePostEnabled = true
是爲了支持 hasRole()
這類表達式。若是想進一步瞭解方法註解能夠看 Introduction to Spring Method Security 這篇文章。
SecurityConfig.java
/** * 開啓方法註解支持,咱們設置prePostEnabled = true是爲了後面可以使用hasRole()這類表達式 * 進一步瞭解可看教程:https://www.baeldung.com/spring-security-method-security */
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/** * TokenBasedRememberMeServices的生成密鑰, * 算法實現詳見文檔:https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token */
private final String SECRET_KEY = "123456";
@Autowired
private CustomUserDetailsService customUserDetailsService;
/** * 必須有此方法,Spring Security官方規定必需要有一個密碼加密方式。 * 注意:例如這裏用了BCryptPasswordEncoder()的加密方法,那麼在保存用戶密碼的時候也必須使用這種方法,確保先後一致。 * 詳情參見項目中Database.java中保存用戶的邏輯 */
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/** * 配置Spring Security,下面說明幾點注意事項。 * 1. Spring Security 默認是開啓了CSRF的,此時咱們提交的POST表單必須有隱藏的字段來傳遞CSRF, * 並且在logout中,咱們必須經過POST到 /logout 的方法來退出用戶,詳見咱們的login.html和logout.html. * 2. 開啓了rememberMe()功能後,咱們必須提供rememberMeServices,例以下面的getRememberMeServices()方法, * 並且咱們只能在TokenBasedRememberMeServices中設置cookie名稱、過時時間等相關配置,若是在別的地方同時配置,會報錯。 * 錯誤示例:xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name") */
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login") // 自定義用戶登入頁面
.failureUrl("/login?error") // 自定義登入失敗頁面,前端能夠經過url中是否有error來提供友好的用戶登入提示
.and()
.logout()
.logoutUrl("/logout")// 自定義用戶登出頁面
.logoutSuccessUrl("/")
.and()
.rememberMe() // 開啓記住密碼功能
.rememberMeServices(getRememberMeServices()) // 必須提供
.key(SECRET_KEY) // 此SECRET須要和生成TokenBasedRememberMeServices的密鑰相同
.and()
/* * 默認容許全部路徑全部人均可以訪問,確保靜態資源的正常訪問。 * 後面再經過方法註解的方式來控制權限。 */
.authorizeRequests().anyRequest().permitAll()
.and()
.exceptionHandling().accessDeniedPage("/403"); // 權限不足自動跳轉403
}
/** * 若是要設置cookie過時時間或其餘相關配置,請在下方自行配置 */
private TokenBasedRememberMeServices getRememberMeServices() {
TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService);
services.setCookieName("remember-cookie");
services.setTokenValiditySeconds(100); // 默認14天
return services;
}
}
複製代碼
UserService.java
本身模擬數據庫操做的Service
,用於向本身經過HashMap
模擬的數據源獲取數據。
@Service
public class UserService {
private Database database = new Database();
public CustomUser getUserByUsername(String username) {
CustomUser originUser = database.getDatabase().get(username);
if (originUser == null) {
return null;
}
/* * 此處有坑,之因此這麼作是由於Spring Security得到到User後,會把User中的password字段置空,以確保安全。 * 由於Java類是引用傳遞,爲防止Spring Security修改了咱們的源頭數據,因此咱們複製一個對象提供給Spring Security。 * 若是經過真實數據庫的方式獲取,則沒有這種問題須要擔憂。 */
return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities());
}
}
複製代碼
CustomUserDetailsService.java
/** * 實現官方提供的UserDetailsService接口便可 */
@Service
public class CustomUserDetailsService implements UserDetailsService {
private Logger LOGGER = LoggerFactory.getLogger(getClass());
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
CustomUser user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("該用戶不存在");
}
LOGGER.info("用戶名:"+username+" 角色:"+user.getAuthorities().toString());
return user;
}
}
複製代碼
咱們在開發網站的過程當中,好比 GET /user/editor
這個請求角色爲 EDITOR
和 ADMIN
確定均可以,若是咱們在每個須要判斷權限的方法上面寫一長串的權限表達式,必定很複雜。可是經過自定義權限註解,咱們能夠經過 @IsEditor
這樣的方法來判斷,這樣一來就簡單了不少。進一步瞭解能夠看:Introduction to Spring Method Security
IsUser.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyAuthority('ROLE_USER', 'ROLE_EDITOR', 'ROLE_REVIEWER', 'ROLE_ADMIN')")
public @interface IsUser {
}
複製代碼
IsEditor.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')")
public @interface IsEditor {
}
複製代碼
IsReviewer.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')")
public @interface IsReviewer {
}
複製代碼
IsAdmin.java
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin {
}
複製代碼
Spring Security自帶表達式
hasRole()
,是否擁有某一個權限
hasAnyRole()
,多個權限中有一個便可,如 hasAnyRole("ADMIN","USER")
hasAuthority()
,Authority
和 Role
很像,惟一的區別就是 Authority
前綴多了 ROLE_
,如 hasAuthority("ROLE_ADMIN")
等價於 hasRole("ADMIN")
,能夠參考上面 IsUser.java
的寫法
hasAnyAuthority()
,同上,多個權限中有一個便可
permitAll()
, denyAll()
,isAnonymous()
, isRememberMe()
,經過字面意思能夠理解
isAuthenticated()
, isFullyAuthenticated()
,這兩個區別就是isFullyAuthenticated()
對認證的安全要求更高。例如用戶經過記住密碼功能登入到系統進行敏感操做,isFullyAuthenticated()
會返回false
,此時咱們可讓用戶再輸入一次密碼以確保安全,而 isAuthenticated()
只要是登入用戶均返回true
。
principal()
, authentication()
,例如咱們想獲取登入用戶的id,能夠經過principal()
返回的 Object
獲取,實際上 principal()
返回的 Object
基本上能夠等同咱們本身編寫的 CustomUser
。而 authentication()
返回的 Authentication
是 Principal
的父類,相關操做可看 Authentication
的源碼。進一步瞭解能夠看後面Controller編寫中獲取用戶數據的四種方法
hasPermission()
,參考字面意思便可
若是想進一步瞭解,能夠參考 Intro to Spring Security Expressions。
咱們經過 thymeleaf-extras-springsecurity
來添加Thymeleaf對Spring Security的支持。
Maven配置
上面的Maven配置已經加過了
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
複製代碼
使用例子
注意咱們要在html中添加 xmlns:sec
的支持
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Admin</title>
</head>
<body>
<p>This is a home page.</p>
<p>Id: <th:block sec:authentication="principal.id"></th:block></p>
<p>Username: <th:block sec:authentication="principal.username"></th:block></p>
<p>Role: <th:block sec:authentication="principal.authorities"></th:block></p>
</body>
</html>
複製代碼
若是想進一步瞭解請看文檔 thymeleaf-extras-springsecurity。
IndexController.java
本控制器沒有任何的權限規定
@Controller
public class IndexController {
@GetMapping("/")
public String index() {
return "index/index";
}
@GetMapping("/login")
public String login() {
return "index/login";
}
@GetMapping("/logout")
public String logout() {
return "index/logout";
}
}
複製代碼
UserController.java
在這個控制器中,我綜合展現了自定義註解的使用和4種獲取用戶信息的方式
@IsUser // 代表該控制器下全部請求都須要登入後才能訪問
@Controller
@RequestMapping("/user")
public class UserController {
@GetMapping("/home")
public String home(Model model) {
// 方法一:經過SecurityContextHolder獲取
CustomUser user = (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
model.addAttribute("user", user);
return "user/home";
}
@GetMapping("/editor")
@IsEditor
public String editor(Authentication authentication, Model model) {
// 方法二:經過方法注入的形式獲取Authentication
CustomUser user = (CustomUser)authentication.getPrincipal();
model.addAttribute("user", user);
return "user/editor";
}
@GetMapping("/reviewer")
@IsReviewer
public String reviewer(Principal principal, Model model) {
// 方法三:一樣經過方法注入的方法,注意要轉型,此方法很二,不推薦
CustomUser user = (CustomUser) ((Authentication)principal).getPrincipal();
model.addAttribute("user", user);
return "user/reviewer";
}
@GetMapping("/admin")
@IsAdmin
public String admin() {
// 方法四:經過Thymeleaf的Security標籤進行,詳情見admin.html
return "user/admin";
}
}
複製代碼
注意
SecurityContext
是線程綁定的,若是咱們在當前的線程中新建了別的線程,那麼他們的 SecurityContext
是不共享的,進一步瞭解請看 Spring Security Context Propagation with @Async在編寫html的時候,基本上就是大同小異了,就是注意一點,**若是開啓了CSRF,在編寫表單POST請求的時候添加上隱藏字段,如 **<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
,不過你們其實不用加也沒事,由於Thymeleaf自動會加上去的😀。
教程粗糙,歡迎指正!
如需深刻了解,若是想系統的學習能夠看看 Security with Spring。