JWT設(shè)計原理 JWT結(jié)合spring-security在項目中的應(yīng)用
JWT譯文
- 什么是JWT
1. 開放標準
2. 數(shù)字簽名 支持HMAC屯蹦,RSA鳄梅,ECDSA加密
3. 驗簽可以保證token的完整性即當token內(nèi)容被篡改的時候可以通過驗簽發(fā)現(xiàn)
4. 當使用加密后可以保證token內(nèi)容不外泄状勤,僅持有私鑰的一方才能將token解開
- 什么時候用JWT
1. 鑒權(quán) 支持單點登錄 開銷小 方便跨域
2. 信息交換 JWT支持加密簽名 所以可以安全的傳遞信息 可做驗簽和解密驗證發(fā)送方是否可靠
- JWT的標準結(jié)構(gòu)應(yīng)該是什么樣的
1. JWT分為三段 頭信息 負載信息 簽名
2. 頭信息 通常由簽名算法+令牌類型組成
3. 中部有效負載
1. 推薦添加到期時間和主題等信息
2. 可以任意添加信息 但是注意如果非加密方式的token 建議token內(nèi)不要包含敏感信息 因為token是暴露在外的
4. 簽名 需要將頭信息和負載內(nèi)容一起做簽名 驗簽的時候可以避免信息被篡改
SPRING-SECURITY譯文
-
特性
- 支持身份驗證,授權(quán)低散,防范常見攻擊
- 支持集成
-
基礎(chǔ)組件
- SecurityContextHolder 存儲和獲取驗證后信息
SecurityContextHolder.getContext().getAuthentication();
- SecurityContext 從SecurityContextHolder中獲得的上下文信息 包含認證信息
- Authentication 不同階段的鑒權(quán)對象 如:鑒權(quán)后的當前登陸人或鑒權(quán)前的PreAuthenticatedAuthenticationToken(預處理攔截器先處理得到預處理token再調(diào)用AuthenticationManager得到最終token)
- GrantedAuthority 授予鑒權(quán)對象的權(quán)限 如:角色 范圍等
- AuthenticationManager 具體Filter如何執(zhí)行身份驗證的API
- ProviderManager 是AuthenticationManager的具體實現(xiàn)
- 首先實現(xiàn)AuthenticationProvider,注意里面的support方法 決定了Provider到底處理那種類型的Authentication,如上第二點所說Authentication 存在多種類型
public interface AuthenticationProvider { // ~ Methods // ======================================================================================================== /** * Performs authentication with the same contract as * {@link org.springframework.security.authentication.AuthenticationManager#authenticate(Authentication)} * . * * @param authentication the authentication request object. * * @return a fully authenticated object including credentials. May return * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support * authentication of the passed <code>Authentication</code> object. In such a case, * the next <code>AuthenticationProvider</code> that supports the presented * <code>Authentication</code> class will be tried. * * @throws AuthenticationException if authentication fails. */ Authentication authenticate(Authentication authentication) throws AuthenticationException; /** * Returns <code>true</code> if this <Code>AuthenticationProvider</code> supports the * indicated <Code>Authentication</code> object. * <p> * Returning <code>true</code> does not guarantee an * <code>AuthenticationProvider</code> will be able to authenticate the presented * instance of the <code>Authentication</code> class. It simply indicates it can * support closer evaluation of it. An <code>AuthenticationProvider</code> can still * return <code>null</code> from the {@link #authenticate(Authentication)} method to * indicate another <code>AuthenticationProvider</code> should be tried. * </p> * <p> * Selection of an <code>AuthenticationProvider</code> capable of performing * authentication is conducted at runtime the <code>ProviderManager</code>. * </p> * * @param authentication * * @return <code>true</code> if the implementation can more closely evaluate the * <code>Authentication</code> class presented */ boolean supports(Class<?> authentication); }
- 其次ProviderManager的authenticate會遍歷所有Provider(getProviders),然后找到上面提到的支持當前Authentication類型的Provider做處理
public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); }
AuthenticationProvider ProviderManager眾多執(zhí)行者中的一個,如上面所講,滿足類型的AuthenticationProvider將被執(zhí)行
AuthenticationEntryPoint 對于鑒權(quán)過程中如異常等響應(yīng)的統(tǒng)一處理
-
AbstractAuthenticationProcessingFilter
- 以UsernamePasswordAuthenticationFilter為例,主要是實現(xiàn)attemptAuthentication方法將request中的參數(shù)進行封裝坯癣,變?yōu)锳uthentication,再傳遞給下游AuthenticationManager
-
DaoAuthenticationProvider
- DaoAuthenticationProvider會從UserDetailsService中加載用戶信息最欠,然后與傳遞過來的用戶名密碼進行比較
//如何定義DaoAuthenticationProvider及注入UserDetailsService //繼承WebSecurityConfigurerAdapter并重寫configure方法 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //第一次登陸賬號密碼驗證Provider //默認使用BCryptPasswordEncoder比對加密后的密碼 daoProvider.setPasswordEncoder(); //驗證方法為spring-security內(nèi)部提供的DaoAuthenticationProvider.additionalAuthenticationChecks DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider(); daoProvider.setUserDetailsService(jwtUserDetailsService); daoProvider.setPasswordEncoder(new Md5PasswordEncoder()); //定義兩個Provider daoProvider負責UserNameAndPasswordToken登錄驗證 auth.authenticationProvider(daoProvider); }
UserDetailsService 獲取當前登錄用戶信息,實現(xiàn)UserDetailsService然后返回UserDetails
public interface UserDetailsService { // ~ Methods // ======================================================================================================== /** * Locates the user based on the username. In the actual implementation, the search * may possibly be case sensitive, or case insensitive depending on how the * implementation instance is configured. In this case, the <code>UserDetails</code> * object that comes back may have a username that is of a different case than what * was actually requested.. * * @param username the username identifying the user whose data is required. * * @return a fully populated user record (never <code>null</code>) * * @throws UsernameNotFoundException if the user could not be found or the user has no * GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
- FilterInvocationSecurityMetadataSource 為當前請求的URL打上一些標簽,如:當前的URL需要什么資源可以訪問,ConfigAttribute為接口可以自己定義實現(xiàn)
@Override public Collection<ConfigAttribute> getAttributes(Object o) { //FilterInvocation filterInvocation=Object o; 獲取當前request //當前URL的特殊標簽 //獲取什么資源可以允許當前request然后將資源id封裝后返回 } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { //全局標簽 return Collections.emptyList(); } @Override public boolean supports(Class<?> aClass) { //什么類型的請求可以走此封裝 return FilterInvocation.class.isAssignableFrom(aClass); }
- AccessDecisionManager 授權(quán)決策接口 跟FilterInvocationSecurityMetadataSource配套使用
//根據(jù)之前提到的AuthenticationManager封裝的Authentication中的角色信息及FilterInvocationSecurityMetadataSource中的請求標簽 判斷當前的角色是否有操作resourceIds的權(quán)限 public void decide(Authentication auth, Object o, Collection<ConfigAttribute> resourceIds)
//開啟自定義資源認證 //@EnableWebSecurity //public class WebSecurityConfig extends WebSecurityConfigurerAdapter @Override protected void configure(HttpSecurity http) throws Exception { //customMetadataSourceService每次請求根據(jù)數(shù)據(jù)庫配置讀取資源元信息及所需權(quán)限 并通過urlAccessDecisionManager與當前登錄人所包含的權(quán)限進行比對 http.authorizeRequests().withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(customMetadataSourceService); o.setAccessDecisionManager(urlAccessDecisionManager); return o; } }).anyRequest().permitAll()
- AuthenticationSuccessHandler 請求成功后處理 這個就不詳細介紹了
-
認證機制
- 因為我們主要說JWT所以簡單說一下 Pre-Authentication Scenarios 當已經(jīng)做了外部鑒權(quán)示罗,到spring-security直接可用,即預驗證場景
- 首先需要實現(xiàn)AbstractPreAuthenticatedProcessingFilter芝硬,這里主要是實現(xiàn)方法getPreAuthenticatedPrincipal蚜点,從request中獲取預授權(quán)信息
- setCheckForPrincipalChanges(true),用來保證security上下文發(fā)生變更時候會走此預授權(quán)
- AbstractPreAuthenticatedProcessingFilter內(nèi)部會將principal封裝成PreAuthenticatedAuthenticationToken(Authentication)并傳遞給下游AuthenticationManager
- AuthenticationManager完成驗證并返回實際Authentication將會存在SecurityContextHolder中便于在系統(tǒng)中獲取當前人員
- 因為我們主要說JWT所以簡單說一下 Pre-Authentication Scenarios 當已經(jīng)做了外部鑒權(quán)示罗,到spring-security直接可用,即預驗證場景
-
上圖 圖1是普通登錄生成token的過程 圖2為使用token進行鑒權(quán)的過程
-
對于JWT實現(xiàn)方式的一些探討 能否借助redis做密鑰生成 滿足自動過期和僅允許一人登錄 答案是可以的 下面就分幾步簡單介紹一下
- header和payload不做探討了 就是標準結(jié)構(gòu) 兩個JSON 且不包含敏感信息
- 首先根據(jù)用戶名+UUID(或任意比較復雜的隨機方案) 生成一個當前用戶的secret 并將secret保存在redis 如JWT_AAA_SEC=***
- 然后將header和payload+secret通過hmacSha256Base64做一個簽名為sign token為base64 header . base64 payload . sign
- 當有請求時 首先根據(jù)username從redis中獲取secret
- 然后重復3中步驟生成sign并與當前token的sign做比較 如果不一致驗簽失敗
- 那重復登錄踢出和自動過期的實現(xiàn)方式就很顯然了 不詳細說了