JSON Web Token (JWT)是一种基于 token 的认证方案。
简单的说,JWT就是一种Token的编码算法,服务器端负责根据一个密码和算法生成Token,然后发给客户端,客户端只负责后面每次请求都在HTTP header里面带上这个Token,服务器负责验证这个Token是不是合法的,有没有过期等,并可以解析出subject和claim里面的数据。
注意:JWT里面的数据是BASE64编码的,没有加密,因此不要放如敏感数据
一个JWT token 看起来是这样的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEzODY4OTkxMzEsImlzcyI6ImppcmE6MTU0ODk1OTUiLCJxc2giOiI4MDYzZmY0Y2ExZTQxZGY3YmM5MGM4YWI2ZDBmNjIwN2Q0OTFjZjZkYWQ3YzY2ZWE3OTdiNDYxNGI3MTkyMmU5IiwiaWF0IjoxMzg2ODk4OTUxfQ.uKqU9dTB6gKwG6jQCuXYAiMNdfNRw98Hw_IWuA5MaMo
传统的垂直架构应用,所有的代码都在一个war包中,用户的请求session通常都是存在tomcat会话中,前端用sessionid来标识本次请求的会话
在分布式架构下,用户的一个请求会跨越多个项目,显然在tomcat中存储已经不合适了
在这张情况下有几种解决方案:
在分布式微服务应用中,为了保证对外暴露的接口安全,通常需要增加一个网关层,对接口所有的请求接口进行统一的拦截,并进行安全校验
springCloud提供的网关层处理是zuul框架
在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/**");
}
}
虽然代码中都有注释,这里还是再梳理一下几个关键点