Grails 的 Spring Security Core 插件使用教程
本教程的目標是
- 用 SpringSecurityCore plugin實現對“URL”的保護,即只有登錄用戶纔可以訪問。
- 更進一步,對不同的URL資源賦予不同的角色,特定的 URL 只允許擁有特定“角色Role”的用戶訪問。
核心概念
首先了解一下 JavaSecure 技術和 SpringSecurity的核心概念:
-
Java Security 中使用的術語和概念:
- Java安全中使用術語“主體”(Subject)來表示訪問請求的來源,通俗說就是某個“人”、“登錄的這個用戶(人、組織等)”。
一個主體可以是任何的實體(實體通俗說就是某種東西,可以是人或者組織)。 - 一個主體可以有多個不同的“身份標識”(Principal)。例如登錄網站的“某個人(subject)”就會有一個“用戶”對象作爲Principal身份標識,代表登錄的這個人。
比如一個應用的用戶這類主體,就可以有用戶名、身份證號碼和手機號碼等多種身份標識。
除了身份標識之外,一個主體還可以有公開或是私有的安全相關的憑證(Credential),包括密碼和密鑰等。
- Java安全中使用術語“主體”(Subject)來表示訪問請求的來源,通俗說就是某個“人”、“登錄的這個用戶(人、組織等)”。
-
和認證相關的概念:
- 認證(Authentication):通過讓用戶輸入“用戶名、密碼”等證明信息來確認該用戶的真實身份。
- 身份標識(Principal):主體的某種ID標識,可以是用戶真實姓名、賬號的用戶名、手機號碼、身份證號等。通常也被當成“用戶”這個概念。
爲什麼身份標識Principal可以當成“用戶”,是因爲一個人可以在不同網站有不同的“身份標識”,即不同的“用戶”賬號。
這個主體在網站的“用戶賬號”就是該主體在這個網站的“身份的標識”Principal。 - 憑證(credentials):用來驗證用戶身份的東西,可以是“密碼”、“證書”、“短信驗證碼”、“指紋”等各類憑據。
-
和授權相關的概念:
- 要意識到授權包括兩個動作,“授權”和“鑑權”。
- 權限(authorities):即訪問某個資源、執行某個操作的權利。也就是 permissions(許可)。
- 授予的權限(granted authorities):這裏是名詞,而不是動詞,表示某個主體已經被授予或者說分配了的權限。注意不是授權動作。
- 訪問控制(Access Control):也稱爲鑑權,即決定已認證的主體是否有權利訪問本資源、URL或執行方法等操作。
- 角色(Role):代表某種工作職責和權利範圍,例如“管理員(admin)”、“編輯(editor)”等。角色會用在兩個地方,
分配權限時和執行訪問控制時,即授權和鑑權時。注意,角色也只是實現訪問控制的一種方式,還有其他的實現方式。 - 角色組(Group):一組角色的集合,是爲了更方便地給用戶分配多個角色而設計的概念。
- 棄權(abstain):放棄投票權。
- 肯定式的(affirmative):只要有一個投票者允許訪問,則認爲有權訪問的一種投票機制。
- 基於共識的(Consensus Based):基於共識的投票機制,是指只要大多數同意則認爲投票通過,有權利訪問。
- 一致性的(Unanimous Based):一致性投票機制,要求所有投票者都同意或都棄權纔算通過。
- 可否決的(Vetoable):只要有一票否決,就認爲投票不通過的機制。
理解 SpringSecurity 的工作原理
1、認證
首先要仔細閱讀的是 Principle 身份標識類。它是 Oracle公司的 Java Security 規範定義的,這個接口定義如下:
public interface Principal {
/**
* 和另外一個 Principle(身份標識)比較,看是否相同。
*
* @return true 表示相同,false 表示不同.
*/
public boolean equals(Object another);
/**
* 返回能表示本身份標識的字符串
*/
public String toString();
/**
* Returns a hashcode for this principal.
*/
public int hashCode();
/**
* 返回本身份標識的名稱。
* 例如:用戶名類型的身份標識返回的就是“用戶名”,身份證類型身份標識返回的就是“身份證號”。
*/
public String getName();
/**
* 如果本身份標識代表的就是參數指定的主體,則返回true,否則返回 false。
*
* @since 1.8
*/
public default boolean implies(Subject subject) {
if (subject == null)
return false;
return subject.getPrincipals().contains(this);
}
}
接下來需要閱讀的是 Spring Security 定義的 Authentication 接口,它繼承自 Principle,代表認證相關信息,
包括身份標識、憑據、認證的結果以及權限,定義如下:
public interface Authentication extends Principal, Serializable {
/**
* 已經授予本認證標識的權限。因爲 Principle 代表的是一個主體(Subject),所以其實等價於是授予 主體 的權限。
* 這個權限是由 AuthenticationManager 設置的。
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 證明本身份標識有效的“憑據(Credential)”。通常就是“密碼”,也可以是其他 AuthenticationManager 認識的東西。
* 由調用方/使用方來設置本憑據。
*/
Object getCredentials();
/**
* 存放額外的認證請求信息,例如 IP地址,整數序列號等。可以是 null。
*/
Object getDetails();
/**
* 返回正在被認證或已經認證過的“身份標識”對象。一般就是登錄的“用戶”對象。
*/
Object getPrincipal();
/**
* 返回 true 表示已經認證成功過了。也就是說可以信任已經發出的 token(令牌)。
*/
boolean isAuthenticated();
/**
* 設置 isAuthenticated 屬性。true 表示可以信任已經發出的令牌(Token),false 表示不再信任該令牌。
*/
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
SpringSecurity 中,認證這個動作,是由 AuthenticationManager 接口的實現類來完成的。
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 接口是一個策略接口(Strategy Interface),表示它可能有不同的實現類來實現不同的“策略”,通俗說就是
實現不同的認證模式。默認是由 ProviderManager 類來實現這個接口。ProviderManager 實現認證的模式就是將認證工作代理給一組
更具體的 AuthenticationProvider 來做。這樣的設計模式就是一個“責任鏈”模式。
ProviderManager 可以有一個parent(父)ProviderManager對象,當它自己的所有 providers 認證提供者都不能決定本次認證時,
它就會詢問它的父對象,來完成認證。這樣就形成了一個樹形結構,以便對資源進行邏輯分組,比如所有的“/api/”用一個
認證管理器,而“/user/”用另外一個認證管理器。根節點就代表了公共認證器,這就可以形成一個樹形結構,結構如下圖所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WwIvjYDR-1586921961312)(./doc_images/authentication.png)]
AuthenticationProvider 接口能夠讓調用方知道本對象是否支持對指定類型的 Authentication 對象進行身份驗證。
public interface AuthenticationProvider {
/**
* 執行身份認證
*/
Authentication authenticate(Authentication authentication) throws AuthenticationException;
/**
* 是否支持指定類的 Authentication 認證。
*/
boolean supports(Class<?> authentication);
}
UserDetails 和 UserDetailsService
SpringSecurity框架中,爲“身份標識”(principle)這個概念提供了一個具體的定義,即 UserDetails 接口。這個接口定義了
userName, password, authorities(權限), expired(過期), lock(鎖定), credentialExpire(密碼過期), enable(用戶可用) 這樣一些
屬性。UserDetails 接口的具體對象,就是 Authentication.getPrinciple() 所返回的對象,它是SpringSecurity框架與具體應用程序
之間的一個適配器,讓SpringSecurity可用適應各種不同的應用程序。
怎麼創建這個 UserDetails 對象呢?
它這是由 UserDetailsService 接口創建的,這個接口只有一個方法:
public interface UserDetailsService {
/**
* 通過用戶名查找對象
*
* @return 一個完整的用戶詳情對象,不能是null
*
* @throws UsernameNotFoundException 如果用戶不存在或沒有任何授權(GrantedAuthority)
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
怎麼來創建、配置一個 AuthenticationManager 呢?
用 AuthenticationManagerBuilder 這個工具類可以創建和配置 AuthenticationManager,例如下面的代碼演示了創建一個頂層、全局的
AuthenticationManager。
@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
... // web stuff here
@Autowired
public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
builder.jdbcAuthentication().dataSource(dataSource).withUser("dave").password("secret").roles("USER");
}
}
2、授權和鑑權(訪問控制)以及資源的權限
SpringSecurity 中,決定一個用戶是否有權限訪問某資源,是由 AccessDecisionVoter 接口的具體實現類來完成的。這個接口有下面的
方法:
boolean supports(ConfigAttribute attribute);
boolean supports(Class<?> clazz);
int vote(Authentication authentication, S object,
Collection<ConfigAttribute> attributes);
其中的 vote 方法是關鍵。vote 方法決定一個“認證”對象(即 Authentication 對象),是否能訪問某個資源 S object。
資源的所有者爲了進一步描述“允許誰訪問本資源”這種規則,於是就用一組 ConfigAttribute 對象來描述,
這就是 Collection attributes 參數。ConfigAttribute 是一個接口,它只有一個簡單的方法,返回一個字符串,
這個字符串將描述這種“訪問規則”,最常見的是返回“用戶角色(User Role)”的定義,例如“ROLE_ADMIN 或者 ROLE_AUDIT”。
下面是我之前錯誤的理解:
原來我認爲“角色”就是不同權限的集合,一個用戶擁有了某個“角色”那麼他就有了一組對應的權限。
從這個意義上說,我之前理解的“角色”其實對應SpringSecurity中的“角色組”,
而之前我理解的“權限”對應SpringSecurity的“角色(Role)”。
其實 SpringSecurity 中權限是由 GrantedAuthorities 接口表示的,它只有一個返回String的方法 getAuthority(),因此
權限通常就是用“角色”來表示的,如“ROLE_ADMIN”表示管理員權限,也就是說角色也代表了它所擁有的權限。
在 Spring Security 中,給用戶授予的權限由 Authenticate 接口的 authorities 方法提供,這個“權限”通常就是一組分配給
用戶的角色字符串,如“ROLE_ADMIN”等。
Grails Spring-Security-Core plugin 使用教程
文檔
工作總覽
- 引入依賴包 ‘org.grails.plugins:spring-security-core:4.0.0.RC3’
- 創建不安全的 web 應用
- 創建 spring-security-core 配置和必要的領域對象
1.創建不安全的 web 應用
創建一個 Contract Domain對象。
創建一個 Controller,列出所有的合同。
可以用 grails 命令,方便地創建 domain、controller 和 view。
2.引入依賴包 ‘org.grails.plugins:spring-security-core:4.0.0.RC3’
build.gradle 文件中添加一行依賴聲明
dependencies {
compile 'org.grails.plugins:spring-security-core:4.0.0.RC3'
}
3.創建 spring-security-core 配置和必要的領域對象
$ grails s2-quickstart com.mycompany.myapp User Role
注意:
-
包名不能省略。
-
要檢查領域名稱是否是數據庫的保留關鍵字,例如有的數據庫就不能使用 User、Group、Role作爲表名。最好避免使用這些常見的名稱
作爲領域對象名。如果一定要使用,需要用mapping指定引用模式,如:static mapping = {
table ‘user
’
}
執行 s2-quickstart 命令後,會創建以下文件:
resources.groovy
application.groovy
Role.groovy
User.groovy
UserRole.groovy
UserPasswordEncoderListener.groovy
然後,我們在 BootStrap.groovy 中初始化數據庫內容。
def init = { servletContext ->
environments {
development {
def dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
new Contract(name: "一期", signDate: dateFormat.parse("2017-09-01 00:00:00")).save()
new Contract(name: "二期", signDate: dateFormat.parse("2018-01-10 00:00:00")).save()
new Contract(name: "三期", signDate: dateFormat.parse("2019-10-15 00:00:00")).save()
def user = new User(username: "yangbo", password: "123").save()
def role = new Role(authority: "ROLE_ADMIN").save()
UserRole.withTransaction {
UserRole.create(user, role)
}
assert UserRole.count == 1
}
}
}
注意:UserRole 必須用withTransaction,因爲 init 閉包不會在事務中或者 session 中運行,需要顯式創建一個事務,
因爲 UserRole.create() save時設置了flush=false,即不會立即保存到數據庫中。
方法是在 application.yml 中添加下面內容:
plugins:
springsecurity:
logout:
postOnly: false
告訴 grails spring-security-core plugin 支持 GET 模式的登出,這樣方便測試,否則要編寫一個 form 來提交登出,測試比較費事。
這時,訪問 contract controller (/contract/index),就會跳轉到 login 頁面,輸入正確的用戶名密碼後,就能進入 /home 頁面,但訪問
/contract 頁面還是提示沒有權限,這是因爲沒有配置訪問 /contract url 所需的權限。
spring-security-ui plugin 支持用戶、角色的創建界面,但 core 插件是沒有的。
測試 RememberMe 功能
登錄時勾選上“Remember Me”,就可以在關閉瀏覽器重新打開瀏覽器後,自動完成登錄,訪問需要登錄的URL。
注意,如果重啓了web服務,那麼記住的token就會失效,需要重新登錄,如果想要避免這種情況,需要使用“持久化記住我”模式的實現。
這是通過在 cookie 中記錄了一個 token,然後通過 token 驗證用戶是否已經成功登錄來實現的。
Token 的格式類似於“yangbo:1583156448794:dda4994f2c2cf3e2afac0cc5169a0bc4”,即
“username : expiryTime : Md5Hex(username:expiryTime:password:key)”
這樣的格式。具體實現可以查看 TokenBasedRememberMeServices 類。另外一種更安全的實現方法是持久性Token,
由 PersistentTokenBasedRememberMeServices 類實現。
到這裏,我們已經完成了最基本的“安全化一個web應用”的開發。
下一步工作
- 添加 security UI,使用 security-ui plugin對用戶、角色、權限進行管理,實現用戶註冊、找回密碼、ACL 等功能。
- 使用 Group 簡化角色的分配
- 使用 grails-spring-security-rest plugin 實現無狀態的 REST 安全化。
值得一讀的 SpringSecurity 文檔
- https://docs.spring.io/spring-security/site/docs/current/reference/html5/#overall-architecture
- https://docs.spring.io/spring-security/site/docs/current/reference/html5/#tech-intro-authentication
其他安全相關的 plugins
-
grails-spring-security-ui Grails安全UI插件
提供對安全領域對象的CRUD功能,即增刪改查“用戶、角色、權限”等對象。 -
grails-spring-security-acl Grails ACL 插件,將權限功能增強到可以對每個實體對象進行授權。
Grails 相關技巧
Spring Security 使用注意事項:
- 在 Spring Security 中,需要給每一個被保護的URL映射一個角色(Role),可以使用“層級角色”(Hierarchical Roles)技術
來簡化這個映射配置。 - 在 Spring Security 中,想要方便地給一個用戶一次性地分配多個角色,可以將多個角色定義爲一個“角色組”(Group),然後
給這個用戶授予“角色組”即可。
如何讓 Spring Security 支持多租戶的 SaaS 場景?
可以用 filter + ThreadLocale 的方式。即先用過濾器在 spring security filter 之前設置好線程本地變量 tenantId,然後
實現一個自定義的 UserDetailService,在查找用戶時通過線程本地變量獲取 tenantId 並且作爲查詢用戶的條件。
如果我們使用 JWT 來提供 REST API,那麼我們還需要自定義 JWT 的 claims 內容,將 tenantId 寫入 claims 中。
GIT 項目地址
github https://github.com/yangbo/grails_tutorials.git
歡迎給 github 項目點贊,分享。