Springboot+SpringSecurity案例
Spring Security 簡介
Spring 是一個非常流行和成功的 Java 應用開發框架。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。一般來說,Web 應用的安全性包括使用者認證(Authentication)和使用者授權(Authorization)兩個部分。使用者認證指的是驗證某個使用者是否為系統中的合法主體,也就是說使用者能否訪問該系統。使用者認證一般要求使用者提供使用者名稱和密碼。系統通過校驗使用者名稱和密碼來完成認證過程。使用者授權指的是驗證某個使用者是否有許可權執行某個操作。在一個系統中,不同使用者所具有的許可權是不同的。比如對一個檔案來說,有的使用者只能進行讀取,而有的使用者可以進行修改。一般來說,系統會為不同的使用者分配不同的角色,而每個角色則對應一系列的許可權。
對於上面提到的兩種應用情景,Spring Security 框架都有很好的支援。在使用者認證方面,Spring Security 框架支援主流的認證方式,包括 HTTP 基本認證、HTTP 表單驗證、HTTP 摘要認證、OpenID 和 LDAP 等。在使用者授權方面,Spring Security 提供了基於角色的訪問控制和訪問控制列表(Access Control List,ACL),可以對應用中的領域物件進行細粒度的控制。
案例介紹
使用springboot+mybatis+SpringSecurity 通過使用者角色許可權資料庫管理,實現基本使用者認證和授權
本節從最基本的使用者認證和授權開始對 Spring Security 進行介紹。一般來說,Web 應用都需要儲存自己系統中的使用者資訊。這些資訊一般儲存在資料庫中。使用者可以註冊自己的賬號,或是由系統管理員統一進行分配。這些使用者一般都有自己的角色,如普通使用者和管理員之類的。某些頁面只有特定角色的使用者可以訪問,比如只有管理員才可以訪問 /admin 這樣的網址。下面介紹如何使用 Spring Security 來滿足這樣基本的認證和授權的需求。
專案結構
pom.xml檔案
<!--springboot--> <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.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency> <!--db--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>6.0.5</version> </dependency> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> <exclusions> <exclusion> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> </exclusion> </exclusions> </dependency> <!--mybatis--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency>
資料庫設計
程式碼分析
1.新增 Spring Security 的過濾器
@Autowired
private MyFilterSecurityInterceptor myFilterSecurityInterceptor;
/**註冊UserDetailsService 的bean
* @return
*/
@Bean
UserDetailsService customUserService() {
return new CustomUserService();
}
/**
* user Details Service驗證
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated() //任何請求,登入後可以訪問
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error")
.permitAll() //登入頁面使用者任意訪問
.and()
.logout().permitAll(); //登出行為任意訪問
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
.csrf().disable();
}
@Service
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi裡面有一個被攔截的url
//裡面呼叫MyInvocationSecurityMetadataSource的getAttributes(Object object)這個方法獲取fi對應的所有許可權
//再呼叫MyAccessDecisionManager的decide方法來校驗使用者的許可權是否足夠
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//執行下一個攔截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void destroy() {
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}
Spring Security 使用的是 Servlet 規範中標準的過濾器機制。對於特定的請求,Spring Security 的過濾器會檢查該請求是否通過認證,以及當前使用者是否有足夠的許可權來訪問此資源。對於非法的請求,過濾器會跳轉到指定頁面讓使用者進行認證,或是返回出錯資訊。需要注意的是,Spring Security 實際上是使用多個過濾器形成的鏈條來工作的。
2.配置 Spring Security 來宣告系統中的合法使用者及其對應的許可權
//自定義UserDetailsService 介面
@Service
public class CustomUserService implements UserDetailsService {
@Autowired
UserDao userDao;
@Autowired
PermissionDao permissionDao;
public UserDetails loadUserByUsername(String username) {
SysUser user = userDao.findByUserName(username);
if (user != null) {
List<Permission> permissions = permissionDao.findByAdminUserId(user.getId());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (Permission permission : permissions) {
if (permission != null && permission.getName() != null) {
GrantedAuthority grantedAuthority = new MyGrantedAuthority(permission.getUrl(), permission.getMethod());
grantedAuthorities.add(grantedAuthority);
}
}
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
} else {
throw new UsernameNotFoundException("admin: " + username + " do not exist!");
}
}
}
使用者相關的資訊是通過org.springframework.security.core.userdetails.UserDetailsService 介面來載入的。該介面的唯一方法是loadUserByUsername(String username),用來根據使用者名稱載入相關的資訊。這個方法的返回值是org.springframework.security.core.userdetails.UserDetails 介面,其中包含了使用者的資訊,包括使用者名稱、密碼、許可權、是否啟用、是否被鎖定、是否過期等。其中最重要的是使用者許可權,由 org.springframework.security.core.GrantedAuthority 介面來表示。雖然 Spring Security 內部的設計和實現比較複雜,但是一般情況下,開發人員只需要使用它預設提供的實現就可以滿足絕大多數情況下的需求,而且只需要簡單的配置宣告即可。
3. 配置對不同 URL 模式的訪問許可權
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {
//decide 方法是判定是否擁有許可權的決策方法
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
String url, method;
AntPathRequestMatcher matcher;
for (GrantedAuthority ga : authentication.getAuthorities()) {
if (ga instanceof MyGrantedAuthority) {
MyGrantedAuthority urlGrantedAuthority = (MyGrantedAuthority) ga;
url = urlGrantedAuthority.getPermissionUrl();
method = urlGrantedAuthority.getMethod();
matcher = new AntPathRequestMatcher(url);
if (matcher.matches(request)) {
//當權限表許可權的method為ALL時表示擁有此路徑的所有請求方式權利。
if (method.equals(request.getMethod()) || "ALL".equals(method)) {
return;
}
}
} else if (ga.getAuthority().equals("ROLE_ANONYMOUS")) {//未登入只允許訪問 login 頁面
matcher = new AntPathRequestMatcher("/login");
if (matcher.matches(request)) {
return;
}
}
}
throw new AccessDeniedException("no right");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
一方面 Spring Security 對開發中經常會用到的功能提供了很好的預設實現,另外一方面也提供了非常靈活的定製能力,允許開發人員提供自己的實現。
支援 restful 風格的介面,判斷使用者是不是有許可權訪問的時候不僅要判斷 url 還要判斷 請求方式。
由於要判斷 url 和 method 所以要在CustomUserService 類的 loadUserByUsername 方法中要新增 許可權的 url 和 method 。但是SimpleGrantedAuthority 只支援傳入一個引數。需要再寫一個類 實現 GrantedAuthority 介面,同時關閉CSRF。CSRF(Cross-site request forgery跨站請求偽造),也被稱為“One Click Attack” 或者Session Riding,攻擊方通過偽造使用者請求訪問受信任站點。Spring Security 4.0之後,引入了CSRF,預設是開啟。不得不說,CSRF和RESTful技術有衝突。CSRF預設支援的方法: GET|HEAD|TRACE|OPTIONS,不支援POST。
介紹如何用 Spring Security 實現基本的使用者認證和授權之後,下面介紹其中的核心物件。
SecurityContext 和 Authentication 物件
org.springframework.security.core.context.SecurityContext介面表示的是當前應用的安全上下文。通過此介面可以獲取和設定當前的認證物件。org.springframework.security.core.Authentication介面用來表示此認證物件。通過認證物件的方法可以判斷當前使用者是否已經通過認證,以及獲取當前認證使用者的相關資訊,包括使用者名稱、密碼和許可權等。要使用此認證物件,首先需要獲取到 SecurityContext 物件。通過org.springframework.security.core.context.SecurityContextHolder 類提供的靜態方法 getContext() 就可以獲取。再通過 SecurityContext物件的 getAuthentication()就可以得到認證物件。通過認證物件的 getPrincipal() 方法就可以獲得當前的認證主體,通常是 UserDetails 介面的實現。典型的認證過程就是當用戶輸入了使用者名稱和密碼之後,UserDetailsService通過使用者名稱找到對應的 UserDetails 物件,接著比較密碼是否匹配。如果不匹配,則返回出錯資訊;如果匹配的話,說明使用者認證成功,就建立一個實現了 Authentication介面的物件,如 org.springframework.security. authentication.UsernamePasswordAuthenticationToken 類的物件。再通過 SecurityContext的 setAuthentication() 方法來設定此認證物件。
預設情況下,SecurityContextHolder使用 ThreadLocal來儲存 SecurityContext物件。因此,SecurityContext物件對於當前執行緒上所有方法都是可見的。這種實現對於 Web 應用來說是合適的。不過在有些情況下,如桌面應用,這種實現方式就不適用了。Spring Security 允許開發人員對此進行定製。開發人員只需要實現介面org.springframework.security.core.context.SecurityContextHolderStrategy並通過 SecurityContextHolder的setStrategyName(String)方法讓 Spring Security 使用此實現即可。另外一種設定方式是使用系統屬性。除此之外,Spring Security 預設提供了另外兩種實現方式:MODE_GLOBAL表示當前應用共享唯一的 SecurityContextHolder;MODE_INHERITABLETHREADLOCAL表示子執行緒繼承父執行緒的 SecurityContextHolder。