JWTs結合SpringCloud使用程式碼示例
阿新 • • 發佈:2018-11-26
文章目錄
什麼是JWT
-
JSON Web Token (JWT)是一種基於 token 的認證方案。
-
簡單的說,JWT就是一種Token的編碼演算法,伺服器端負責根據一個密碼和演算法生成Token,然後發給客戶端,客戶端只負責後面每次請求都在HTTP header裡面帶上這個Token,伺服器負責驗證這個Token是不是合法的,有沒有過期等,並可以解析出subject和claim裡面的資料。
-
注意:JWT裡面的資料是BASE64編碼的,沒有加密,因此不要放如敏感資料
一個JWT token 看起來是這樣的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEzODY4OTkxMzEsImlzcyI6ImppcmE6MTU0ODk1OTUiLCJxc2giOiI4MDYzZmY0Y2ExZTQxZGY3YmM5MGM4YWI2ZDBmNjIwN2Q0OTFjZjZkYWQ3YzY2ZWE3OTdiNDYxNGI3MTkyMmU5IiwiaWF0IjoxMzg2ODk4OTUxfQ.uKqU9dTB6gKwG6jQCuXYAiMNdfNRw98Hw_IWuA5MaMo
在什麼時候使用JWTs
傳統的垂直架構應用,所有的程式碼都在一個war包中,使用者的請求session通常都是存在tomcat會話中,前端用sessionid來標識本次請求的會話
在分散式架構下,使用者的一個請求會跨越多個專案,顯然在tomcat中儲存已經不合適了
在這張情況下有幾種解決方案:
- 使用獨立快取來儲存使用者的session,比如存在redis或memcached中;spring框架提供的springSession就是這種解決方案。這種方案的前提必須是所有的服務必須連結同一個快取中心,不能跨越兩個不同的系統
- 使用JWTs獨立儲存使用者資訊,後臺使用攔截器對收到的JWTs進行解析,轉成實際使用者資訊再分發給其他的相關服務
JWTs結合SpringCloud使用
在分散式微服務應用中,為了保證對外暴露的介面安全,通常需要增加一個閘道器層,對介面所有的請求介面進行統一的攔截,並進行安全校驗
springCloud提供的閘道器層處理是zuul框架
首先,需要建立一個獨立的gate服務
在pom.xml中引入相應的依賴包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
為了處理JWTs請求,我們還需要引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
然後,建立一個Filter繼承com.netflix.zuul.ZuulFilter
下面是zuulFilte示例程式碼
@Component
public class MyHeaderFilter extends ZuulFilter {
public static final String GATE_SUBJECT_USER = "jwts_user";// 使用者登入資訊
public static final String GATE_ACCESSTOKEN = "AccessToken";//
public static final String GATE_SECRETKEY = "jwts_key_181118";
private static Logger log = LoggerFactory.getLogger(MyHeaderFilter.class);
@Override
public String filterType() {
//這裡是三個字串:pre,route,post
return "pre";
}
@Override
public int filterOrder() {
//過濾器優先順序,同一級別的過濾,數字小的優先執行
return 1;
}
@Override
public boolean shouldFilter() {
// 下面這行是核心程式碼,所有的引數都是間接或直接通過RequestContext獲取
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
try {
request.setCharacterEncoding("UTF-8");
} catch (Exception e) {
log.error(e.getMessage(), e);
}
String uri = request.getRequestURI().toString();
String method = request.getMethod();
log.info("url :{} method: {} ", uri, method);
// 這裡可以對url進行判斷,以確定是否進入過濾方法
// 返回true會進入下面的run方法,返回false會跳過
return true;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 1. clear userInfo from HTTP header to avoid fraud attack
ctx.addZuulRequestHeader("user-info", "");
// 2. verify the passed user token
String accessToken = request.getHeader(GATE_ACCESSTOKEN);
log.info("AccessToken: {}", accessToken);
Claims claims = null;
if (StringUtils.isNotBlank(accessToken)) {
try {
claims = TokenUtil.parseJWT(accessToken, GATE_SECRETKEY);
if (claims == null) {
this.stopZuulRoutingWithError(ctx, HttpStatus.UNAUTHORIZED,
"Error AccessToken for the API (" + request.getRequestURI().toString() + ")");
return null;
}
log.info("claims is:{}", claims);
if (claims.getSubject().equals(GATE_SUBJECT_USER)){
String userInfo = (String) claims.get("userInfo");
log.info("userInfo:{}", userInfo);
// 3. set userInfo to HTTP header
String encodeUserInfo = userInfo;
try {
encodeUserInfo = URLEncoder.encode(userInfo, "UTF-8");
} catch (Exception e) {
e.printStackTrace();
}
ctx.addZuulRequestHeader("user-info", encodeUserInfo);
log.info("ZuulRequestHeaders userInfo: {}", ctx.getZuulRequestHeaders().get("user-info"));
}
} catch (Exception e) {
this.stopZuulRoutingWithError(ctx, HttpStatus.UNAUTHORIZED,
"Error AccessToken for the API (" + request.getRequestURI().toString() + ")");
return null;
}
} else {
this.stopZuulRoutingWithError(ctx, HttpStatus.UNAUTHORIZED,
"AccessToken is needed for the API (" + request.getRequestURI().toString() + ")");
}
//這個方法的返回值目前沒什麼作用,我們直接返回null就可以
return null;
}
private void stopZuulRoutingWithError(RequestContext ctx, HttpStatus status, String responseText) {
ctx.removeRouteHost();
ctx.setResponseStatusCode(status.value());
ctx.setResponseBody(responseText);
//zuul通過sendfalse來中斷請求
ctx.setSendZuulResponse(false);
}
}
TokenUtil工具類程式碼
public class TokenUtil {
private static Logger logger = LoggerFactory.getLogger(TokenUtil.class);
private static Long timeLimit = 1000 * 60 * 60 * 24l;// 1天
public static final String PRIVATEKEY = "privateKey";
public static final String ACCESSTOKEN = "AccessToken";// 公私鑰
// 生成token
public static String createToken(String subject, Map<String, Object> map, String secretKey){
try {
byte[] bytes = Base64.encodeBase64(secretKey.getBytes("utf-8"));
String userToken = createToken(subject, map, bytes);
return userToken;
} catch (Exception e) {
logger.error("createToken error",e);
}
return null;
}
private static String createToken(String subject, Map<String, Object> map, byte[] secretKey) {
String userToken = null;
JwtBuilder builder = Jwts.builder().setSubject(subject)
.setExpiration(new Date(System.currentTimeMillis() + timeLimit));
if (map != null) {
for (String key : map.keySet()) {
builder.claim(key, map.get(key));
}
}
userToken = builder.signWith(SignatureAlgorithm.HS512, secretKey).compact();
return userToken;
}
/**
* 解密 jwt
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt, String secretKey) throws Exception {
byte[] bytes = Base64.encodeBase64(secretKey.getBytes("utf-8"));
Claims claims = Jwts.parser().setSigningKey(bytes).parseClaimsJws(jwt).getBody();
return claims;
}
}
後臺服務工程新增過濾器
通過過濾器抓取userInfo資訊,並存儲在ThreadLocal裡面
public class AuthenticationHeaderInterceptor implements HandlerInterceptor {
private static Logger log = LoggerFactory.getLogger(AuthenticationHeaderInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
UserInfo userInfo = null;
String userInfoJson = request.getHeader("user-info");
if (StringUtils.isNotBlank(userInfoJson)) {
try {
userInfoJson=URLDecoder.decode(userInfoJson,"utf-8");
userInfo = JsonUtil.json2Object(UserInfo.class, userInfoJson);
} catch (Exception e) {
log.error("userInfo decode error!",e);
}
//將userInfo儲存在ThreadLocal中
RequestUtil.setupUserInfo(userInfoJson, userInfo);
}
return true;
}
}
public class RequestUtil {
private static Logger log = LoggerFactory.getLogger(RequestUtil.class);
private static ThreadLocal<Object[]> userInfoKeeper = new ThreadLocal<Object[]>();
public static void setupUserInfo(String userInfoJson, UserInfo userInfo) {
userInfoKeeper.set(new Object[]{userInfoJson, userInfo});
}
public static UserInfo getUserInfo(HttpServletRequest request) {
Object[] data = (Object[])userInfoKeeper.get();
UserInfo userInfo = null;
if (data != null) {
userInfo = (UserInfo)data[1];
}
return userInfo;
}
public static void setUserInfoHeader(HttpHeaders headers) {
Object[] data = (Object[])userInfoKeeper.get();
if (data != null) {
String userInfoJson = (String)data[0];
String encodedUserInfoJson = userInfoJson;
try {
encodedUserInfoJson = URLEncoder.encode(userInfoJson,"UTF-8");
} catch (Exception e) {
log.error("setUserInfoHeader",e);
}
headers.set("user-info", encodedUserInfoJson);
}
}
}
新增攔截器配置
@Configuration
public class WebAppConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AuthenticationHeaderInterceptor()).addPathPatterns("/open/**");
}
}
總結
雖然程式碼中都有註釋,這裡還是再梳理一下幾個關鍵點
- filterType有三個型別pre,route,post直接用字串就可以。分別代表請求前、請求中、請求後。我們要做的使用者登入資訊驗證,所以使用的是pre
- 在使用者登入的時候,系統要返回一個accessToken(就是JWTs)給客戶端
- 客戶端將accessToken是作為請求的header引數傳過來的
- gate服務從header引數中獲取accessToke,並解析成userInfo資訊
- 將解析出來的userInfo放在header裡面分發給後臺服務
- 後臺服務新增過濾器,抓取userInfo資訊,並存儲在ThreadLocal裡面
- 這樣在業務程式碼中就能判斷當前請求的使用者是否已經登入