在web應用開發中,安全無疑是十分重要的,選擇Spring Security來保護web應用是一個很是好的選擇。Spring Security 是spring項目之中的一個安全模塊,能夠很是方便與spring項目無縫集成。特別是在spring boot項目中加入spring security更是十分簡單。本篇咱們介紹spring security,以及spring security在web應用中的使用。前端
假設咱們如今建立好了一個springboot
的web應用,有一個控制器以下:java
@Controller public class AppController { @RequestMapping("/hello") @ResponseBody String home() { return "Hello ,spring security!"; } }
咱們啓動應用,假設端口是8080,那麼當咱們在瀏覽器訪問http://localhost:8080/hello
的時候能夠在瀏覽器看到Hello ,spring security!
。mysql
此時,/hello是能夠自由訪問。假設,咱們須要具備某個角色的用戶才能訪問的時候,咱們能夠引入spring security來進行保護。加入以下依賴,並重啓應用:react
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
再次訪問/hello
,咱們能夠獲得一個http-basic
的認證彈窗,以下:web
說明spring security 已經起做用了。若是咱們點擊取消,則會看到錯誤信息,以下所示:算法
There was an unexpected error (type=Unauthorized, status=401).
咱們在實際項目中不可能會使用,上面http-basic方式的彈窗來讓用戶完成登陸,而是會有一個登陸頁面。因此,咱們須要關閉http-basic的方式,關閉http-basic方式的認證彈窗的配置以下:spring
security.basic.enabled=false
spring security 默認提供了表單登陸的功能。咱們新建一個類SecurityConfiguration
,並加入一些代碼,以下所示:sql
@Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } }
上面的代碼其實就是 一種配置,authorizeRequests() 定義哪些URL須要被保護、哪些不須要被保護。 formLogin() 定義當須要用戶登陸時候,轉到的登陸頁面。此時,咱們並無寫登陸頁面,可是spring security默認提供了一個登陸頁面,以及登陸控制器。數據庫
加完了上面的配置類以後,咱們重啓應用。而後繼續訪問http://localhost:8080/hello。會發現自動跳轉到一個登陸頁面了,以下所示:
這個頁面是spring security 提供的默認的登陸頁面,其的html內容以下:
<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'> <h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'> <table> <tr><td>User:</td><td><input type='text' name='username' value=''></td></tr> <tr><td>Password:</td><td><input type='password' name='password'/></td></tr> <tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr> <input name="_csrf" type="hidden" value="635780a5-6853-4fcd-ba14-77db85dbd8bd" /> </table> </form></body></html>
咱們能夠發現,這裏有個form 。action="/login"
,這個/login
依然是spring security
提供的。form表單提交了三個數據:
爲了登陸系統,咱們須要知道用戶名密碼,spring security 默認的用戶名是user,spring security啓動的時候會生成默認密碼(在啓動日誌中能夠看到)。本例,咱們指定一個用戶名密碼,在配置文件中加入以下內容:
# security security.basic.enabled=false security.user.name=admin security.user.password=admin
重啓項目,訪問被保護的/hello頁面。自動跳轉到了spring security 默認的登陸頁面,咱們輸入用戶名admin密碼admin。點擊Login
按鈕。會發現登陸成功並跳轉到了/hello。除了登陸,spring security還提供了rememberMe功能,這裏不作過多解釋。
一般狀況下,咱們須要實現「特定資源只能由特定角色訪問」的功能。假設咱們的系統有以下兩個角色:
如今咱們給系統增長「/product」 表明商品信息方面的資源(USER能夠訪問);增長"/admin"代碼管理員方面的資源(USER不能訪問)。代碼以下:
@Controller @RequestMapping("/product") public class ProductTestController { @RequestMapping("/info") @ResponseBody public String productInfo(){ return " some product info "; } } ------------------------------------------- @Controller @RequestMapping("/admin") public class AdminTestController { @RequestMapping("/home") @ResponseBody public String productInfo(){ return " admin home page "; } }
在正式的應用中,咱們的用戶和角色是保存在數據庫中的;本例爲了方便演示,咱們來建立兩個存放於內存的用戶和角色。咱們在上一步中建立的SecurityConfiguration
中增長角色用戶,以下代碼:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin1") // 管理員,同事具備 ADMIN,USER權限,能夠訪問全部資源 .password("admin1") .roles("ADMIN", "USER") .and() .withUser("user1").password("user1") // 普通用戶,只能訪問 /product/** .roles("USER"); }
這裏,咱們增長了 管理員(admin1,密碼admin1),以及普通用戶(user1,密碼user1)
繼續增長「連接-角色」控制配置,代碼以下:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/product/**").hasRole("USER") .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); }
這個配置在上一步中登陸配置的基礎上增長了連接對應的角色配置。上面的配置,咱們能夠知道:
下面來驗證一下普通用戶登陸,重啓項目,在瀏覽器中輸入:http://localhost:8080/admin/home。一樣,咱們會到達登陸頁面,咱們輸入用戶名user1
,密碼也爲user1
結果錯誤頁面了,拒絕訪問了,信息爲:
There was an unexpected error (type=Forbidden, status=403). Access is denied
咱們把瀏覽器中的uri修改爲:/product/info
,結果訪問成功。能夠看到some product info
。說明 user1只能訪問 product/** ,這個結果與咱們預期一致。
再來驗證一下管理員用戶登陸,重啓瀏覽器以後,輸入http://localhost:8080/admin/home。在登陸頁面中輸入用戶名admin1,密碼admin1,提交以後,能夠看到admin home page
,說明訪問管理員資源了。咱們再將瀏覽器uri修改爲/product/info
,刷新以後,也能看到some product info
,說明 admin1用戶能夠訪問全部資源,這個也和咱們的預期一致。
上面咱們實現了「資源 - 角色」的訪問控制,效果和咱們預期的一致,可是並不直觀,咱們不妨嘗試在控制器中獲取「當前登陸用戶」的信息,直接輸出,看看效果。以/product/info爲例,咱們修改其代碼,以下:
@RequestMapping("/info") @ResponseBody public String productInfo(){ String currentUser = ""; Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if(principl instanceof UserDetails) { currentUser = ((UserDetails)principl).getUsername(); }else { currentUser = principl.toString(); } return " some product info,currentUser is: "+currentUser; }
這裏,咱們經過SecurityContextHolder
來獲取了用戶信息,並拼接成字符串輸出。重啓項目,在瀏覽器訪問http://localhost:8080/product/info. 使用 admin1的身份登陸,能夠看到瀏覽器顯示some product info,currentUser is: admin1
.
至此,咱們已經對spring security
有了一個基本的認識了。瞭解瞭如何在項目中加入spring security,以及如何控制資源的角色訪問控制。spring security原不止這麼簡單,咱們纔剛剛開始。爲了可以更好的在實戰中使用spring security 咱們須要更深刻的瞭解。下面咱們先來了解spring security的一些核心概念。
spring security核心組件有:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager,下面分別介紹。
安全上下文,用戶經過Spring Security 的校驗以後,驗證信息存儲在SecurityContext中,SecurityContext的接口定義以下:
public interface SecurityContext extends Serializable { /** * Obtains the currently authenticated principal, or an authentication request token. * * @return the <code>Authentication</code> or <code>null</code> if no authentication * information is available */ Authentication getAuthentication(); /** * Changes the currently authenticated principal, or removes the authentication * information. * * @param authentication the new <code>Authentication</code> token, or * <code>null</code> if no further authentication information should be stored */ void setAuthentication(Authentication authentication); }
能夠看到SecurityContext
接口只定義了兩個方法,實際上其主要做用就是獲取Authentication
對象。
SecurityContextHolder看名知義,是一個holder,用來hold住SecurityContext實例的。在典型的web應用程序中,用戶登陸一次,而後由其會話ID標識。服務器緩存持續時間會話的主體信息。在Spring Security中,在請求之間存儲SecurityContext
的責任落在SecurityContextPersistenceFilter
上,默認狀況下,該上下文將上下文存儲爲HTTP請求之間的HttpSession
屬性。它會爲每一個請求恢復上下文SecurityContextHolder
,而且最重要的是,在請求完成時清除SecurityContextHolder
。SecurityContextHolder是一個類,他的功能方法都是靜態的(static)。
SecurityContextHolder能夠設置指定JVM策略(SecurityContext的存儲策略),這個策略有三種:
SecurityContextHolder默認使用MODE_THREADLOCAL模式,即存儲在當前線程中。在spring security應用中,咱們一般能看到相似以下的代碼:
SecurityContextHolder.getContext().setAuthentication(token);
其做用就是存儲當前認證信息。
authentication 直譯過來是「認證」的意思,在Spring Security 中Authentication用來表示當前用戶是誰,通常來說你能夠理解爲authentication就是一組用戶名密碼信息。Authentication也是一個接口,其定義以下:
public interface Authentication extends Principal, Serializable { Collection<? extends GrantedAuthority> getAuthorities(); Object getCredentials(); Object getDetails(); Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
接口有4個get方法,分別獲取
Authorities
, 填充的是用戶角色信息。Credentials
,直譯,證書。填充的是密碼。Details
,用戶信息。Principal
直譯,形容詞是「主要的,最重要的」,名詞是「負責人,資本,本金」。感受很彆扭,因此,仍是不翻譯了,直接用原詞principal來表示這個概念,其填充的是用戶名。所以能夠推斷其實現類有這4個屬性。這幾個方法做用以下:
getAuthorities
: 獲取用戶權限,通常狀況下獲取到的是用戶的角色信息。getCredentials
: 獲取證實用戶認證的信息,一般狀況下獲取到的是密碼等信息。getDetails
: 獲取用戶的額外信息,(這部分信息能夠是咱們的用戶表中的信息)getPrincipal
: 獲取用戶身份信息,在未認證的狀況下獲取到的是用戶名,在已認證的狀況下獲取到的是 UserDetails (UserDetails也是一個接口,裏邊的方法有getUsername,getPassword等)。isAuthenticated
: 獲取當前 Authentication 是否已認證。setAuthenticated
: 設置當前 Authentication 是否已認證(true or false)。UserDetails,看命知義,是用戶信息的意思。其存儲的就是用戶信息,其定義以下:
public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
方法含義以下:
getAuthorites
:獲取用戶權限,本質上是用戶的角色信息。getPassword
: 獲取密碼。getUserName
: 獲取用戶名。isAccountNonExpired
: 帳戶是否過時。isAccountNonLocked
: 帳戶是否被鎖定。isCredentialsNonExpired
: 密碼是否過時。isEnabled
: 帳戶是否可用。提到了UserDetails
就必須得提到UserDetailsService
, UserDetailsService也是一個接口,且只有一個方法loadUserByUsername
,他能夠用來獲取UserDetails。
一般在spring security應用中,咱們會自定義一個CustomUserDetailsService來實現UserDetailsService接口,並實現其public UserDetails loadUserByUsername(final String login);
方法。咱們在實現loadUserByUsername
方法的時候,就能夠經過查詢數據庫(或者是緩存、或者是其餘的存儲形式)來獲取用戶信息,而後組裝成一個UserDetails
,(一般是一個org.springframework.security.core.userdetails.User
,它繼承自UserDetails) 並返回。
在實現loadUserByUsername
方法的時候,若是咱們經過查庫沒有查到相關記錄,須要拋出一個異常來告訴spring security來「善後」。這個異常是org.springframework.security.core.userdetails.UsernameNotFoundException
。
AuthenticationManager 是一個接口,它只有一個方法,接收參數爲Authentication
,其定義以下:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
AuthenticationManager 的做用就是校驗Authentication
,若是驗證失敗會拋出AuthenticationException
異常。AuthenticationException
是一個抽象類,所以代碼邏輯並不能實例化一個AuthenticationException異常並拋出,實際上拋出的異常一般是其實現類,如DisabledException
,LockedException
,BadCredentialsException
等。BadCredentialsException
可能會比較常見,即密碼錯誤的時候。
這裏,咱們只是簡單的瞭解了spring security中有哪些東西,先混個臉熟。這裏並不須要咱們一會兒全記住這些名詞和概念。先大概看看,有個印象。
在第一節中,咱們經過在pom文件中增長spring-boot-starter-security
依賴,便使得咱們的項目收到了spring security保護,又經過增長SecurityConfiguration
實現了一些安全配置,實現了連接資源的個性化訪問控制。那麼這是如何實現的呢?瞭解其原理,可使咱們使用起來駕輕就熟。
在spring security的官方文檔中,咱們能夠看到這麼一句話:
Spring Security’s web infrastructure is based entirely on standard servlet filters.
咱們能夠得知,spring security 在web應用中是基於filter的。filter咱們就很熟了,在沒有struts,沒有spring mvc以前,咱們就是經過一個個servlet,一個個filter來實現業務功能的,一般咱們會有多個filter,他們按序執行,一個執行完以後,調用filterChain中的下一個doFilter。Spring Security 在 Filter 中建立 Authentication 對象,並調用 AuthenticationManager 進行校驗
spring security 維護了一個filter chain,chain中的每個filter都具備特定的責任,並根據所需的服務在配置總添加。filter的順序很重要,由於他們之間存在依賴關係。spring security中有以下filter(按順序的):
SecurityContextHolder
中設置SecurityContext
,而且SecurityContext
的任何更改均可以複製到HttpSession
當web請求結束時(準備好與下一個web請求一塊兒使用)SecurityContextHolder
能夠修改成包含有效的Authentication
請求令牌SecurityContextHolder
AuthenticationEntryPoint
這裏咱們列舉了幾乎全部的spring security filter。正是這些filter完成了spring security的各類功能。目前咱們只是知道了有這些filter,並不清楚他們是怎麼集成到應用中的。在繼續深刻了解以前,咱們須要瞭解一下DelegatingFilterProxy
。
DelegatingFilterProxy
是一個特殊的filter,存在於spring-web模塊中。DelegatingFilterProxy
經過繼承GenericFilterBean
使得本身變爲了一個Filter(由於GenericFilterBean implements Filter)。它是一個Filter,其命名卻以proxy
結尾。很是有意思,爲了瞭解其功能,咱們看一下它的使用配置:
<filter> <filter-name>myFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>myFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
這個配置是咱們使用web.xml配置Filter時作法。可是與普通的Filter不一樣的是DelegatingFilterProxy
並無實際的過濾邏輯,他會嘗試尋找filter-name
節點所配置的myFilter
,並將過濾行爲委託給myFilter
來處理。這種方式可以利用Spring豐富的依賴注入工具和生命週期接口,所以DelegatingFilterProxy
提供了web.xml
與應用程序上下文之間的連接。很是有意思,能夠慢慢體會。
spring security的入口filter就是springSecurityFilterChain。在沒有spring boot以前,咱們要使用spring security的話,一般在web.xml中添加以下配置:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
看到沒,這裏配置的是DelegatingFilterProxy
。有了上面的介紹以後,咱們就知道,它實際上會去找到filter-name節點中的Filter——springSecurityFilterChain,並將實際的過濾工做交給springSecurityFilterChain
處理。
在使用spring boot以後,這一xml配置被Java類配置給代替了。咱們前面在代碼種使用過@EnableWebSecurity
註解,經過跟蹤源碼能夠發現@EnableWebSecurity
會加載WebSecurityConfiguration
類,而WebSecurityConfiguration
類中就有建立springSecurityFilterChain
這個Filter的代碼:
@Bean(name = {"springSecurityFilterChain"}) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty(); if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() { }); this.webSecurity.apply(adapter); } return (Filter)this.webSecurity.build(); }
這裏,咱們介紹了spring security的入口——springSecurityFilterChain,也介紹了它的兩種配置形式。可是,springSecurityFilterChain是誰,怎麼起做用的,咱們還不清楚,下面繼續看。
在spring的官方文檔中,咱們能夠發現這麼一句話:
Spring Security’s web infrastructure should only be used by delegating to an instance of
FilterChainProxy
. The security filters should not be used by themselves.spring security 的web基礎設施(上面介紹的那一堆filter)只能經過委託給
FilterChainProxy
實例的方式來使用。而不能直接使用那些安全filter。
這句話彷佛透漏了一個信號,上面說的入口springSecurityFilterChain
其實就是FilterChainProxy
,若是不信,調試一下 代碼也能發現,確實就是FilterChainProxy
。它的全路徑名稱是org.springframework.security.web.FilterChainProxy
。打開其源碼,第一行註釋是這樣:
Delegates {@code Filter} requests to a list of Spring-managed filter beans.
因此,沒錯了。它就是DelegatingFilterProxy
要找的人,它就是DelegatingFilterProxy
要委託過濾任務的人。下面貼出其部分代碼:
public class FilterChainProxy extends GenericFilterBean { private List<SecurityFilterChain> filterChains;// public FilterChainProxy(SecurityFilterChain chain) { this(Arrays.asList(chain)); } public FilterChainProxy(List<SecurityFilterChain> filterChains) { this.filterChains = filterChains; } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilterInternal(request, response, chain); } private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest fwRequest = firewall .getFirewalledRequest((HttpServletRequest) request); HttpServletResponse fwResponse = firewall .getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(fwRequest); if (filters == null || filters.size() == 0) { fwRequest.reset(); chain.doFilter(fwRequest, fwResponse); return; } VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters); vfc.doFilter(fwRequest, fwResponse); } private List<Filter> getFilters(HttpServletRequest request) { for (SecurityFilterChain chain : filterChains) { if (chain.matches(request)) { return chain.getFilters(); } } return null; } }
能夠看到,裏邊有個SecurityFilterChain
的集合。這個纔是衆多security filter藏身之處,doFilter的時候會從SecurityFilterChain取出第一個匹配的Filter集合並返回。
說到這裏,可能有點模糊了。這裏小結一下,梳理一下。
FilterChainProxy
(一個filter)FilterChainProxy
裏邊有一個SecurityFilterChain
集合,doFIlter的時候會從其中取。到這裏,思路清楚多了,如今還不知道SecurityFilterChain
是怎麼來的。下面介紹。
前面咱們介紹了springSecurityFilterChain,它是由xml配置的,或者是由@EnableWebSecurity
註解的做用下初始化的(@Import({WebSecurityConfiguration.class))。具體是在WebSecurityConfiguration類中。上面咱們貼過代碼,你能夠返回看,這裏再次貼出刪減版:
@Bean( name = {"springSecurityFilterChain"}) public Filter springSecurityFilterChain() throws Exception { // 刪除部分代碼 return (Filter)this.webSecurity.build(); }
最後一行,發現webSecurity.build()
產生了FilterChainProxy
。所以,推斷SecurityFilterChain就是webSecurity裏邊弄的。貼出源碼:
public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements SecurityBuilder<Filter>, ApplicationContextAware { @Override protected Filter performBuild() throws Exception { int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size(); // 咱們要找的 securityFilterChains List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>( chainSize); for (RequestMatcher ignoredRequest : ignoredRequests) { securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest)); } for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) { securityFilterChains.add(securityFilterChainBuilder.build()); } // 建立 FilterChainProxy ,傳入securityFilterChains FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains); if (httpFirewall != null) { filterChainProxy.setFirewall(httpFirewall); } filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy; postBuildAction.run(); return result; } }
至此,咱們清楚了,spring security 是怎麼在spring web應用中工做的了。具體的細節就是執行filter裏的代碼了,這裏再也不繼續深刻了。咱們的目的是摸清楚他是怎麼工做的,大體的脈路是怎樣,目前整理的內容已經達到這個目的了。
下面開始一些實戰使用spring security 的實例。依然依託開篇的例子,並在此基礎上調整。
開篇的例子中,咱們使用了內存用戶角色來演示登陸認證。可是實際項目咱們確定是經過數據庫完成的。實際項目中,咱們可能會有3張表:用戶表,角色表,用戶角色關聯表。固然,不一樣的系統會有不一樣的設計,不必定非得是這樣的三張表。本例演示的意義在於:若是咱們想在已有項目中增長spring security的話,就須要調整登陸了。主要是自定義UserDetailsService
,此外,可能還須要處理密碼的問題,由於spring並不知道咱們怎麼加密用戶登陸密碼的。這時,咱們可能須要自定義PasswordEncoder
,下面也會提到。
繼續完善開篇的項目,如今給項目添加spring-data-jpa
,並使用MySQL數據庫。所以在POM文件中加入以下配置:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
在application.properties文件中加入數據庫鏈接信息:
spring.datasource.url=jdbc:mysql://localhost:3306/yourDB?useUnicode=true&characterEncoding=UTF-8 spring.datasource.username=dbuser spring.datasource.password=****** spring.datasource.driver-class-name=com.mysql.jdbc.Driver
這裏,爲了簡單方便演示,咱們只建立一張表,字段以下:
@Entity public class User implements java.io.Serializable{ @Id @Column private Long id; @Column private String login; @Column private String password; @Column private String role; // 省略get set 等 }
而後咱們添加2條數據,以下:
id | login | password | role |
---|---|---|---|
1 | user1 | $2a$10$PhynBxXaIYdBzd/OgXrKzeVr3Bj4fiDxdii14fMOVIwJTqoDoFL1e |
USER |
2 | admin | $2a$10$PhynBxXaIYdBzd/OgXrKzeVr3Bj4fiDxdii14fMOVIwJTqoDoFL1e |
ADMIN |
密碼這裏都是使用了BCryptPasswordEncoder
需在SecurityConfiguration
中加入配置,後面會貼。
前面咱們提到過,UserDetailsService,spring security在認證過程當中須要查找用戶,會調用UserDetailsService的loadUserByUsername方法獲得一個UserDetails,下面咱們來實現他。代碼以下:
@Component("userDetailsService") public class CustomUserDetailsService implements UserDetailsService { @Autowired UserRepository userRepository; @Override public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException { // 1. 查詢用戶 User userFromDatabase = userRepository.findOneByLogin(login); if (userFromDatabase == null) { //log.warn("User: {} not found", login); throw new UsernameNotFoundException("User " + login + " was not found in db"); //這裏找不到必須拋異常 } // 2. 設置角色 Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>(); GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(userFromDatabase.getRole()); grantedAuthorities.add(grantedAuthority); return new org.springframework.security.core.userdetails.User(login, userFromDatabase.getPassword(), grantedAuthorities); } }
這個方法作了2件事情,查詢用戶以及設置角色,一般一個用戶會有多個角色,即上面的userFromDatabase.getRole()
一般是一個list,因此設置角色的時候,就是for循環new 多個SimpleGrantedAuthority並設置。(本例爲了簡單沒有設置角色表以及用戶角色關聯表,只在用戶中增長了一個角色字段,因此grantedAuthorities只有一個)
同時修改以前的SecurityConfiguration
,加入CustomUserDetailsService
bean配置,以下:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService)// 設置自定義的userDetailsService .passwordEncoder(passwordEncoder()); /*auth .inMemoryAuthentication() .withUser("admin1") .password("admin1") .roles("ADMIN", "USER") .and() .withUser("user1").password("user1") .roles("USER");*/ } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
上面咱們自定義了userDetailsService
,此時,spring security 在其做用流程中會調用,不出意外的話,重啓系統,咱們使用user1登陸能夠看到/product/info,可是不能看/admin/home。下面咱們來重啓項目驗證一下。
先輸入user1,以及錯誤密碼,結果以下:
再輸入user1 ,以及正確密碼,結果以下:
再將瀏覽器連接修改成/admin/home,結果顯示:
There was an unexpected error (type=Forbidden, status=403). Access is denied
這與咱們的預期徹底一致,至此,咱們已經在項目中加入了spring security,而且可以經過查詢數據庫用戶,角色信息交給spring security完成認證受權。
還記得咱們開篇所舉的例子嗎?咱們使用管理員帳號密碼登陸以後,就能夠訪問/admin/home了,此時修改瀏覽器地址欄爲/product/info以後刷新頁面,仍然能夠訪問,說明認證狀態被保持了;若是關閉瀏覽器從新輸入/admin/home就會提示咱們從新登陸,這有點session的感受。若是此時,咱們將瀏覽器cookie禁用掉,你會發現登陸以後自動跳轉只會獲得403,403是拒絕訪問的意思,是沒有權限的意思,說明這種狀況下受權狀態和session是掛鉤的。即這時spring security使用了session。可是不是全部的系統都須要session,咱們能讓spring security不適用session嗎?答案是能夠!
使用spring security 咱們能夠準確控制session什麼時候建立以及Spring Security如何與之交互:
這裏,咱們要關注的是 stateless,一般稱爲無狀態的。爲啥要關注這個stateless無狀態的狀況的呢?由於目前,咱們的應用基本都是先後端分離的應用。比方說,你的一套java api是給react前端、安卓端、IOS端 調用的。這個時候你還提什麼session啊,這時候咱們須要的是無狀態,一般以一種token的方式來交互。
spring security 配置stateless 的方式以下,依然是修改咱們以前定義的SecurityConfiguration
:
http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
上面咱們提到了stateless,實際中咱們的先後端分離項目都是無狀態的,並無登陸狀態保持,服務器經過客戶端調用傳遞的token來識別調用者是誰。
一般咱們的系統流程是這樣的:
若是咱們想在spring security項目中使用自定義的token,那麼咱們須要思考下面的問題:
下面從登陸發token開始,這裏須要使用到UsernamePasswordAuthenticationToken
,以及SecurityContextHolder
,代碼以下:
@RequestMapping(value = "/authenticate",method = RequestMethod.POST) public Token authorize(@RequestParam String username, @RequestParam String password) { // 1 建立UsernamePasswordAuthenticationToken UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password); // 2 認證 Authentication authentication = this.authenticationManager.authenticate(token); // 3 保存認證信息 SecurityContextHolder.getContext().setAuthentication(authentication); // 4 加載UserDetails UserDetails details = this.userDetailsService.loadUserByUsername(username); // 5 生成自定義token return tokenProvider.createToken(details); } @Inject private AuthenticationManager authenticationManager;
上面代碼中1,2,3,4步驟都是和spring security交互的。只有第5步是咱們本身定義的,這裏tokenProvider
就是咱們系統中token的生成方式(這個徹底是個性化的,一般是個加密串,一般可能會包含用戶信息,過時時間等)。其中的Token
也是咱們自定義的返回對象,其中包含token信息相似{"token":"abcd","expires":1234567890}
.
咱們的tokenProvider
一般至少具備兩個方法,即:生成token,驗證token。大體以下:
public class TokenProvider { private final String secretKey; private final int tokenValidity; public TokenProvider(String secretKey, int tokenValidity) { this.secretKey = secretKey; this.tokenValidity = tokenValidity; } // 生成token public Token createToken(UserDetails userDetails) { long expires = System.currentTimeMillis() + 1000L * tokenValidity; String token = computeSignature(userDetails, expires); return new Token(token, expires); } // 驗證token public boolean validateToken(String authToken, UserDetails userDetails) { check token return true or false; } // 從token中識別用戶 public String getUserNameFromToken(String authToken) { // …… return login; } public String computeSignature(UserDetails userDetails, long expires) { // 一些特有的信息組裝 ,並結合某種加密活摘要算法 return 例如 something+"|"+something2+MD5(s); } }
至此,咱們客戶端能夠經過調用http://host/context/authenticate
來得到一個token了,相似這樣的:{"token":"abcd","expires":1234567890}
。那麼下次請求的時候,咱們帶上 token=abcd
這個參數(或者也能夠是自定義的請求頭中)如何在spring security中復原「session」呢。咱們須要一個filter:
public class MyTokenFilter extends GenericFilterBean { private final Logger log = LoggerFactory.getLogger(XAuthTokenFilter.class); private final static String XAUTH_TOKEN_HEADER_NAME = "my-auth-token"; private UserDetailsService detailsService; private TokenProvider tokenProvider; public XAuthTokenFilter(UserDetailsService detailsService, TokenProvider tokenProvider) { this.detailsService = detailsService; this.tokenProvider = tokenProvider; } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { try { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; String authToken = httpServletRequest.getHeader(XAUTH_TOKEN_HEADER_NAME); if (StringUtils.hasText(authToken)) { // 從自定義tokenProvider中解析用戶 String username = this.tokenProvider.getUserNameFromToken(authToken); // 這裏仍然是調用咱們自定義的UserDetailsService,查庫,檢查用戶名是否存在, // 若是是僞造的token,可能DB中就找不到username這我的了,拋出異常,認證失敗 UserDetails details = this.detailsService.loadUserByUsername(username); if (this.tokenProvider.validateToken(authToken, details)) { log.debug(" validateToken ok..."); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(details, details.getPassword(), details.getAuthorities()); // 這裏仍是上面見過的,存放認證信息,若是沒有走這一步,下面的doFilter就會提示登陸了 SecurityContextHolder.getContext().setAuthentication(token); } } // 調用後續的Filter,若是上面的代碼邏輯未能復原「session」,SecurityContext中沒有想過信息,後面的流程會檢測出"須要登陸" filterChain.doFilter(servletRequest, servletResponse); } catch (Exception ex) { throw new RuntimeException(ex); } } }
目前爲止,咱們實現了自定義的token生成類,以及經過一個filter來攔截客戶端請求,解析其中的token,復原無狀態下的"session",讓當前請求處理線程中具備認證受權數據,後面的業務邏輯才能執行。下面,咱們須要將自定義的內容整合到spring security中。
首先編寫一個類,繼承SecurityConfigurerAdapter
:
public class MyAuthTokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private TokenProvider tokenProvider; // 咱們以前自定義的 token功能類 private UserDetailsService detailsService;// 也是我實現的UserDetailsService public MyAuthTokenConfigurer(UserDetailsService detailsService, TokenProvider tokenProvider) { this.detailsService = detailsService; this.tokenProvider = tokenProvider; } @Override public void configure(HttpSecurity http) throws Exception { MyAuthTokenFilter customFilter = new MyAuthTokenFilter(detailsService, tokenProvider); http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }
在 SecurityConfiguration
配置類中加入以下內容:
// 增長方法 private MyAuthTokenConfigurer securityConfigurerAdapter() { return new MyAuthTokenConfigurer(userDetailsService, tokenProvider); } // 依賴注入 @Inject private UserDetailsService userDetailsService; @Inject private TokenProvider tokenProvider; //方法修改 , 增長securityConfigurerAdapter @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() // .... 其餘配置 .and() .apply(securityConfigurerAdapter());// 這裏增長securityConfigurerAdapter }
至此咱們就完成了無狀態應用中token認證結合spring security。
本篇內容,咱們經過一個小例子開始介紹瞭如何給web應用引入spring security保護;在展現了http-basic驗證以後,咱們使用了內存用戶實驗了「角色-資源」訪問控制;而後咱們介紹了spring security的一些核心概念;以後咱們介紹了spring security 是經過filter的形式在web應用中發生做用的,並列舉了filter列表,介紹了入口filter,介紹了springboot是如何載入spring security入口filter的。最後咱們經過兩個實戰中的例子展現了spring security的使用。
spring security 功能也很是強大,可是仍是挺複雜的,本篇內容若有差錯還請指出。
參考文檔:
其餘推薦:
SpringMVC是怎麼工做的,SpringMVC的工做原理
Mybatis Mapper接口是如何找到實現類的-源碼分析