微服務網(wǎng)關與用戶身份識別,JWT+Spring Security進行網(wǎng)關安全認證
JWT+Spring Security進行網(wǎng)關安全認證
JWT和Spring Security相結合進行系統(tǒng)安全認證是目前使用比較多的一種安全認證組合。瘋狂創(chuàng)客圈crazy-springcloud微服務開發(fā)腳手架使用JWT身份令牌結合Spring Security的安全認證機制完成用戶請求的安全權限認證。整個用戶認證的過程大致如下:
(1)前臺(如網(wǎng)頁富客戶端)通過REST接口將用戶名和密碼發(fā)送到UAA用戶賬號與認證微服務進行登錄。
(2)UAA服務在完成登錄流程后,將Session ID作為JWT的負載(payload),生成JWT身份令牌后發(fā)送給前臺。
(3)前臺可以將JWT令牌存到localStorage或者sessionStorage中,當然,退出登錄時,前端必須刪除保存的JWT令牌。
(4)前臺每次在請求微服務提供者的REST資源時,將JWT令牌放到請求頭中。crazy-springcloud腳手架做了管理端和用戶端的前臺區(qū)分,管理端前臺的令牌頭為Authorization,用戶端前臺的令牌頭為token。
(5)在請求到達Zuul網(wǎng)關時,Zuul會結合Spring Security進行攔截,從而驗證JWT的有效性。
(6)Zuul驗證通過后才可以訪問微服務所提供的REST資源。
需要說明的是,在crazy-springcloud微服務開發(fā)腳手架中,Provider微服務提供者自身不需要進行單獨的安全認證,Provider之間的內(nèi)部遠程調(diào)用也是不需要安全認證的,安全認證全部由網(wǎng)關負責。嚴格來說,這套安全機制是能夠滿足一般的生產(chǎn)場景安全認證要求的。如果覺得這個安全級別不是太高,單個的Provider微服務也需要進行獨立的安全認證,那么實現(xiàn)起來也是很容易的,只需要導入公共的安全認證模塊base-auth即可。實際上早期的crazy-springcloud腳手架也是這樣做的,后期發(fā)現(xiàn)這樣做純屬多慮,而且大大降低了Provider服務提供者模塊的可復用性和可移植性(這是微服務架構的巨大優(yōu)勢之一)。所以,crazy-springcloud后來將整體架構調(diào)整為由網(wǎng)關(如Zuul或者Nginx)負責安全認證,去掉了Provider服務提供者的安全認證能力。
JWT安全令牌規(guī)范詳解
JWT(JSON Web Token)是一種用戶憑證的編碼規(guī)范,是一種網(wǎng)絡環(huán)境下編碼用戶憑證的JSON格式的開放標準(RFC 7519)。JWT令牌的格式被設計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)、用戶身份認證等場景。
一個編碼之后的JWT令牌字符串分為三部分:header+payload+signature。這三部分通過點號“.”連接,第一部分常被稱為頭部(header),第二部分常被稱為負載(payload),第三部分常被稱為簽名(signature)。
1.JWT的header
編碼之前的JWT的header部分采用JSON格式,一個完整的頭部就像如下的JSON內(nèi)容:
{
"typ":"JWT",
"alg":"HS256"
}其中,"typ"是type(類型)的簡寫,值為"JWT"代表JWT類型;"alg"是加密算法的簡寫,值為"HS256"代表加密方式為HS256。
采用JWT令牌編碼時,header的JSON字符串將進行Base64編碼,編碼之后的字符串構成了JWT令牌的第一部分。
2.JWT的playload
編碼之前的JWT的playload部分也是采用JSON格式,playload是存放有效信息的部分,一個簡單的playload就像如下的JSON內(nèi)容:
{
"sub":"session id",
"exp":1579315717,
"iat":1578451717
}采用JWT令牌編碼時,playload的JSON字符串將進行Base64編碼,編碼之后的字符串構成了JWT令牌的第二部分。
3.JWT的signature
JWT的第三部分是一個簽名字符串,這一部分是將header的Base64編碼和payload的Base64編碼使用點號(.)連接起來之后,通過header聲明的加密算法進行加密所得到的密文。為了保證安全,加密時需要加入鹽(salt)。
下面是一個演示用例:用Java代碼生成JWT令牌,然后對令牌的header部分字符串和payload部分字符串進行Base64解碼,并輸出解碼后的JSON。
package com.crazymaker.demo.auth;
//省略import
@Slf4j
public class JwtDemo
{
@Test
public void testBaseJWT()
{
try
{
/**
*JWT的演示內(nèi)容
*/
String subject = "session id";
/**
*簽名的加密鹽
*/
String salt = "user password";
/**
*簽名的加密算法
*/
Algorithm algorithm = Algorithm.HMAC256(salt);
//簽發(fā)時間
long start = System.currentTimeMillis() - 60000;
//過期時間,在簽發(fā)時間的基礎上加上一個有效時長
Date end = new Date(start + SessionConstants.SESSION_TIME_OUT *1000);
/**
*獲取編碼后的JWT令牌
*/
String token = JWT.create()
.withSubject(subject)
.withIssuedAt(new Date(start))
.withExpiresAt(end)
.sign(algorithm);
log.info("token=" + token);
//編碼后輸出demo為:
//token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZXNza
W9uIGlkIiwiZXhwIjoxNTc5MzE1NzE3LCJpYXQiOjE1Nzg0NTE3MTd9.iANh9Fa0B_6H5TQ11bLCWcEpmWxuCwa2Rt6rnzBWteI
//以.分隔令牌
String[] parts = token.split("\\." );
/**
*對第一部分和第二部分進行解碼
*解碼后的第一部分:header
*/
String headerJson =
StringUtils.newStringUtf8(Base64.decodeBase64(parts[0]));
log.info("parts[0]=" + headerJson);
//解碼后的第一部分輸出的示例為://parts[0]={"typ":"JWT","alg":"HS256"}
/**
*解碼后的第二部分:payload
*/
String payloadJson;
payloadJson = StringUtils.newStringUtf8
(Base64.decodeBase64(parts[1]));
log.info("parts[1]=" + payloadJson);
//輸出的示例為:
//解碼后的第二部分:parts[1]={"sub":"session id","exp":1579315535,"iat":
1578451535}
} catch (Exception e)
{
e.printStackTrace();
}
}
...
}在編碼前的JWT中,payload部分JSON中的屬性被稱為JWT的聲明。JWT的聲明分為兩類:
(1)公有的聲明(如iat)。
(2)私有的聲明(自定義的JSON屬性)。
公有的聲明也就是JWT標準中注冊的聲明,主要為以下JSON屬性:
(1)iss:簽發(fā)人。
(2)sub:主題。
(3)aud:用戶。
(4)iat:JWT的簽發(fā)時間。
(5)exp:JWT的過期時間,這個過期時間必須要大于簽發(fā)時間。
(6)nbf:定義在什么時間之前該JWT是不可用的。
私有的聲明是除了公有聲明之外的自定義JSON字段,私有的聲明可以添加任何信息,一般添加用戶的相關信息或其他業(yè)務需要的必要信息。下面的JSON例子中的uid、user_name、nick_name等都是私有聲明。
{
"uid": "123...",
"sub": "session id",
"user_name": "admin",
"nick_name": "管理員",
"exp": 1579317358,
"iat": 1578453358
}下面是一個向JWT令牌添加私有聲明的實例,代碼如下:
package com.crazymaker.demo.auth;
//省略import
@Slf4j
public class JwtDemo
{
/**
*測試私有聲明
*/
@Test
public void testJWTWithClaim()
{
try
{
String subject = "session id";
String salt = "user password";
/**
*簽名的加密算法
*/
Algorithm algorithm = Algorithm.HMAC256(salt);
//簽發(fā)時間
long start = System.currentTimeMillis() - 60000;
//過期時間,在簽發(fā)時間的基礎上加上一個有效時長
Date end = new Date(start + SessionConstants.SESSION_TIME_OUT *1000);
/**
*JWT建造者
*/
JWTCreator.Builder builder = JWT.create();
/**
*增加私有聲明
*/
builder.withClaim("uid", "123...");
builder.withClaim("user_name", "admin");
builder.withClaim("nick_name","管理員");
/**
*獲取編碼后的JWT令牌
*/
String token =builder
.withSubject(subject)
.withIssuedAt(new Date(start))
.withExpiresAt(end)
.sign(algorithm);
log.info("token=" + token);
//以.分隔,這里需要轉義
String[] parts = token.split("\\." );
String payloadJson;
/**
*解碼payload
*/
payloadJson = StringUtils.newStringUtf8
(Base64.decodeBase64(parts[1]));
log.info("parts[1]=" + payloadJson);
//輸出demo為:parts[1]=
//{"uid":"123...","sub":"session id","user_name":"admin",
"nick_name":"管理員","exp":1579317358,"iat":1578453358}
} catch (Exception e)
{
e.printStackTrace();
}
}
}由于JWT的payload聲明(JSON屬性)是可以解碼的,屬于明文信息,因此不建議添加敏感信息。
?JWT+Spring Security認證處理流程
實際開發(fā)中如何使用JWT進行用戶認證呢?瘋狂創(chuàng)客圈的crazy-springcloud開發(fā)腳手架將JWT令牌和Spring Security相結合,設計了一個公共的、比較方便復用的用戶認證模塊base-auth。一般來說,在Zuul網(wǎng)關或者微服務提供者進行用戶認證時導入這個公共的base-auth模塊即可。
這里還是按照6.4.2節(jié)中請求認證處理流程的5個步驟介紹base-auth模塊中JWT令牌的認證處理流程。
首先看第一步:定制一個憑證/令牌類,封裝用戶信息和JWT認證信息。
package com.crazymaker.springcloud.base.security.token;
//省略import
public class JwtAuthenticationToken extends AbstractAuthenticationToken
{
private static final long serialVersionUID = 3981518947978158945L;
//封裝用戶信息:用戶id、密碼
private UserDetails userDetails;
//封裝的JWT認證信息
private DecodedJWT decodedJWT;
...
}再看第二步:定制一個認證提供者類和憑證/令牌類進行配套,并完成對自制憑證/令牌實例的驗證。
package com.crazymaker.springcloud.base.security.provider;
//省略import
public class JwtAuthenticationProvider implements AuthenticationProvider
{
//用于通過session id查找用戶信息
private RedisOperationsSessionRepository sessionRepository;
public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository)
{
this.sessionRepository = sessionRepository;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException
{
//判斷JWT令牌是否過期
JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
DecodedJWT jwt =jwtToken.getDecodedJWT();
if (jwt.getExpiresAt().before(Calendar.getInstance().getTime()))
{
throw new NonceExpiredException("認證過期");
}
//取得session id
String sid = jwt.getSubject();
//取得令牌字符串,此變量將用于驗證是否重復登錄
String newToken = jwt.getToken();
//獲取session
Session session = null;
try
{
session = sessionRepository.findById(sid);
} catch (Exception e)
{
e.printStackTrace();
}
if (null == session)
{
throw new NonceExpiredException("還沒有登錄,請登錄系統(tǒng)!");
}
String json = session.getAttribute(G_USER);
if (StringUtils.isBlank(json))
{
throw new NonceExpiredException("認證有誤,請重新登錄");
}
//取得session中的用戶信息
UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class);
if (null == userDTO)
{
throw new NonceExpiredException("認證有誤,請重新登錄");
}
判斷是否在其他地方已經(jīng)登錄 //判斷是否在其他地方已經(jīng)登錄
if (null == newToken || !newToken.equals(userDTO.getToken()))
{
throw new NonceExpiredException("您已經(jīng)在其他的地方登錄!");
}
String userID = null;
if (null == userDTO.getUserId())
{
userID = String.valueOf(userDTO.getId());
} else
{
userID = String.valueOf(userDTO.getUserId());
}
UserDetails userDetails = User.builder()
.username(userID)
.password(userDTO.getPassword())
.authorities(SessionConstants.USER_INFO)
.build();
try
{
//用戶密碼的密文作為JWT的加密鹽
String encryptSalt = userDTO.getPassword();
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
//創(chuàng)建驗證器
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(sid)
.build();
//進行JWTtoken驗證
verifier.verify(newToken);
} catch (Exception e)
{
throw new BadCredentialsException("認證有誤:令牌校驗失敗,請重新登錄", e);
}
//返回認證通過的token,包含用戶信息,如user id等
JwtAuthenticationToken passedToken =
new JwtAuthenticationToken(userDetails, jwt, userDetails.getAuthorities());
passedToken.setAuthenticated(true);
return passedToken;
}
//支持自定義的令牌JwtAuthenticationToken
@Override
public boolean supports(Class> authentication)
{
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}JwtAuthenticationProvider負責對傳入的JwtAuthenticationToken憑證/令牌實例進行多方面的驗證:(1)驗證解碼后的DecodedJWT實例是否過期;(2)由于本演示中JWT的subject(主題)信息存放的是用戶的Session ID,因此還要判斷會話是否存在;(3)使用會話中的用戶密碼作為鹽,對JWT令牌進行安全性校驗。
如果以上驗證都順利通過,就構建一個新的JwtAuthenticationToken令牌,將重要的用戶信息(UserID)放入令牌并予以返回,供后續(xù)操作使用。
第三步:定制一個過濾器類,從請求中獲取用戶信息組裝成JwtAuthenticationToken憑證/令牌,交給認證管理者。在crazy-springcloud腳手架中,前臺有用戶端和管理端的兩套界面,所以,將認證頭部信息區(qū)分成管理端和用戶端兩類:管理端的頭部字段為Authorization;用戶端的認證信息頭部字段為token。
過濾器從請求中獲取認證的頭部字段,解析之后組裝成JwtAuthenticationToken令牌實例,提交給AuthenticationManager進行驗證。
package com.crazymaker.springcloud.base.security.filter;
//省略import
public class JwtAuthenticationFilter extends OncePerRequestFilter
{
...
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws
{
...
Authentication passedToken = null;
AuthenticationException failed = null;
//從HTTP請求取得JWT令牌的頭部字段 String token = null;
//用戶端存放的JWT的HTTP頭部字段為token
String sessionIDStore = SessionHolder.getSessionIDStore();
if (sessionIDStore.equals(SessionConstants.SESSION_STORE))
{
token = request.getHeader(SessionConstants.AUTHORIZATION_HEAD);
}
//管理端存放的JWT的HTTP頭部字段為Authorization
else if (sessionIDStore.equals
(SessionConstants.ADMIN_SESSION_STORE))
{
token = request.getHeader
(SessionConstants.ADMIN_AUTHORIZATION_HEAD);
}
//沒有取得頭部,報異常
else
{
failed = new InsufficientAuthenticationException("請求頭認證消息為空" );
unsuccessfulAuthentication(request, response, failed);
return;
}
token = StringUtils.removeStart(token, "Bearer " );
try
{
if (StringUtils.isNotBlank(token))
{
//組裝令牌
JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
//提交給AuthenticationManager進行令牌驗證,獲取認證后的令牌
passedToken = this.getAuthenticationManager()
.authenticate(authToken);
//取得認證后的用戶信息,主要是用戶id
UserDetails details = (UserDetails) passedToken.getDetails();
//通過details.getUsername()獲取用戶id,并作為請求屬性進行緩存
request.setAttribute(SessionConstants.USER_IDENTIFIER, details.getUsername());
} else
{
failed = new InsufficientAuthenticationException("請求頭認證消息為空" );
}
} catch (JWTDecodeException e)
{
...
}
...
filterChain.doFilter(request, response);
}
...
}AuthenticationManager將調(diào)用注冊在內(nèi)部的JwtAuthenticationProvider認證提供者,對JwtAuthenticationToken進行驗證。
為了使得過濾器能夠生效,必須將過濾器加入HTTP請求的過濾處理責任鏈,這一步可以通過實現(xiàn)一個AbstractHttpConfigurer配置類來完成。
第四步:定制一個HTTP的安全認證配置類(AbstractHttpConfigurer子類),將上一步定制的過濾器加入請求的過濾處理責任鏈。
package com.crazymaker.springcloud.base.security.configurer;
...
public class JwtAuthConfigurer<T extends JwtAuthConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigu
{
private JwtAuthenticationFilter jwtAuthenticationFilter;
public JwtAuthConfigurer()
{
//創(chuàng)建認證過濾器
this.jwtAuthenticationFilter = new JwtAuthenticationFilter();
}
//將過濾器加入http過濾處理責任鏈
@Override
public void configure(B http) throws Exception
{
//獲取Spring Security共享的AuthenticationManager實例
//將其設置到jwtAuthenticationFilter認證過濾器 jwtAuthenticationFilter.setAuthenticationManager(http.getSharedObject
jwtAuthenticationFilter.setAuthenticationFailureHandler(new AuthFailureHandler());
JwtAuthenticationFilter filter = postProcess(jwtAuthenticationFilter);
//將過濾器加入http過濾處理責任鏈
http.addFilterBefore(filter, LogoutFilter.class);
}
...
}第五步:定義一個Spring Security安全配置類(
WebSecurityConfigurerAdapter子類),對Web容器的HTTP安全認證機制進行配置。這是最后一步,有兩項工作:一是在HTTP安全策略上應用JwtAuthConfigurer配置實例;二是構造AuthenticationManagerBuilder認證管理者實例。這一步可以通過繼承WebSecurityConfigurerAdapter適配器來完成。
package com.crazymaker.springcloud.cloud.center.zuul.config;
...
@ConditionalOnWebApplication
@EnableWebSecurity()
public class ZuulWebSecurityConfig extends WebSecurityConfigurerAdapter
{
//注入session存儲實例,用于查找session(根據(jù)session id)
@Resource
RedisOperationsSessionRepository sessionRepository;
//配置HTTP請求的安全策略,應用DemoAuthConfigurer配置類實例
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.csrf().disable()
...
.authorizeRequests()
.and()
.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin().disable()
.sessionManagement().disable()
.cors()
.and()
//在HTTP安全策略上應用JwtAuthConfigurer配置類實例
.apply(new JwtAuthConfigurer<>()) .tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissi
.and()
.logout().disable()
.sessionManagement().disable();
}
//配置認證Builder,由其負責構造AuthenticationManager實例
//Builder所構造的AuthenticationManager實例將作為HTTP請求的共享對象
//可以通過http.getSharedObject(AuthenticationManager.class)來獲取
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
//在Builder實例中加入自定義的Provider認證提供者實例
auth.authenticationProvider(jwtAuthenticationProvider());
}
//創(chuàng)建一個JwtAuthenticationProvider提供者實例
@DependsOn({"sessionRepository"})
@Bean("jwtAuthenticationProvider")
protected AuthenticationProvider jwtAuthenticationProvider()
{
return new JwtAuthenticationProvider(sessionRepository);
}
...
}至此,一個基于JWT+Spring Security的用戶認證處理流程就定義完了。但是,此流程僅僅涉及JWT令牌的認證,沒有涉及JWT令牌的生成。一般來說,JWT令牌的生成需要由系統(tǒng)的UAA(用戶賬號與認證)服務(或者模塊)負責完成。
Zuul網(wǎng)關與UAA微服務的配合
crazy-springcloud腳手架通過Zuul網(wǎng)關和UAA微服務相互結合來完成整個用戶的登錄與認證閉環(huán)流程。二者的關系大致為:
(1)登錄時,UAA微服務負責用戶名稱和密碼的驗證并且將用戶信息(包括令牌加密鹽)放在分布式Session中,然后返回JWT令牌(含Session ID)給前臺。
(2)認證時,前臺請求帶上JWT令牌,Zuul網(wǎng)關能根據(jù)令牌中的Session ID取出分布式Session中的加密鹽,對JWT令牌進行驗證。在crazy-springcloud腳手架的會話架構中,Zuul網(wǎng)關必須能和UAA微服務進行會話的共享,如圖6-7所示。

圖6-7 Zuul網(wǎng)關和UAA微服務進行會話的共享
在crazy-springcloud的UAA微服務提供者crazymaker-uaa實現(xiàn)模塊中,controller(控制層)的REST登錄接口的定義如下:
@Api(value = "用戶端登錄與退出", tags = {"用戶信息、基礎學習DEMO"})
@RestController
@RequestMapping("/api/session" )
public class SessionController
{
//用戶端會話服務
@Resource
private FrontUserEndSessionServiceImpl userService;
//用戶端的登錄REST接口
@PostMapping("/login/v1" )
@ApiOperation(value = "用戶端登錄" )
public RestOut login(@RequestBody LoginInfoDTO loginInfoDTO, HttpServlet
{
//調(diào)用服務層登錄方法獲取令牌
LoginOutDTO dto = userService.login(loginInfoDTO);
response.setHeader("Content-Type", "text/html;charset=utf-8" );
response.setHeader(SessionConstants.AUTHORIZATION_HEAD, dto.getToken());
return RestOut.success(dto);
}
...
} 用戶登錄時,在服務層,客戶端會話服務
FrontUserEndSessionServiceImpl負責從用戶數(shù)據(jù)庫中獲取用戶,然后進行密碼驗證。
package com.crazymaker.springcloud.user.info.service.impl;
//省略import
@Slf4j
@Service
public class FrontUserEndSessionServiceImpl
{
//Dao Bean,用于查詢數(shù)據(jù)庫用戶
@Resource
UserDao userDao;
//加密器
@Resource
private PasswordEncoder passwordEncoder;
//緩存操作服務
@Resource
RedisRepository redisRepository;
//Redis會話存儲服務
@Resource
private RedisOperationsSessionRepository sessionRepository;
/**
*登錄處理
*@param dto用戶名、密碼
*@return登錄成功的dto
*/
public LoginOutDTO login(LoginInfoDTO dto)
{
String username = dto.getUsername();
//從數(shù)據(jù)庫獲取用戶
List list = userDao.findAllByUsername(username);
if (null == list || list.size() <= 0)
{
throw BusinessException.builder().errMsg("用戶名或者密碼錯誤" );
}
UserPO userPO = list.get(0);
//進行密碼的驗證
//String encode = passwordEncoder.encode(dto.getPassword());
String encoded = userPO.getPassword();
String raw = dto.getPassword();
boolean matched = passwordEncoder.matches(raw, encoded);
if (!matched)
{
throw BusinessException.builder().errMsg("用戶名或者密碼錯誤" );
}
//設置session,方便Spring Security進行權限驗證
return setSession(userPO);
}
/**
*1:將userid -> session id作為鍵-值對(Key-Value Pair)緩存起來,防止頻繁創(chuàng)建session
*2:將用戶信息保存到分布式Session
*3:創(chuàng)建JWT token,提供給Spring Security進行權限驗證
*@param userPO用戶信息
*@return登錄的輸出信息
*/
private LoginOutDTO setSession(UserPO userPO)
{
if (null == userPO)
{
throw BusinessException.builder().errMsg("用戶不存在或者密碼錯誤" ).build();
}
/**
*根據(jù)用戶id查詢之前保存的session id
*防止頻繁登錄的時候session被大量創(chuàng)建
*/
String uid = String.valueOf(userPO.getUserId());
String sid = redisRepository.getSessionId(uid);
Session session = null;
try
{
/**
*查找現(xiàn)有的session
*/
session = sessionRepository.findById(sid);
} catch (Exception e)
{
//e.printStackTrace();
log.info("查找現(xiàn)有的session失敗,將創(chuàng)建一個新的session" );
}
if (null == session)
{
session = sessionRepository.createSession();
//新的session id和用戶id一起作為鍵-值對進行保存
//用戶訪問的時候可以根據(jù)用戶id查找session id
sid = session.getId();
redisRepository.setSessionId(uid, sid);
}
String salt = userPO.getPassword();
構建 //構建JWT token
String token = AuthUtils.buildToken(sid, salt);
/**
*將用戶信息緩存到分布式Session
*/
UserDTO cacheDto = new UserDTO();
BeanUtils.copyProperties(userPO, cacheDto);
cacheDto.setToken(token);
session.setAttribute(G_USER, JsonUtil.pojoToJson(cacheDto));
LoginOutDTO outDTO = new LoginOutDTO();
BeanUtils.copyProperties(cacheDto, outDTO);
return outDTO;
}
} 如果用戶驗證通過,那么前端會話服務
FrontUserEndSessionServiceImpl在setSession方法中創(chuàng)建Redis分布式Session(如果不存在舊Session),然后將用戶信息(密碼為令牌的salt)緩存起來。如果用戶存在舊Session,那么舊Session的ID將通過用戶的uid查找到,然后通過sessionRepository找到舊Session,做到在頻繁登錄的場景下不會導致Session被大量創(chuàng)建。
最終,uaa-provider微服務將返回JWT令牌(subject設置為Session ID)給前臺。由于Zuul網(wǎng)關和uaa-provider微服務共享分布式Session,在進行請求認證時,Zuul網(wǎng)關能通過JWT令牌中的Session ID取出分布式Session中的用戶信息和加密鹽,對JWT令牌進行驗證。
使用Zuul過濾器添加代理請求的用戶標識
完成用戶認證后,Zuul網(wǎng)關的代理請求將轉發(fā)給上游的微服務Provider實例。此時,代理請求仍然需要帶上用戶的身份標識,而此時身份標識不一定是Session ID,而是和上游的Provider強相關:
(1)如果Provider是將JWT令牌作為用戶身份標識(和Zuul一
樣),那么Zuul網(wǎng)關將JWT令牌傳給Provider微服務提供者。(2)如果Provider是將Session ID作為用戶身份標識,那么Zuul需要將JWT令牌的subject中的Session ID解析出來,然后傳給Provider微服務提供者。
(3)如果Provider是將用戶ID作為用戶身份標識,那么Zuul既不能將JWT令牌傳給Provider,又不能將Session ID傳給Provider,而是要將會話中緩存的用戶ID傳給Provider。
前兩種用戶身份標識的傳遞方案都要求Provider微服務和網(wǎng)關共享會話,而實際場景中,這種可能性不是100%。另外,負責安全認證的網(wǎng)關可能不是Zuul,而是性能更高的OpenResty(甚至是Kong),如果這樣,共享Session技術難度就會更大??傊瑸榱耸钩绦虻目蓴U展性和可移植性更好,建議使用第三種用戶身份標識的代理傳遞方案。
crazy-springcloud腳手架采用的是第三種用戶標識傳遞方案。
JWT令牌被驗證成功后,網(wǎng)關的代理請求被加上"USER-ID"頭,將用戶ID作為用戶身份標識添加到請求頭部,傳遞給上游Provider。這個功能使用了一個Zuul過濾器實現(xiàn),代碼如下:
package com.crazymaker.springcloud.cloud.center.zuul.filter;
//省略import@Component
@Slf4j
public class ModifyRequestHeaderFilter extends ZuulFilter
{
/**
*根據(jù)條件判斷是否需要路由,是否需要執(zhí)行該過濾器
*/
@Override
public boolean shouldFilter()
{
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
/**
*存在用戶端認證token
*/
String token = request.getHeader(SessionConstants
.AUTHORIZATION_HEAD);
if (!StringUtils.isEmpty(token))
{
return true;
}
/**
*存在管理端認證token
*/
token = request.getHeader(SessionConstants
.ADMIN_AUTHORIZATION_HEAD);
if (!StringUtils.isEmpty(token))
{
return true;
}
return false;
}
/**
*調(diào)用上游微服務之前修改請求頭,加上USER-ID頭
*
*@return
*@throws ZuulException
*/
@Override
public Object run() throws ZuulException
{
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
//認證成功,請求的"USER-ID"(USER_IDENTIFIER)屬性被設置
String identifier = (String) request.getAttribute
(SessionConstants.USER_IDENTIFIER);
//代理請求加上 "USER-ID" 頭
if (StringUtils.isNotBlank(identifier))
{
ctx.addZuulRequestHeader(SessionConstants.USER_IDENTIFIER, identifier);
}
return null;
}
@Override
public String filterType()
{
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder()
{
return 1;
}
}本文給大家講解的內(nèi)容是 微服務網(wǎng)關與用戶身份識別,JWT+Spring Security進行網(wǎng)關安全認證
下篇文章給大家講解的是微服務網(wǎng)關與用戶身份識別,服務提供者之間的會話共享關系;
覺得文章不錯的朋友可以轉發(fā)此文關注小編;
感謝大家的支持!
本文就是愿天堂沒有BUG給大家分享的內(nèi)容,大家有收獲的話可以分享下,想學習更多的話可以到微信公眾號里找我,我等你哦。
