1. 程式人生 > >JWTs結合SpringCloud使用程式碼示例

JWTs結合SpringCloud使用程式碼示例

文章目錄

什麼是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中儲存已經不合適了

在這張情況下有幾種解決方案:

  1. 使用獨立快取來儲存使用者的session,比如存在redis或memcached中;spring框架提供的springSession就是這種解決方案。這種方案的前提必須是所有的服務必須連結同一個快取中心,不能跨越兩個不同的系統
  2. 使用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/**");
	}
}

總結

雖然程式碼中都有註釋,這裡還是再梳理一下幾個關鍵點

  1. filterType有三個型別pre,route,post直接用字串就可以。分別代表請求前、請求中、請求後。我們要做的使用者登入資訊驗證,所以使用的是pre
  2. 在使用者登入的時候,系統要返回一個accessToken(就是JWTs)給客戶端
  3. 客戶端將accessToken是作為請求的header引數傳過來的
  4. gate服務從header引數中獲取accessToke,並解析成userInfo資訊
  5. 將解析出來的userInfo放在header裡面分發給後臺服務
  6. 後臺服務新增過濾器,抓取userInfo資訊,並存儲在ThreadLocal裡面
  7. 這樣在業務程式碼中就能判斷當前請求的使用者是否已經登入

參考