項目簡介
- 本登錄系統(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源碼

所有的請求首先會到 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多請求共享

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

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 頭定義的算法生成哈希。哈希簽名的過程如下:
指定密碼,密碼保存在服務(wù)器中,不能向客戶端公開;
使用 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 時需要注意以下事項:
JWT 默認(rèn)不加密,如果要寫入敏感信息必須加密,可以用生成的原始令牌再次對內(nèi)容進(jìn)行加密;
JWT 無法使服務(wù)器保存會話狀態(tài),當(dāng)令牌生成后在有效期內(nèi)無法取消也不能更改;
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);
}

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驗證,不加會報如下錯誤:

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ù)OAuth2Request和Authentication去構(gòu)造OAuth2Authentication,然后通過OAuth2Authentication最終去創(chuàng)造AccessToken。至此,密碼登錄方式完成。效果如下:

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é)。