接上一篇的权限控制,再讨论再网关zuul的登录认证实现。
网关使用SpringCloud的zuul,登录认证选择使用自定义共享session的方式,来实现集群的登录验证。保护接口的私密,保证系统安全。
Filter
zuul提供了filter来对请求进行过滤处理,首先,了解网关zuul的filter。
zuul的filter有三种类型的,pre,route,post,error,static。
因为登录验证在网关接受到请求之后就要做的,所以在prefilter中增加登录验证的逻辑。
请求Header
cookies和特殊的http请求headers
使用共享session的方式,后端服务必然也要使用用户的缓存数据,这就需要将用户标识传递给后端服务,通过http请求Header传递,将用户请求的Cookie或者特殊的http请求headers(取决于自己的需要)传递给网关后面的业务服务。zuul提供了配置zuul.sensitiveHeaders来配置需要传递给后端服务的请求头。如:
zuul:
sensitive-headers: Cookie,Set-Cookie
登录
要登录验证,首先需要登录,登录如何实现呢。
验证用户登录信息有效性后,生成token,存储用户token到redis中,作为有效token,同时将用户的缓存信息存入redis中。
redis中数据存储结构为两个键值对
样例代码如下:
public Map login(String userId, String password) {
//验证用户登录信息有效性
Boolean result = validate(userId, password);
Map map = new HashMap<>();
//存储两个键值对,一个是token和用户数据的键值对,第二个是用户id和token的键值对,实现通过用户ID找到用户的token,实现即刻失效用户token的功能。根据需求,也可以只存储一个。
if(result){
String body = getUser(userId);
String sessionKey = SESSION_PREFIX+"_"+sessionId;
String userKey = USER_ID_PREFIX+"_"+userId;
redisTemplate.opsForValue().set(sessionKey, userId, EXPIRE_SECOND, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(userKey, sessionId, EXPIRE_SECOND, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(dataKey, body, EXPIRE_SECOND, TimeUnit.SECONDS);
map.put("code","0");
map.put("msg","ok");
return map;
}
map.put("code","0");
map.put("msg","用户名或密码错误");
return map;
}
登录验证
考虑到登录验证的时候,还有隐藏的其他可能需要的功能。
考虑将最基本的第一个和第二个功能加入进来,其他的后续再实现。
增加1,2需求后登录认证的流程如下
通常需要登录验证的接口列表,不刷新token有效时间的接口列表都通过配置来实现,这里不再赘述。
登录认证代码如下:
@Component
public class PreFilter {
public static final Logger log = LoggerFactory.getLogger(PreFilter.class);
@Autowired
private RedisTemplate redisTemplate;
@Value("${server.context-path}")
private String contextPath;
@Value("${auth.session.expireSecond}")
private Integer expire;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
// 得到Rquest Response
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest req = ctx.getRequest();
HttpServletResponse res = ctx.getResponse();
try {
String uri = req.getRequestURI();
//得到SessionId
String sessionId = req.getSession(true).getId();
//是否登录认证
Boolean isIgnore = isIgore(uri);
if(isIgnore){
ctx.setSendZuulResponse(true);// 对该请求进行路由
ctx.setResponseStatusCode(200);
return null;
}
//是否刷新session过期时间
Boolean isRefreshExpire = isRefreshExpire(uri);
Boolean isLogin = false;
if(isRefreshExpire){
// 检查过期时间并刷新
isLogin = checkLoginWithExpire(sessionId);
}else{
// 检查过期时间
isLogin = checkLoginWithoutExpire(sessionId);
}
//认证成功
if(isLogin){
ctx.setSendZuulResponse(true);// 对该请求进行路由
ctx.setResponseStatusCode(200);
return null;
}
//认证失败
Map result = new HashMap<>();
result.put("code","1");
result.put("msg","请登录");
ctx.getResponse().setContentType("application/json;charset=utf-8");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
ctx.setResponseBody(JSON.toJSONString(result));// 返回错误内容
return null;
} catch (NoSuchAlgorithmException e) {
log.error("generatePVID error", e);
}
return null;
}
/**
* 是否登录认证
* @param uri 请求接口标志
* @return
*/
private Boolean isIgore(String uri) {
String s = uri.replaceAll(contextPath,"");
for(String reg : ZuulInit.getIgnoreUrl()){
if(s.matches(reg)){
return true;
}
}
return false;
}
/**
* 是否刷新过期时间
* @param uri
* @return
*/
private Boolean isRefreshExpire(String uri){
String s = uri.replaceAll(contextPath,"");
for(String reg : ZuulInit.getExpireUrl()){
if(s.matches(reg)){
return true;
}
}
return false;
}
/**
* 检查过期时间,并刷新过期时间
* @param sessionId
* @return
*/
private Boolean checkLoginWithExpire(String sessionId){
String key = SESSION_PREFIX + domain + "_" + sessionId;
String userId = (String)redisTemplate.opsForValue().get(key);
String userKey = USER_PREFIX + "_" + userId;
String dataKey = DATA_PREFIX + "_" + sessionId;
if(!StringUtil.isEmpty(userId)){
Boolean r = redisTemplate.expire(key, expire,TimeUnit.SECONDS);
//刷新时间没有成功,返回认证不通过
if(!r){
return false;
}
r = redisTemplate.expire(userKey, expire, TimeUnit.SECONDS);
if(!r){
return false;
}
r = redisTemplate.expire(dataKey, expire, TimeUnit.SECONDS);
if(!r){
return false;
}
return true;
}
return false;
}
/**
* 检查过期时间
* @param sessionId
* @return
*/
private Boolean checkLoginWithoutExpire(String sessionId){
String key = SESSION_PREFIX + domain + "_" + sessionId;
String userId = (String)redisTemplate.opsForValue().get(key);
if(!StringUtil.isEmpty(userId)){
return true;
}
return false;
}
如上代码只是一个简单的功能实现,还需要在此基础上面向对象的设计、优化等考虑。
(完)