一:技术概况及背景需求:
1:以下介绍的项目所涉及到的技术有,Spingboot、Security、Mysql、Redis。故此集成只适用于以上项目。
2:现在有这样一个需求。从平台登录用户,子系统的入口在平台。通过平台可以访问子系统同时还能拿到平台登录的用户信息,无token或token过期不能访问系统
二:实现思路分析;
(1):通过共享Redis。从Redis中获取用户信息。俩个系统之间共享Redis。子系统用过滤器去拦截,但是难点在于解析token获取Redis的Key以及拿到的用户信息如果去做一个全局方法去调用(本文就是用这种思路去实现的)
(2):使用nacos注册中心然后远程去调用有权限系统的接口去获取登录验证以及用户信息(耦合性较高)
(3):用GetWay网关去做拦截和放行处理,但是后续自己要处理解析Token获取用户信息
三:实现过程
(1):俩个系统之间配置相同的Redis(端口号等需要保持一致)
(2);编写拦截器去拦截请求,并对token进行验证(token是否过去,token是否正确)以及对token过期时间的延长。
(2.1)编写过滤器配置类(配置需要过滤的请求)
@Configuration
public class FilterConfig {
@Autowired
MyFilter myFilter;
@Bean
public FilterRegistrationBean filterRegistrationBean(){
//注册过滤器(初始化过滤器)
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
//添加过滤的路径,凡是路径带/user就进入过滤器
filterRegistrationBean.addUrlPatterns("/informationnumber/*");
return filterRegistrationBean;
}
}
(2.2)过滤器实现业务逻辑
* 作者:kt
* 时间:2023/3/1 11:28
* 描述:过滤器
*/
@Component
public class MyFilter implements Filter {
String secret="abcdefghijklmnopqrstuvwxyz";
@Autowired
private RedisCache redisCache;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("-----------------过滤器初始化----------------");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//校验用户登录状态
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String token = request.getHeader("token");
if (token==null){
response.getWriter().write(JSON.toJSONString(ResponseObj.fail("403")));
return;
}
//第一次登录没有token,给null会报错,所以我们判断一下token是否为空,为空给一个空串
//三元运算
token = token == null ? "" : token;
//查询Redis中token的剩余时间
Claims claims = parseToken(token);
//拼接token
String A=claims.toString().split("=")[1].replace("}","");
String userKey="login_tokens:"+A;
//获取用户信息 拿着userKey去redis里面获取用户信息
Object user= redisCache.getCacheObject(userKey);
LoginUser user1 = JSON.parseObject(JSON.toJSONString(user), LoginUser.class);
//创建session
// HttpSession session = request.getSession();
// session.setAttribute("token",token);//设置token,参数token是要设置的具体值
Long expire = user1.getExpireTime();
// //获取sesion中的token
// session.getAttribute("token");
// System.out.println(session.getAttribute("token"));//在需要使获取token;
if (expire >System.currentTimeMillis()) {
//时间大于0 放行
/*
有一个问题,用户一直在网页上操作,但是token一直在倒计时,操作着操作着就变未登录了
不合理,所以,当用户在操作时,每次经过过滤器调用不同接口时,就重置一下token的时间
*/
//更新过期时间
user1.setExpireTime(user1.getLoginTime() + 1800000*60000);
String dock = JSON.toJSONString(user1);
response.setHeader("user",dock);
//放行
filterChain.doFilter(servletRequest, servletResponse);
} else {
//token过期、if (user1.getExpireTime() - System.currentTimeMillis() <= 20)都会else
//将返回信息放入响应体中
response.getWriter().write(JSON.toJSONString(ResponseObj.fail("403")));
System.out.println("未登录!");
return ;
}
}
@Override
public void destroy() {
System.out.println("过滤器已经死亡!");
}
//解析token方法
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
}
(3)获取用户信息后将用户信息放入Response中然后编写全局方法去获取用户信息(实例为从Response中获取),当然也可以考虑放入session中,编写从session中获取用户信息的方法。
package com.zkyq.acss.util;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.zkyq.acss.entity.LoginUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class TokenUtil {
public LoginUser getCurrentUser() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
HttpServletResponse response = servletRequestAttributes.getResponse();
String userInfo = response.getHeader("user");
if (StringUtils.isEmpty(userInfo)) {
return null;
}
Object parse = JSON.parse(userInfo);
LoginUser user = JSONObject.parseObject(parse.toString(), LoginUser.class);
return user;
}
}
四:踩坑提示:
(1)俩个子系统的Redis的序列化工具和权限系统的Redis序列化工具需要保持一致;
(2)俩个系统之间的JWT解密方式要一致,签名方式也必须一致;
(3)jwt验证token报错:io.jsonwebtoken.UnsupportedJwtException: Signed Claims JWSs are not supported的解决;
io.jsonwebtoken.UnsupportedJwtException: Signed Claims JWSs are not supported.
at io.jsonwebtoken.JwtHandlerAdapter.onClaimsJws(JwtHandlerAdapter.java:50)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:487)
at io.jsonwebtoken.impl.DefaultJwtParser.parseClaimsJwt(DefaultJwtParser.java:514)
at com.glzt.test.util.JwtUtils.getClaimsFromToken(JwtUtils.java:120)
at com.glzt.test.util.JwtUtils.memoryUserInRedis(JwtUtils.java:185)
at com.glzt.test.util.JwtUtils.generateToken(JwtUtils.java:162)
at com.glzt.test.util.JwtUtils.generateToken(JwtUtils.java:148)
// 错误写法:parseClaimsJwt
Jwts.parser().setSigningKey(SECRET).parseClaimsJwt(token.replace(TOKEN_PREFIX, "")).getBody();
// 正确写法:parseClaimsJws
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();