SpringCloud+Spring Security+OAuth2 + JWT + Gateway講解

項目簡介

  • 本登錄系統(tǒng)是一個適應(yīng)前后端分離并支持傳統(tǒng)PC登錄的全方位微服務(wù)登錄架構(gòu)
  • 基于Spring Boot 2.2.8.、 Spring Cloud Hoxton.SR5 和 Spring Cloud Alibaba 2.2.1
  • 深度定制Spring Security,基于RBAC(暫未實現(xiàn))、jwt和oauth2的無狀態(tài)統(tǒng)一權(quán)限認(rèn)證的
  • 單點登錄、單點登出(JWT方式已實現(xiàn))、續(xù)簽等功能(JWT方式已實現(xiàn)
  • 提供C端多租戶功能(暫未實現(xiàn)
  • 提供第三方被授權(quán)登錄方式(openId方式)
  • 提供供內(nèi)部服務(wù)調(diào)用的OAuth2客戶端模式
  • 提供基于OAuth2的第三方授權(quán)碼模式
  • 提供自定義添加OAuth2的四種模式的擴展
  • 統(tǒng)一角色權(quán)限校驗

實現(xiàn)思路

1.基于Spring Security源碼
Spring Security過濾器鏈

所有的請求首先會到 AbstractAuthenticationProcessingFilter 中,并調(diào)用doFilter方法,該過濾器會判斷用戶是否需要登錄,如果不登錄直接返回。如果需要登錄,則調(diào)用attemptAuthentication判斷自定義攔截器,如果存在自定義攔截器,則會調(diào)用子類的該方法,用于用戶名、密碼登錄。否則會進(jìn)UsernamePasswordAuthenticationFilter。

UsernamePasswordAuthenticationFilter 主要負(fù)責(zé)登錄請求(包含表單等),請求首先回到 attemptAuthentication 方法中,判斷用戶名和密碼是否是空,如果不是,就去構(gòu)建 UsernamePasswordAuthenticationToken 對象。

UsernamePasswordAuthenticationToken 是 Authentication 接口的其中一個實現(xiàn),如果第一次進(jìn)入(這時還沒有執(zhí)行權(quán)限認(rèn)證),該構(gòu)造方法首先會把用戶名、密碼設(shè)置到本地,然后會賦予權(quán)限一個空權(quán)限,然后會 setAuthenticated,因為這時還沒有執(zhí)行權(quán)限,所以會設(shè)置為false。

然后回到 UsernamePasswordAuthenticationFilter ,執(zhí)行了 setDetails 方法,把request信息和 UsernamePasswordAuthenticationToken 對象塞入進(jìn)去。

UsernamePasswordAuthenticationFilter 最后一步會進(jìn)去 AuthenticationManager(也就是如上圖第二步所示),它本身沒什么作用,主要用來管理多個AuthenticationProvider,因為本身登錄方式有多種,比如用戶名密碼登錄,第三方登錄等區(qū)別,并判斷當(dāng)前請求是否支持當(dāng)前AuthenticationProvider,如果支持,則執(zhí)行真正的校驗邏輯,會調(diào)用 authenticate 方法,該方法默認(rèn)從AbstractUserDetailsAuthenticationProvider 實現(xiàn),該類的 authenticate 方法會調(diào)用 retrieveUser 方式,該方式是一個抽象方法,由 DaoAuthenticationProvider 實現(xiàn)。

劃重點:真正的校驗邏輯就在這個方法內(nèi),而最終會拿到 getUserDetailsService的loadUserByUsername方法,這個方法就是我們自己程序要實現(xiàn)的用戶校驗邏輯。

在上步拿到UserDetails后,AbstractUserDetailsAuthenticationProvider 把拿到的UserDetails校驗,調(diào)用preAuthenticationChecks.check(UserDetails)方法,會進(jìn)行一些預(yù)檢查,會判斷用戶是否鎖定,是否過期等操作。在做完預(yù)檢查后,會調(diào)用additionalAuthenticationChecks 進(jìn)行一些附加檢查,該方法是一個抽象類,由DaoAuthenticationProvider 實現(xiàn),主要是對密碼的校驗(PasswordEncoder)。上面檢查都做完后,return this.createSuccessAuthentication(),至此,整個AbstractUserDetailsAuthenticationProvider 認(rèn)證流程走完。

上面說到 createSuccessAuthentication(),該方法會重新(重點:記住是重新構(gòu)造UsernamePasswordAuthenticationToken,上面調(diào)過一次)構(gòu)造UsernamePasswordAuthenticationToken方法,把username、password和權(quán)限重新塞回去。

至此,整個調(diào)用鏈結(jié)束,然后Authentication會沿著剛才的調(diào)用鏈返回回去,然后又回到 UsernamePasswordAuthenticationFilter,拿到用戶名、密碼那。最終回到最開始的 AbstractAuthenticationProcessingFilter 過濾器 ,AbstractAuthenticationProcessingFilter.doFilter 成功結(jié)束return

PS:this.successfulAuthentication(request, response, chain, authResult),該方法會調(diào)用我們自定義的successfulHandler處理器(重點:在OAuth2中,會接手successfulHandler,然后返回token,因此在OAuth2不需要寫該handler),successfulAuthentication 方法會把登錄成功用戶信息存到ThreadLocal中,全局共享。

PS:上面所有流程中任何一處出現(xiàn)錯誤或者異常則會掉unsuccessfulAuthentication方法,該方法會調(diào)用自定義的failureHandler把存入的用戶信息從ThreadLocal中清除。

2.Security多請求共享
session共享

如上圖所示,其實上面最后一步在 successfulAuthenticationunsuccessfulAuthentication 就能看到 SecurityContext,它的作用是把 Authentication 包裝起來,而 SecurityContextHolder 則是一個ThreadLocal封裝,封裝 SecurityContext

security過濾器鏈

SecurityContextPersistenceFilter 是過濾鏈上第一個過濾器,所有請求都先過它,響應(yīng)最后過它,它的作用是:
請求過來后,檢查session,判斷session是否有
SecurityContext,如果有就把SecurityContext拿出來放到線程里;當(dāng)整個響應(yīng)回來最后一個過它的時候,它檢查線程,如果線程里有SecurityContext,就拿出來放到Session里,因為整個響應(yīng)過程是在一個線程里的,在線程其他位置隨時可以拿到用戶信息。

Oauth2.0

客戶端必須得到用戶的授權(quán)(authorization grant),才能獲得令牌(access token)

OAuth 2.0定義了四種授權(quán)方式:
  • 授權(quán)碼模式(authorization code)
  • 簡化模式(implicit)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(client credentials)

而它默認(rèn)的授權(quán)服務(wù)接口是:

/oauth/authorize:驗證接口, AuthorizationEndpoint
/oauth/token:獲取token
/oauth/confirm_access:用戶授權(quán)
/oauth/error:認(rèn)證失敗
/oauth/check_token:資源服務(wù)器用來校驗token
/oauth/token_key:jwt模式下獲取公鑰;位于:TokenKeyEndpoint ,通過 JwtAccessTokenConverter 訪問key

JWT

簡述

客戶端身份經(jīng)過服務(wù)器驗證通過后,會生成帶有簽名的 JSON 對象并將它返回給客戶端??蛻舳嗽谑盏竭@個 JSON 對象后存儲起來。

在以后的請求中客戶端將 JSON 對象連同請求內(nèi)容一起發(fā)送給服務(wù)器,服務(wù)器收到請求后通過 JSON 對象標(biāo)識用戶,如果驗證不通過則不返回請求的數(shù)據(jù)。

驗證不通過的情況有很多,比如簽名不正確、無權(quán)限等。在 JWT 中服務(wù)器不保存任何會話數(shù)據(jù),使得服務(wù)器更加容易擴展。

Base64URL 算法

在講解 JWT 的組成結(jié)構(gòu)前我們先來講解一下 Base64URL 算法。這個算法和 Base64 算法類似,但是有一點區(qū)別。

我們通過名字可以得知這個算法使用于 URL 的,因此它將 Base64 中的 + 、 / 、 = 三個字符替換成了 - 、 _ ,刪除掉了 = 。因為這個三個字符在 URL 中有特殊含義。

JWT 組成結(jié)構(gòu)

JWT 是由三段字符串和兩個 . 組成,每個字符串和字符串之間沒有換行(類似于這樣:xxxxxx.yyyyyy.zzzzzz),每個字符串代表了不同的功能,我們將這三個字符串的功能按順序列出來并講解:

1. JWT 頭

JWT 頭描述了 JWT 元數(shù)據(jù),是一個 JSON 對象,它的格式如下:

json{"alg":"HS256","typ":"JWT"}

這里的 alg 屬性表示簽名所使用的算法,JWT 簽名默認(rèn)的算法為 HMAC SHA256 , alg 屬性值 HS256 就是 HMAC SHA256 算法。typ 屬性表示令牌類型,這里就是 JWT。

2. 有效載荷

有效載荷是 JWT 的主體,同樣也是個 JSON 對象。有效載荷包含三個部分:

標(biāo)準(zhǔn)注冊聲明
標(biāo)準(zhǔn)注冊聲明不是強制使用是的,但是我建議使用。它一般包括以下內(nèi)容:

iss:jwt的簽發(fā)者/發(fā)行人;

sub:主題;

aud:接收方;

exp:jwt過期時間;

nbf:jwt生效時間;

iat:簽發(fā)時間

jti:jwt唯一身份標(biāo)識,可以避免重放攻擊

公共聲明:
可以在公共聲明添加任何信息,我們一般會在里面添加用戶信息和業(yè)務(wù)信息,但是不建議添加敏感信息,因為公共聲明部分可以在客戶端解密。

私有聲明:
私有聲明是服務(wù)器和客戶端共同定義的聲明,同樣這里不建議添加敏感信息。

下面這個代碼段就是定義了一個有效載荷:

json{"exp":"201909181230","role":"admin","isShow":false}

3. 哈希簽名

哈希簽名的算法主要是確保數(shù)據(jù)不會被篡改。它主要是對前面所講的兩個部分進(jìn)行簽名,通過 JWT 頭定義的算法生成哈希。哈希簽名的過程如下:

  1. 指定密碼,密碼保存在服務(wù)器中,不能向客戶端公開;

  2. 使用 JWT 頭指定的算法進(jìn)行簽名,進(jìn)行簽名前需要對 JWT 頭和有效載荷進(jìn)行 Base64URL 編碼,JWT 頭和郵箱載荷編碼后的結(jié)果之間需要用 . 來連接。

簡單示例如下:

HMACSHA256(base64UrlEncode(JWT 頭) + "." + base64UrlEncode(有效載荷),密碼)

最終結(jié)果如下:

base64UrlEncode(JWT 頭)+"."+base64UrlEncode(有效載荷)+"."+HMACSHA256(base64UrlEncode(JWT 頭) + "." + base64UrlEncode(有效載荷),密碼)

JWT 注意事項

在使用 JWT 時需要注意以下事項:

  1. JWT 默認(rèn)不加密,如果要寫入敏感信息必須加密,可以用生成的原始令牌再次對內(nèi)容進(jìn)行加密;

  2. JWT 無法使服務(wù)器保存會話狀態(tài),當(dāng)令牌生成后在有效期內(nèi)無法取消也不能更改;

  3. JWT 包含認(rèn)證信息,如果泄露了,任何人都可以獲得令牌所有的權(quán)限;因此 JWT 有效期不能太長,對于重要操作每次請求都必須進(jìn)行身份驗證。

認(rèn)證中心(uaa-server)

本項目深度定制了OAuth2.0,擴展了部分OAuth2部分接口,原因是現(xiàn)有業(yè)務(wù)不支持,原生接口只有傳用戶名、密碼,而本公司業(yè)務(wù)除了用戶名、密碼還有買家多租戶方式,因此不太適合,比如重寫了如密碼模式獲取token、刷新token(暫未完成)、客戶端模式獲取token、openId獲取token等方式,這些都是原生沒有的。

其次如果用JWT這種獲取token方式是要加密的,默認(rèn)可以使用對稱加密,也可以是用非對稱加密,本項目使用非對稱加密方式,因為非對稱加密幾乎不可能被破解,私鑰存在認(rèn)證中心,公鑰存在各資源服務(wù)器上。

JWT的RSA非對稱密鑰生成

1.生成密鑰文件
使用jdk自帶的keytool工具,執(zhí)行后會在當(dāng)前目錄生成fzp-jwt.jks(存在uaa資源目錄下)密鑰文件

keytool -genkey -alias dhgate -keyalg RSA -storetype PKCS12 -keysize 1024 -keystore fzp-jwt.jks

參數(shù)解析

-genkey:創(chuàng)建證書
-alias:證書的別名。在一個證書庫文件中,別名是唯一用來區(qū)分多個證書的標(biāo)識符
-keyalg:密鑰的算法,非對稱加密的話就是RSA
-keystore:證書庫文件保存的位置和文件名。如果路徑寫錯的話,會出現(xiàn)報錯信息。如果在路徑下,證書庫文件不存在,那么就會創(chuàng)建一個
-keysize:密鑰長度,一般都是1024
-validity:證書的有效期,單位是天。比如36500的話,就是100年

2.提取公鑰

keytool -list -rfc -keystore fzp-jwt.jks -storepass libaojun@dhgate.com | openssl x509 -inform pem -pubkey

公私鑰

參數(shù)解析

-keystore:密鑰文件
-storepass:密鑰密碼

用戶名密碼獲取token(/oauth/user/token)

該接口后期需要優(yōu)化,原因是前期業(yè)務(wù)路線變化,前期實現(xiàn)給自己挖坑,也是最開始討論所有認(rèn)證中心+網(wǎng)關(guān)拆分,導(dǎo)致后面多了C端用戶,并在C端用戶上擴展多租戶讓原本的用戶名、密碼登錄方式變成了各種填坑,因此也是后面要做的拆分。預(yù)期是不做認(rèn)證中心拆分,而達(dá)到多租戶效果。

代碼邏輯:

    /**
     * Oauth2 密碼模式
     * @param passwordLoginParamDto
     * @param request
     * @param response
     * @throws IOException
     */
    @ApiOperation(value = "用戶名密碼獲取token")
    @RequestMapping(value = "/oauth/user/token", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    public void userTokenInfo(@RequestBody PasswordLoginParamDto passwordLoginParamDto,
                              HttpServletRequest request, HttpServletResponse response) throws IOException {
        String userName = "";
        if(passwordLoginParamDto.getUserType() == 1){
            userName = String.format("%s,%s", passwordLoginParamDto.getUsername(), passwordLoginParamDto.getUserType());
        }else{
            userName = String.format("%s;%s,%s", passwordLoginParamDto.getUsername(), passwordLoginParamDto.getShopId(), passwordLoginParamDto.getUserType());
        }

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, passwordLoginParamDto.getPassword());
        TokenTransferDto dto = getTokenTransferDto("username or password error!");
        dto.request = request;
        dto.response = response;
        dto.token = token;
        writerDefaultToken(dto);
    }
image.png

1.將用戶名、密碼和用戶類型(如果是C端用戶還得傳shopId)傳入UsernamePasswordAuthenticationToken(這個類作用和實現(xiàn)原理在前面Security有提到),然后會校驗用戶名、密碼等信息,校驗邏輯可以是數(shù)據(jù)庫,也可以是內(nèi)存。
2.構(gòu)造TokenTransferDto默認(rèn)dto,把錯誤信息、request、response、UsernamePasswordAuthenticationToken等構(gòu)造進(jìn)去。
3.指定模式為密碼模式,獲取clientId和clientSecret(默認(rèn)內(nèi)存獲取,已實現(xiàn)從數(shù)據(jù)庫獲取,并加入緩存),根據(jù)clientId拿到ClientDetails(ClientDetails在AuthorizationServerConfig.configure去刷到緩存中),拿到ClientDetails的目的是,OAuth2默認(rèn)需要Basic驗證,不加會報如下錯誤:

ClientDetails

4.根據(jù)附加參數(shù)(可選,刷新必須傳)、ClientId、Scope和當(dāng)前使用的OAuth2模式來構(gòu)造TokenRequest,而TokenRequest的作用是:在隱式流中,令牌通過AuthorizationEndpoint直接請求,在這種情況下,AuthorizationRequest被轉(zhuǎn)換為TokenRequest,以便通過令牌授予鏈進(jìn)行處理。
5.TokenRequest根據(jù)clientSecret、grant_type、password和GrantedAuthority去構(gòu)造OAuth2Request。
6.根據(jù)OAuth2RequestAuthentication去構(gòu)造OAuth2Authentication,然后通過OAuth2Authentication最終去創(chuàng)造AccessToken。至此,密碼登錄方式完成。效果如下:
密碼模式token

clientId獲取token(/oauth/client/token)

此登錄方式適合服務(wù)間內(nèi)部調(diào)用登錄用,安全性比較低(但比簡單模式高),因為不用傳入用戶名、密碼等信息,就可以拿到登錄信息

    /**
     * Oauth2 客戶端模式
     * @param request
     * @param response
     * @throws IOException
     */
    @ApiOperation(value = "clientId獲取token")
    @PostMapping("/oauth/client/token")
    public void clientTokenInfo(HttpServletRequest request, HttpServletResponse response)throws IOException {
        TokenTransferDto dto = getTokenTransferDto("clientId or secret error.");
        dto.request = request;
        dto.response = response;
        writerClientToken(dto);
    }

實現(xiàn)方式跟密碼模式基本一致,請查看密碼模式方式。該結(jié)果只能拿到userId,并不能拿到其他信息,返回結(jié)果如下:


客戶端模式
openId獲取token(/oauth/openId/token)

openId這種登錄方式比較特別,因為它不在OAuth2的四種模式之內(nèi),但查看源碼能看到可以依靠OAuth2提供的擴展實現(xiàn)該方式,詳情源碼可以參照UsernamePasswordAuthenticationToken,這在前面的Security已經(jīng)有介紹。

@ApiOperation(value = "openId獲取token")
    @PostMapping("/oauth/openId/token")
    public void getTokenByOpenId(
            @RequestBody OpenIdLoginParamDto openIdLoginParamDto,
            HttpServletRequest request, HttpServletResponse response) throws IOException {
        OpenIdAuthenticationToken token = new OpenIdAuthenticationToken(openIdLoginParamDto.getOpenId());
        TokenTransferDto dto = getTokenTransferDto("openId error!");
        dto.request = request;
        dto.response = response;
        dto.token = token;
        writerDefaultToken(dto);
    }

1.首先要實現(xiàn)一個類似于UsernamePasswordAuthenticationToken的token類(請看源碼),去生成要傳入的參數(shù)。
然后再實現(xiàn)AuthenticationProvider接口,該接口作用主要是生成UserDetails,然后把參數(shù)傳入UsernamePasswordAuthenticationToken并構(gòu)造:

/**
 * @author lbj
 */
public class OpenIdAuthenticationProvider implements AuthenticationProvider {

    private MySocialUserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;
        String openId = (String) authenticationToken.getPrincipal();
        UserDetails user = userDetailsService.loadUserByOpenId(openId);
        if (user == null) {
            throw new InternalAuthenticationServiceException("openId result user is null.");
        }
        OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(user, user.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public MySocialUserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(MySocialUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

然后再創(chuàng)建OpenIdAuthenticationSecurityConfig來加入Security的默認(rèn)支持方式

/**
 * openId的相關(guān)處理配置
 *
 * @author lbj
 */
@Component
public class OpenIdAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private MySocialUserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) {
        //openId provider
        OpenIdAuthenticationProvider provider = new OpenIdAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        http.authenticationProvider(provider);
    }
}

最后在Security的攔截類加入自定義的登錄方式

@Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable().httpBasic().disable().authorizeRequests().anyRequest().permitAll()
        .and().apply(openIdAuthenticationSecurityConfig); //就是這里對OpenId生效

        // 基于密碼 等模式可以無session,不支持授權(quán)碼模式
        if (authenticationEntryPoint != null) {
            http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
                    .accessDeniedHandler(customAccessDeniedHandler);
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        } else {
            // 授權(quán)碼模式單獨處理,需要session的支持,此模式可以支持所有oauth2的認(rèn)證
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        }

        // 解決不允許顯示在iframe的問題
        http.headers().frameOptions().disable();
        http.headers().cacheControl();

    }

資源中心

各資源服務(wù)器需要實現(xiàn)ResourceServerConfigurerAdapter,代表開啟一個資源服務(wù)器,再配置下各資源服務(wù)引入的token方式,配置為resJwt的都會被標(biāo)記為資源服務(wù)器,配置如下:

dhgate:
  oauth2:
    token:
      store:
        type: resJwt

前端傳入token后,各資源服務(wù)器會去解析當(dāng)前token(網(wǎng)關(guān)會校驗當(dāng)前登錄有效性),解析方式在各資源服務(wù)器的資源文件中(public.cert),如果公鑰驗證成功,會進(jìn)入DefaultUserAuthenticationConverter

如果當(dāng)前用戶已登錄,則會去DefaultUserAuthenticationConverter中去獲取用戶的登錄信息,然后塞入ThreadLocal中

public class JWTfaultUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
            private Collection<? extends GrantedAuthority> defaultAuthorities;

            public Authentication extractAuthentication(Map<String, ?> map) {
                if (map.containsKey("user_info")) {
                    Object principal = map.get("user_info");
                //  Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
                    LoginAppUser loginUser = new LoginAppUser();
                    if (principal instanceof Map) {
                        loginUser = BeanUtil.mapToBean((Map) principal, LoginAppUser.class, true);
                    }
                    return new UsernamePasswordAuthenticationToken(loginUser, "N/A", loginUser.getAuthorities());
                }
                return null;
            }
        }

因為SecurityContext在當(dāng)前線程全局有效,所以登錄信息可以在資源服務(wù)器任何一個地方拿到

/**
 * @author 作者 lbj
 * @version 創(chuàng)建時間:2020年07月01日 上午20:57:51 獲取用戶信息
 */
public class SysUserUtil {

    /**
     * 獲取登陸的 LoginAppUser
     *
     * @return
     */
    public static LoginAppUser getLoginAppUser() {
        
        // 當(dāng)OAuth2AuthenticationProcessingFilter設(shè)置當(dāng)前登錄時,直接返回
        // 強認(rèn)證時處理
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication instanceof OAuth2Authentication) {
            OAuth2Authentication oAuth2Auth = (OAuth2Authentication) authentication;
            authentication = oAuth2Auth.getUserAuthentication();

            if (authentication instanceof UsernamePasswordAuthenticationToken) {
                UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken) authentication;

                if (authenticationToken.getPrincipal() instanceof LoginAppUser) {
                    return (LoginAppUser) authenticationToken.getPrincipal();
                } else if (authenticationToken.getPrincipal() instanceof Map) {

                    LoginAppUser loginAppUser = BeanUtil.mapToBean((Map) authenticationToken.getPrincipal(), LoginAppUser.class, true);
                    return loginAppUser;
                }
            } else if (authentication instanceof PreAuthenticatedAuthenticationToken) {
                // 刷新token方式
                PreAuthenticatedAuthenticationToken authenticationToken = (PreAuthenticatedAuthenticationToken) authentication;
                return (LoginAppUser) authenticationToken.getPrincipal();
            }
        }
       
        return null;
    }
}

登錄信息已經(jīng)存到了本地線程變量中,因此上面代碼能在資源服務(wù)器任何地方拿到用戶登錄信息。

Gateway

網(wǎng)關(guān)在本項目中的角色是對來自認(rèn)證中心的服務(wù)鑒權(quán)、過濾地址、服務(wù)轉(zhuǎn)發(fā)等功能(后續(xù)功能添加中,比如全局過濾,局部過濾)

網(wǎng)關(guān)鑒權(quán)

要實現(xiàn)網(wǎng)關(guān)鑒權(quán),則網(wǎng)關(guān)必須得標(biāo)記為資源服務(wù)器,因網(wǎng)關(guān)使用webflux異步非阻塞原理實現(xiàn),底層服務(wù)器是基于netty,不向下兼容,不兼容普通web相關(guān),所以網(wǎng)關(guān)得單獨實現(xiàn)一套資源服務(wù)認(rèn)證。資源認(rèn)證流程如下:

/**
 * webflux資源服務(wù)器配置
 *
 * @author lbj
 * @date 2020/07/02
 */
@EnableConfigurationProperties(SecurityProperties.class)
public class ResourceServerConfiguration {
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private PermitAuthenticationWebFilter permitAuthenticationWebFilter;

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.addFilterBefore(permitAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        //認(rèn)證處理器
        ReactiveAuthenticationManager customAuthenticationManager = new CustomAuthenticationManager(tokenStore);
        JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint();
        //token轉(zhuǎn)換器
        ServerBearerTokenAuthenticationConverter tokenAuthenticationConverter = new ServerBearerTokenAuthenticationConverter();
        tokenAuthenticationConverter.setAllowUriQueryParameter(true);
        //oauth2認(rèn)證過濾器
        AuthenticationWebFilter oauth2Filter = new AuthenticationWebFilter(customAuthenticationManager);
        oauth2Filter.setServerAuthenticationConverter(tokenAuthenticationConverter);
        oauth2Filter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        oauth2Filter.setAuthenticationSuccessHandler(new Oauth2AuthSuccessHandler());
        http.addFilterAt(oauth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);

        ServerHttpSecurity.AuthorizeExchangeSpec authorizeExchange = http.authorizeExchange();
//        if (securityProperties.getAuth().getHttpUrls().length > 0) {
//            authorizeExchange.pathMatchers(securityProperties.getAuth().getHttpUrls()).authenticated();
//        }
        if (securityProperties.getIgnore().getUrls().length > 0) {
            authorizeExchange.pathMatchers(securityProperties.getIgnore().getUrls()).permitAll();
        }

        ServerHttpSecurity.AuthorizeExchangeSpec.Access access = authorizeExchange
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .anyExchange();
        setAuthenticate(access);

                //.anyExchange().authenticated()  //這個跟下面那行是一樣的,只是下面能更細(xì)的控制權(quán)限
                   // .access(permissionAuthManager)   // 應(yīng)用api權(quán)限控制 后期權(quán)限控制會用,暫時先不做
        http
            .exceptionHandling()
                .accessDeniedHandler(new JsonAccessDeniedHandler())
                .authenticationEntryPoint(entryPoint)
        .and()
            .headers()
                .frameOptions()
                .disable()
        .and()
            .httpBasic().disable()
            .csrf().disable();

        return http.build();
    }

    /**
     * url權(quán)限控制,默認(rèn)是認(rèn)證就通過,可以重寫實現(xiàn)個性化 permisson設(shè)置
     * @param authorizedAccess
     */
    public ServerHttpSecurity setAuthenticate(ServerHttpSecurity.AuthorizeExchangeSpec.Access authorizedAccess) {
        return authorizedAccess.authenticated().and();
    }
}
網(wǎng)關(guān)轉(zhuǎn)發(fā)、動態(tài)路由

網(wǎng)關(guān)轉(zhuǎn)發(fā)和動態(tài)路由需要如下配置,id為要匹配的id、predicates為要匹配的路徑,加前綴、filters為截去的路徑,動態(tài)路由實現(xiàn)是基于Nacos的配置監(jiān)聽,Gateway給了一個端點來實現(xiàn)此功能,當(dāng)然Gateway提供多種方式實現(xiàn)路由:

[
    {
        "id": "ebay-service",
        "predicates": [{
            "name": "Path",
            "args": {
                "pattern": "/ebay/**"
            }
        }],
        "uri": "lb://ebay-service",
        "filters": [{
            "name": "StripPrefix",
            "args": {
                "parts": "1"
            }
        }]
    }
]
網(wǎng)關(guān)統(tǒng)一請求攔截

統(tǒng)一資源過濾是需要把普通資源服務(wù)器的鑒權(quán)工作移到網(wǎng)關(guān),只需網(wǎng)關(guān)也是資源服務(wù)器就能做到此功能,前面已經(jīng)實現(xiàn)了網(wǎng)關(guān)作為資源服務(wù)器,并且實現(xiàn)了對普通服務(wù)轉(zhuǎn)發(fā),因此只需如下配置就可以對各資源服務(wù)器統(tǒng)一請求攔截:

  security:
    ignore:
      httpUrls: >
        /uaa/**,
        /dsuser/users-anon/**,
        /dsuser/api/**,
        /buser/users-anon/**,
        /buser/api/**,
        /shopify/dhgate/shopify/unauth/**,
        /myyshop/everybody/**,
        /umc/email/**,
        /myyshop-order/everybody/**,
        /myyshop/everybody/**,
        /umc/email/**,
        /myyshop-order/order/payCallBack,
        /search/myyshop/search,
        /myyshop-order/shipping/api/**
網(wǎng)關(guān)攔截器 PermitAuthenticationWebFilter

該攔截器作用于全局,并且順序是在鑒權(quán)之前,順序配置只需要在網(wǎng)關(guān)資源中心加個攔截器并指定生效于哪個攔截器之前即可:

http.addFilterBefore(permitAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);

作用是判斷當(dāng)前地址是否需要放權(quán),放權(quán)判斷你是通過spring提供的通配符判斷,如果放權(quán)則刪除當(dāng)前請求header token:

/**
 * webflux資源服務(wù)器配置
 *
 * @author lbj
 * @date 2020/07/02
 */
@EnableConfigurationProperties(SecurityProperties.class)
public class ResourceServerConfiguration {
    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private PermitAuthenticationWebFilter permitAuthenticationWebFilter;

    @Bean
    SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http.addFilterBefore(permitAuthenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        //認(rèn)證處理器
        ReactiveAuthenticationManager customAuthenticationManager = new CustomAuthenticationManager(tokenStore);
        JsonAuthenticationEntryPoint entryPoint = new JsonAuthenticationEntryPoint();
        //token轉(zhuǎn)換器
        ServerBearerTokenAuthenticationConverter tokenAuthenticationConverter = new ServerBearerTokenAuthenticationConverter();
        tokenAuthenticationConverter.setAllowUriQueryParameter(true);
        //oauth2認(rèn)證過濾器
        AuthenticationWebFilter oauth2Filter = new AuthenticationWebFilter(customAuthenticationManager);
        oauth2Filter.setServerAuthenticationConverter(tokenAuthenticationConverter);
        oauth2Filter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
        oauth2Filter.setAuthenticationSuccessHandler(new Oauth2AuthSuccessHandler());
        http.addFilterAt(oauth2Filter, SecurityWebFiltersOrder.AUTHENTICATION);

        ServerHttpSecurity.AuthorizeExchangeSpec authorizeExchange = http.authorizeExchange();
//        if (securityProperties.getAuth().getHttpUrls().length > 0) {
//            authorizeExchange.pathMatchers(securityProperties.getAuth().getHttpUrls()).authenticated();
//        }
        if (securityProperties.getIgnore().getUrls().length > 0) {
            authorizeExchange.pathMatchers(securityProperties.getIgnore().getUrls()).permitAll();
        }

        ServerHttpSecurity.AuthorizeExchangeSpec.Access access = authorizeExchange
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .anyExchange();
        setAuthenticate(access);

                //.anyExchange().authenticated()  //這個跟下面那行是一樣的,只是下面能更細(xì)的控制權(quán)限
                   // .access(permissionAuthManager)   // 應(yīng)用api權(quán)限控制 后期權(quán)限控制會用,暫時先不做
        http
            .exceptionHandling()
                .accessDeniedHandler(new JsonAccessDeniedHandler())
                .authenticationEntryPoint(entryPoint)
        .and()
            .headers()
                .frameOptions()
                .disable()
        .and()
            .httpBasic().disable()
            .csrf().disable();

        return http.build();
    }

    /**
     * url權(quán)限控制,默認(rèn)是認(rèn)證就通過,可以重寫實現(xiàn)個性化 permisson設(shè)置
     * @param authorizedAccess
     */
    public ServerHttpSecurity setAuthenticate(ServerHttpSecurity.AuthorizeExchangeSpec.Access authorizedAccess) {
        return authorizedAccess.authenticated().and();
    }
}

至此,整個SpringCloud+Spring Security+OAuth2 + JWT + Gateway集成通過源碼+現(xiàn)有代碼結(jié)束。

此外。

Security攔截器

Security攔截器順序在FilterComparator中,可以動態(tài)加入比他前或者后的順序

private static final int INITIAL_ORDER = 100;
    private static final int ORDER_STEP = 100;
    private final Map<String, Integer> filterToOrder = new HashMap();

    FilterComparator() {
        FilterComparator.Step order = new FilterComparator.Step(100, 100);
        this.put(ChannelProcessingFilter.class, order.next());
        this.put(ConcurrentSessionFilter.class, order.next());
        this.put(WebAsyncManagerIntegrationFilter.class, order.next());
        this.put(SecurityContextPersistenceFilter.class, order.next());
        this.put(HeaderWriterFilter.class, order.next());
        this.put(CorsFilter.class, order.next());
        this.put(CsrfFilter.class, order.next());
        this.put(LogoutFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter", order.next());
        this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationRequestFilter", order.next());
        this.put(X509AuthenticationFilter.class, order.next());
        this.put(AbstractPreAuthenticatedProcessingFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter", order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter", order.next());
        this.filterToOrder.put("org.springframework.security.saml2.provider.service.servlet.filter.Saml2WebSsoAuthenticationFilter", order.next());
        this.put(UsernamePasswordAuthenticationFilter.class, order.next());
        this.put(ConcurrentSessionFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.openid.OpenIDAuthenticationFilter", order.next());
        this.put(DefaultLoginPageGeneratingFilter.class, order.next());
        this.put(DefaultLogoutPageGeneratingFilter.class, order.next());
        this.put(ConcurrentSessionFilter.class, order.next());
        this.put(DigestAuthenticationFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationFilter", order.next());
        this.put(BasicAuthenticationFilter.class, order.next());
        this.put(RequestCacheAwareFilter.class, order.next());
        this.put(SecurityContextHolderAwareRequestFilter.class, order.next());
        this.put(JaasApiIntegrationFilter.class, order.next());
        this.put(RememberMeAuthenticationFilter.class, order.next());
        this.put(AnonymousAuthenticationFilter.class, order.next());
        this.filterToOrder.put("org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter", order.next());
        this.put(SessionManagementFilter.class, order.next());
        this.put(ExceptionTranslationFilter.class, order.next());
        this.put(FilterSecurityInterceptor.class, order.next());
        this.put(SwitchUserFilter.class, order.next());
    }

FilterComparator 比較器中初始化了Spring Security 自帶的Filter 的順序,即在創(chuàng)建時已經(jīng)確定了默認(rèn)Filter的順序。并將所有過濾器保存在一個 filterToOrder Map中。key值是Filter的類名,value是過濾器的順序號。

完結(jié)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容