1.shiro中session的共享问题
当我们通过nginx反向代理到我们的集群后,如果用户再进行登录,用户的session会存储在第一次负载均衡到的服务器上,但是如果我们调用其中的一些功能或者说权限后,用户在另外一台服务器上并没有session信息,就会导致提示未登录问题,需要用户再次进行登录。
但是我们不可能让用户登录很多次,这对用户的体验来说是非常不友好的,因此解决shiro安全框架中的session问题是非常有必要的。
2.如何解决session共享问题
这里使用的是第三方的框架crazycake
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.3.1</version>
</dependency>
2.1修改shiro的配置类
DefaultWebSessionManager类主要用于获取请求头中的JSESSIONID的值
然后再通过RedisSessionDao从redis中查询对应的Key,如果存在,则认为当前用户已登录
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm());
//redis管理
securityManager.setCacheManager(redisCacheManager());
//设置session管理
securityManager.setSessionManager(defaultWebSessionManager());
return securityManager;
}
@Bean
public DefaultWebSessionManager defaultWebSessionManager(){
MyWebSessionManager defaultWebSessionManager = new MyWebSessionManager();
defaultWebSessionManager.setSessionDAO(sessionDAO());
return defaultWebSessionManager;
}
@Bean
public SessionDAO sessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
RedisManager redisManager = new RedisManager();
redisManager.setHost("127.0.0.1:6379");
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
2.2解决前端不支持cookie
默认DefaultWebSessionManager它只接受Cookie中存储的JSESSIONID.
查询发现redis中不存在对应的key
如何解决:
客户发送请求时,再请求头中携带sessionId, 然后重写DefaultWebSessionManager中getSessionId()的方法。
2.3JSESSIONID放入请求头
修改登录的接口
修改前端登录方法
修改main.js文件
2.4 重写DefaultWebSessionManager的方法
public class MyWebSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//获取请求头中名称为token的内容
String id = WebUtils.toHttp(request).getHeader("token");
if (!StringUtils.isEmpty(id)) { //如果存在该token
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "Stateless request");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//从cookie中获取sessionId.
return super.getSessionId(request, response);
}
}
}
修改shiro配置类:
@Bean
public DefaultWebSessionManager defaultWebSessionManager(){
//使用自定义的defaultWebSessionManager
MyWebSessionManager defaultWebSessionManager = new MyWebSessionManager();
defaultWebSessionManager.setSessionDAO(sessionDAO());
return defaultWebSessionManager;
}
修改shiroFilter过滤器
我们发现跨域请求,会发送两个请求:第一个OPTIONS请求,第二个请求是真实的请求。
OPTIONS请求:先头部队。
所以我们对OPTIONS请求都要放行
public class MyFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//响应数据的中文乱码
response.setContentType("application/json;charset=utf-8");
//返回的json数据
R<Object> error = R.error("登录了吗你?????");
//转换为json字符串返回
String string = JSON.toJSONString(error);
//响应前端
PrintWriter writer = response.getWriter();
writer.print(string);
writer.flush();
writer.close();
return false;
}
//会发送两个请求:第一个OPTIONS请求,第二个请求是真实的请求。
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
//获取请求方式
String method = req.getMethod();
if("OPTIONS".equals(method)){
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}
}
3.前端前置路由守卫
//前置路由守卫设置
router.beforeEach((to,from,next) => {
//获取当前路由路径
var path = to.path;
if(path == "/login"){
return next();//继续放行
}
//判断是否登录
var token = sessionStorage.getItem("token");
if(token){
return next();
}
//返回到主界面登录
return next("/login")
})
4.防止恶意重复登录
@Controller
@ResponseBody
public class LoginController {
@PostMapping("/login")
public R login(@RequestBody LoginVo loginVo){
Subject subject = SecurityUtils.getSubject();
//判断是否已经登录 防止重复登录查询数据库
if (subject.isAuthenticated()) {
return R.success(subject.getSession().getId().toString());
}
//封装用户登录信息
UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getUsername(),loginVo.getPassword());
try {
//进行登录认证
subject.login(token);
return R.success(subject.getSession().getId().toString());//返回sessionId给前端
}catch (Exception e){
e.printStackTrace();
return R.error("登录失败");
}
}
}
5.退出登录
@GetMapping("/logout")
public R logout(){
//清除redis缓存
Subject subject = SecurityUtils.getSubject();
subject.logout();
return R.success("退出成功");
}
前端退出:
//退出登录
logout(){
this.$http.get("http://localhost:8088/user/logout").then(result => {
if(result.data.code == 1){
this.$message.success("退出成功")
//清除token
sessionStorage.clear()
//返回登录界面
this.$router.push("/login")
}
})