JWTs结合SpringCloud使用代码示例

文章目录

      • 什么是JWT
      • 在什么时候使用JWTs
      • JWTs结合SpringCloud使用
        • 首先,需要创建一个独立的gate服务
        • 后台服务工程添加过滤器
      • 总结
      • 参考

什么是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.cloudgroupId>
	<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>

为了处理JWTs请求,我们还需要引入

<dependency>
	<groupId>io.jsonwebtokengroupId>
	<artifactId>jjwtartifactId>
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. 这样在业务代码中就能判断当前请求的用户是否已经登录

参考

  • JWTs简介

你可能感兴趣的:(框架)