javascript
Spring Security用户认证和权限控制(默认实现)
1 背景
實際應用系統中,為了安全起見,一般都必備用戶認證(登錄)和權限控制的功能,以識別用戶是否合法,以及根據權限來控制用戶是否能夠執行某項操作。
Spring Security是一個安全相關的框架,能夠與Spring項目無縫整合,本文主要是介紹Spring Security默認的用戶認證和權限控制的使用方法和原理,但不涉及到自定義實現。
Spring Security用戶認證和權限控制(自定義實現)這篇文章專門講解用戶認證和權限控制相關的自定義實現。
2 實戰示例
2.1 創建工程
創建一個名為authentication-server的spring boot工程,項目結構如下圖所示:
說明:該spring boot工程主要是整合了Spring Security框架和Spring MVC框架。
2.2 配置說明
pom.xml配置文件如下所示:
<?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><groupId>com.authentication.server</groupId><artifactId>authentication-server</artifactId><version>0.0.1-SNAPSHOT</version><name>authentication-server</name><description>統一用戶認證中心</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><properties><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>2.2 用戶認證
要想使用Spring Security框架,配置類需要繼承WebSecurityConfigurerAdapter類,并通過注解@EnableWebSecurity來啟用Spring Security。
本文的用戶認證是使用Spring Security默認的基于用戶名和密碼的表單認證,需要在配置類中重寫protected void configure(AuthenticationManagerBuilder auth)方法,并在重寫的方法中指定默認從哪里獲取認證用戶的信息,即指定一個UserDetailsService接口的實現類。此外,還需要重寫protected void configure(HttpSecurity http)方法,并在重寫的方法中進行一系列的安全配置。本示例的配置類WebSecurityConfig代碼如下所示:
package com.authentication.server.config;import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;/*** Spring Security配置類*/ @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsServiceImpl;/*** 用戶認證配置*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {/*** 指定用戶認證時,默認從哪里獲取認證用戶信息*/auth.userDetailsService(userDetailsServiceImpl);}/*** Http安全配置*/@Overrideprotected void configure(HttpSecurity http) throws Exception {/*** 表單登錄:使用默認的表單登錄頁面和登錄端點/login進行登錄* 退出登錄:使用默認的退出登錄端點/logout退出登錄* 記住我:使用默認的“記住我”功能,把記住用戶已登錄的Token保存在內存里,記住30分鐘* 權限:除了/toHome和/toUser之外的其它請求都要求用戶已登錄* 注意:Controller中也對URL配置了權限,如果WebSecurityConfig中和Controller中都對某文化URL配置了權限,則取較小的權限*/http.formLogin().defaultSuccessUrl("/toHome", false).permitAll().and().logout().permitAll().and().rememberMe().tokenValiditySeconds(1800).and().authorizeRequests().antMatchers("/toHome", "/toUser").permitAll().anyRequest().authenticated();}/*** 密碼加密器*/@Beanpublic PasswordEncoder passwordEncoder() {/*** BCryptPasswordEncoder:相同的密碼明文每次生成的密文都不同,安全性更高*/return new BCryptPasswordEncoder();}}Spring Security進行用戶認證時,需要根據用戶的賬號、密碼、權限等信息進行認證,因此,需要根據查詢到的用戶信息封裝成一個認證用戶對象并交給Spring Security進行認證。查詢用戶信息并封裝成認證用戶對象的過程是在UserDetailsService接口的實現類(需要用戶自己實現)中完成的。本示例的UserDetailsService接口實現類UserDetailsServiceImpl的代碼如下所示:
package com.authentication.server.service.impl;import com.authentication.server.model.AuthUser; import com.authentication.server.service.AuthUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils;import java.util.ArrayList; import java.util.List;/*** 自定義的認證用戶獲取服務類*/ @Component("userDetailsServiceImpl") public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate AuthUserService authUserServiceImpl;/*** 根據用戶名獲取認證用戶信息*/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {if(StringUtils.isEmpty(username)) {throw new UsernameNotFoundException("UserDetailsService沒有接收到用戶賬號");} else {/*** 根據用戶名查找用戶信息*/AuthUser authUser = authUserServiceImpl.getAuthUserByUsername(username);if(authUser == null) {throw new UsernameNotFoundException(String.format("用戶'%s'不存在", username));}List<GrantedAuthority> grantedAuthorities = new ArrayList<>();for (String role : authUser.getRoles()) {//封裝用戶信息和角色信息到SecurityContextHolder全局緩存中grantedAuthorities.add(new SimpleGrantedAuthority(role));}/*** 創建一個用于認證的用戶對象并返回,包括:用戶名,密碼,角色*/return new User(authUser.getUsername(), authUser.getPassword(), grantedAuthorities);}} }查詢用戶信息的接口AuthUserService 的代碼如下所示:
package com.authentication.server.service;import com.authentication.server.model.AuthUser;/*** 用戶服務類*/ public interface AuthUserService {/*** 通過用戶賬號獲取認證用戶信息*/AuthUser getAuthUserByUsername(String username);}查詢用戶信息的接口實現類AuthUserServiceImpl的代碼如下所示:
package com.authentication.server.service.impl;import com.authentication.server.model.AuthUser; import com.authentication.server.service.AuthUserService; import org.springframework.stereotype.Service;import java.util.ArrayList; import java.util.List;/*** 用戶服務實現類*/ @Service public class AuthUserServiceImpl implements AuthUserService {/*** 通過用戶賬號獲取用戶信息*/@Overridepublic AuthUser getAuthUserByUsername(String username) {/*** 實際上這里應該是從數據庫中查詢或者是調用其它服務接口獲取,* 為了方便,這里直接創建用戶信息* admin用戶擁有 ROLE_ADMIN 和 ROLE_EMPLOYEE 這兩個角色* employee用戶擁有 ROLE_EMPLOYEE 這個角色* temp用戶沒有角色*/if(username.equals("admin")) {AuthUser user = new AuthUser();user.setId(1L);user.setUsername("admin");/*** 密碼為123(通過BCryptPasswordEncoderl加密后的密文)*/user.setPassword("$2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS");List<String> roles = new ArrayList<>();roles.add("ROLE_ADMIN");roles.add("ROLE_EMPLOYEE");user.setRoles(roles);return user;} else if(username.equals("employee")) {AuthUser user = new AuthUser();user.setId(2L);user.setUsername("employee");/*** 密碼為123(通過BCryptPasswordEncoderl加密后的密文)*/user.setPassword("$2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS");List<String> roles = new ArrayList<>();roles.add("ROLE_EMPLOYEE");user.setRoles(roles);return user;} else if (username.equals("temp")) {AuthUser user = new AuthUser();user.setId(3L);user.setUsername("temp");/*** 密碼為123(通過BCryptPasswordEncoderl加密后的密文)*/user.setPassword("$2a$10$U6g06YmMfRJXcNfLP28TR.xy21u1A5kIeY/OZMKBDVMbn7PGJiaZS");List<String> roles = new ArrayList<>();user.setRoles(roles);return user;} else {return null;}}}用戶信息實體類如下所示:
package com.authentication.server.model;import java.util.List;/*** 用戶實體類*/ public class AuthUser {/** 用戶ID */private Long id;/** 用戶賬號 */private String username;/** 賬號密碼 */private String password;/** 角色集合 */private List<String> roles;public Long getId() {return id;}public void setId(Long id) {this.id = id;}public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}public List<String> getRoles() {return roles;}public void setRoles(List<String> roles) {this.roles = roles;} }用戶認證成功之后,可以通過@AuthenticationPrincipal注解來獲取認證用戶信息,本示例中獲取認證用戶信息的web入口類UserController的代碼如下所示:
package com.authentication.server.controller;import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController;import java.security.Principal;/*** 用戶接口類(返回JSON)*/ @RestController public class UserController {/*** 獲取登錄后的Principal(需要登錄)*/@GetMapping("/getPrincipal")public Object getPrincipal(@AuthenticationPrincipal Principal principal){return principal;}/*** 獲取登錄后的UserDetails(需要登錄)*/@GetMapping("/getUserDetails")public Object getUserDetails(@AuthenticationPrincipal UserDetails userDetails) {return userDetails;}}2.3 權限控制
Spring Security提供了默認的權限控制功能,需要預先分配給用戶特定的權限,并指定各項操作執行所要求的權限。用戶請求執行某項操作時,Spring Security會先檢查用戶所擁有的權限是否符合執行該項操作所要求的權限,如果符合,才允許執行該項操作,否則拒絕執行該項操作。
本示例中使用的是Spring Security提供的方法級別的權限控制,即根據權限來控制用戶是否能夠請求某個方法。首先,需要在工程的主啟動類中使用注解@EnableGlobalMethodSecurity(prePostEnabled = true)來啟動方法級別的權限控制,并指定是在方法執行之前進行權限驗證;然后,需要在方法的入口處通過注解@PreAuthorize()來指定執行對應方法需要什么樣的權限。
本示例的主啟動類AuthenticationServerApplication的代碼如下所示:
package com.authentication.server;import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.ComponentScan; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;/*** 主啟動類*/ @ComponentScan("com.authentication.server.*") @EnableGlobalMethodSecurity(prePostEnabled = true) @SpringBootApplication public class AuthenticationServerApplication {public static void main(String[] args) {SpringApplication.run(AuthenticationServerApplication.class, args);}}本示例中使用的是基于角色的權限控制,即驗證用戶所擁有的角色是否符合執行某個方法所需要的角色,如果符合,才允許執行該方法,否則拒絕執行該方法。需要在方法入口處通過注解 @PreAuthorize(“hasRole(‘角色名稱’)”)來指定執行對應方法需要什么角色,并且是在執行對應方法之前進行角色驗證。
本示例的方法入口控制類PageController的代碼如下所示:
package com.authentication.server.controller;import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping;/*** 頁面接口類(頁面跳轉)*/ @Controller public class PageController {/*** 跳轉到admin.html頁面(需要登錄,且需要ROLE_ADMIN角色)*/@GetMapping("/toAdmin")@PreAuthorize("hasRole('ROLE_ADMIN')")public String toAdmin() {return "admin.html";}/*** 跳轉到employee.html頁面(需要登錄,且需要ROLE_EMPLOYEE角色)*/@GetMapping("/toEmployee")@PreAuthorize("hasRole('ROLE_EMPLOYEE')")public String toEmployee() {return "employee.html";}/*** 跳轉到employee.html頁面(需要登錄,但不需要角色)* 注意:雖然WebSecurityConfig中配置了/toUser不需要登錄,但是這里配置的權限更小,因此,/toUser以這里的配置為準*/@GetMapping("/toUser")@PreAuthorize("isAuthenticated()")public String toUser() {return "user.html";}/*** 跳轉到home.html頁面(需要登錄,但不需要角色)* 注意:雖然這里配置了/toAbout不需要登錄,但WebSecurityConfig中配置的權限更小,因此,/toAbout以WebSecurityConfig中配置的為準*/@RequestMapping("/toAbout")@PreAuthorize("permitAll")public String toAbout() {return "about.html";}/*** 跳轉到home.html頁面(不需要登錄)*/@RequestMapping("/toHome")public String toHome() {return "home.html";}}靜態頁面admin.html的代碼如下所示:
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>admin頁面</title> </head> <body><h1>這是Admin頁面(需要登錄,且需要ROLE_ADMIN角色)</h1> </body> </html>靜態頁面employee.html、user.html、home.html、about.html的代碼與admin.html的相似,就不一一展示。
3 功能測試
3.1 用戶認證功能測試
運行AuthenticationServerApplication主啟動類以啟動項目,然后通過瀏覽器訪問以下地址請求toUser()方法(即訪問user.html頁面),由于toUser()方法需要用戶經過認證之后才能訪問,因此,會自動跳轉到用戶認證頁面(如下圖所示)進行認證:
http://localhost:8080/toUser
輸入用戶名admin和密碼123并點擊Sign in按鈕,認證成功后會自動請求/toHome路徑并跳轉到自定義的認證成功跳轉頁面home.hmtl(如下圖所示):
通過瀏覽器訪問以下地址可以查看到認證用戶信息如下圖所示:
3.2 “記住我” 功能測試
用戶進行認證時,如果勾選了用戶認證頁面中的Remember me on this computer選項,則當用戶關閉瀏覽器之后,系統會記住該用戶一段時間(由設置的有效期決定,本示例中是1800秒),如果在這段時間之內,當用戶重新訪問該系統時,用戶不需要重新進行認證就已經是已認證的狀態。
使用用戶名admin和密碼123進行用戶認證時勾選上用戶認證頁面中的Remember me on this computer選項,然后重復執行關閉瀏覽器后再訪問以下地址,并觀察關閉了瀏覽器之后,再重新訪問時是跳轉到認證頁面(未認證狀態)還是直接跳轉到了user.html頁面(已認證狀態):
http://localhost:8080/toUser3.3 退出功能測試
用戶認證成功之后,用戶就處于已認證的狀態,就可以在權限之內訪問系統,此時可以通過訪問以下地址請求退出已認證狀態:
http://localhost:8080/logout首先會跳轉到退出確認頁面(如下圖所示),用戶點擊了Log Out按鈕之后才會真正的執行退出操作,即回到未認證狀態。
3.4 權限控制功能測試
用戶在未認證的情況下通過瀏覽器訪問以下地址請求toAdmin()方法(即訪問admin.html頁面),由于該方法要求用戶已認證且具有ROLE_ADMIN權限才能訪問,因此會自動跳轉到用戶認證頁面。
http://localhost:8080/toAdmin此時如果用用戶名employee和密碼123進行認證,認證成功之后由于該用戶沒有ROLE_ADMIN權限,因此會自動跳轉到沒有權限的頁面(如下圖所示):
如果用用戶名admin和密碼123進行認證,認證成功之后由于該用戶擁有ROLE_ADMIN權限,因此會自動跳轉到admin.html頁面(如下圖所示):
可見,確實起到了權限控制的作用。
4 原理分析
4.1 用戶認證的過濾器鏈
Spring Security的用戶認證流程是由一系列的過濾器鏈來實現的,默認的關于用戶認證的過濾器鏈大致如下圖所示:
-
SecurityContextPersistenceFilter:?在請求開始時,從配置好的 SecurityContextRepository 中獲取 SecurityContext,并設置給 SecurityContextHolder。在請求完成后,把 SecurityContextHolder 所持有的SecurityContext 保存到配置好的 SecurityContextRepository,同時清除 securityContextHolder 所持有的 SecurityContext。
-
UsernamePasswordAuthenticationFilter:?用于處理來自表單提交的認證。該表單必須提供用戶名和密碼,其內部還有登錄成功或失敗后的處理器 AuthenticationSuccessHandler 和 AuthenticationFailureHandler。
-
ExceptionTranslationFilter:?能夠捕獲過濾器鏈中產生的所有異常,但只處理兩類異常:AuthenticationException 和 AccessDeniedException,而其它的異常則繼續拋出。
如果捕獲到的是 AuthenticationException,那么將會使用其對應的 AuthenticationEntryPoint 的commence()方法進行處理。在處理之前,ExceptionTranslationFilter會先使用 RequestCache 將當前的HttpServerletRequest的信息保存起來,以至于用戶登錄成功后可以跳轉到之前的界面。
如果捕獲到的是 AccessDeniedException,那么將會根據當前訪問的用戶是否已經登錄認證而做不同的處理,如果未登錄,則使用關聯的 AuthenticationEntryPoint 的 commence()方法進行處理,否則使用關聯的 AccessDeniedHandler 的handle()方法進行處理。 -
FilterSecurityInterceptor:?用于保護HTTP資源的,它需要一個 AuthenticationManager 和一個 AccessDecisionManager 的引用。它會從 SecurityContextHolder 中獲取 Authentication,然后通過 SecurityMetadataSource 可以得知當前請求是否在請求受保護的資源。對于請求那些受保護的資源,如果 Authentication.isAuthenticated() 返回false(即用戶未認證),或者FilterSecurityInterceptor 的 alwaysReauthenticate 屬性的值為 true,那么將會使用其引用的 AuthenticationManager 對Authentication進行認證,認證之后再使用認證后的 Authentication 替換 SecurityContextHolder 中原有的那個。然后使用 AccessDecisionManager 對用戶當前請求進行權限檢查。
4.2 用戶認證的流程
Spring Security支持多種用戶認證的方式,最常用的是基于用戶名和密碼的用戶認證方式,其認證流程如下圖所示:
4.3 “記住我” 功能的流程
用戶可以使用賬號和密碼進行認證,但是如果用戶使用賬號和密碼進行認證時選擇了“記住我”功能,則在有效期內,當用戶關閉瀏覽器后再重新訪問服務時,不需要用戶再次輸入賬號和密碼重新進行認證,而是通過“記住我”功能自動認證。
“記住我”功能的認證流程如下圖所示:
上述的用戶認證處理邏輯都是基于Spring Security提供的默認實現,我們只需要自己實現一個UserDetailsService接口用于獲取用戶認證信息即可,十分簡便。當然,Spring Security也能夠支持我們使用自定義的用戶認證處理邏輯,我們可以自己實現AuthenticationFilter和AuthenTicationProvider,以達到按照需求進行用戶認證的目的。博主的另外一篇文章會專門分享自定義用戶認證的實現。
4.4 權限控制的原理
Spring Security允許我們通過Spring EL權限驗證表達式來指定訪問URL或方法所需要的權限,用戶在訪問某個URL或方法時,如果對應的權限驗證表達式返回結果為true,則表示用戶擁有訪問該URL或方法的權限,如果返回結果為false,則表示沒有權限。Spring Security為我們提供了以下的權限驗證表達式:
| hasRole([role]) | 當前用戶是否擁有指定角色。 |
| hasAnyRole([role1,role2]) | 多個角色是一個以逗號進行分隔的字符串。如果當前用戶擁有指定角色中的任意一個則返回true。 |
| hasAuthority([auth]) | 等同于hasRole |
| hasAnyAuthority([auth1,auth2]) | 等同于hasAnyRole |
| Principle | 代表當前用戶的principle對象 |
| authentication | 直接從SecurityContext獲取的當前Authentication對象 |
| permitAll | 總是返回true,表示允許所有的 |
| denyAll | 總是返回false,表示拒絕所有的 |
| isAnonymous() | 當前用戶是否是一個匿名用戶 |
| isRememberMe() | 表示當前用戶是否是通過Remember-Me自動登錄的 |
| isAuthenticated() | 表示當前用戶是否已經登錄認證成功了。 |
| isFullyAuthenticated() | 如果當前用戶既不是一個匿名用戶,同時又不是通過Remember-Me自動登錄的,則返回true。 |
權限驗證表達式只能驗證用戶是否具有訪問某個URL或方法的權限,但是權限驗證的這個步驟可以在不同的階段進行。Spring Security中定義了以下四個支持使用權限驗證表達式的注解,其中前兩者可以用來在方法調用前或者調用后進行權限驗證,后兩者可以用來對集合類型的參數或者返回值進行過濾:
- @PreAuthorize
- @PostAuthorize
- @PreFilter
- @PostFilter
權限驗證表達式需要和注解結合使用,示例如下所示:
@PreAuthorize("hasRole('ROLE_ADMIN')") public void addUser(User user) {... }@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") public User find(int id) {return null; }@PreAuthorize("#id<10") public User find(int id) {return null; }@PreAuthorize("principal.username.equals(#username)") public User find(String username) {return null; }@PreAuthorize("#user.name.equals('abc')") public void add(User user) {... }@PostAuthorize("returnObject.id%2==0") public User find(int id) {...return user; }@PostFilter("filterObject.id%2==0") public List<User> findAll() {List<User> userList = new ArrayList<User>();...return userList; }@PreFilter(filterTarget="ids", value="filterObject%2==0") public void delete(List<Integer> ids, List<String> usernames) {... }5 總結
本文從使用方法和原理分析這兩個方法簡要的介紹了Spring Security的用戶認證和權限控制這兩大功能,但都是基于Spring Security的默認實現,我們也可以自定義用戶認證和權限控制的實現邏輯,Spring Security用戶認證和權限控制(自定義實現)詳細介紹了用戶認證相關和權限控制相關的自定義實現。關于授權服務器、資源服務器的內容可以查閱以下幾篇文章:
OAuth2授權服務器和四種授權方式?這篇文章介紹了授權服務器和四種授權方式的配置與使用方法。
OAuth2資源服務器?這篇文章介紹了基于方法級別的權限控制的資源服務器的配置與使用方法。
總結
以上是生活随笔為你收集整理的Spring Security用户认证和权限控制(默认实现)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php传递数据给jquery,将值从ph
- 下一篇: ajax将响应结果显示到iframe,J