修改用户登录过期时间机制

master
zengwh 5 years ago
parent 73d2842c2c
commit b7100325e2

@ -247,15 +247,13 @@ public class FontController {
@ResponseBody @ResponseBody
public Msg checkToken(String token) throws Exception{ public Msg checkToken(String token) throws Exception{
if(StringUtils.isNotBlank(token) && StringUtils.isNotBlank(token) ) { if(StringUtils.isNotBlank(token) && StringUtils.isNotBlank(token) ) {
token = MD5.JM(Base64.decode(token));
Cache cache = CacheManager.getCacheInfo(token); Cache cache = CacheManager.getCacheInfo(token);
if (cache == null) { if (cache == null) {
return Msg.fail("token已过期或不存在"); return Msg.fail("token已过期或不存在");
}else{
Power_UserVo user = (Power_UserVo)cache.getValue();
CacheManager.putCache(token,new Cache(user,System.currentTimeMillis(),TOKEN_EXPIRE_TIME*1000));
} }
//更新过期时间
Power_UserVo user = (Power_UserVo) cache.getValue();
String date = String.valueOf(DateUtils.getDate());
CacheManager.putCache(token,new Cache(date,user,TOKEN_EXPIRE_TIME));
}else{ }else{
return Msg.fail("token不能为空"); return Msg.fail("token不能为空");
} }
@ -284,43 +282,43 @@ public class FontController {
if(StringUtils.isBlank(sysFlag)){ if(StringUtils.isBlank(sysFlag)){
return Msg.fail("sysFlag不能为空!"); return Msg.fail("sysFlag不能为空!");
} }
token = MD5.JM(Base64.decode(token));
Cache cacheInfo = CacheManager.getCacheInfo(token); Cache cacheInfo = CacheManager.getCacheInfo(token);
Power_UserVo user = (Power_UserVo) cacheInfo.getValue(); if(null != cacheInfo){
if(null != user){ Power_UserVo user = (Power_UserVo) cacheInfo.getValue();
List<User_Dept_Menu> menuList = user.getMenuList(); if(null != user) {
List<User_Dept_Menu> list = new ArrayList<>(); List<User_Dept_Menu> menuList = user.getMenuList();
Set<String> menus = new TreeSet<>(); List<User_Dept_Menu> list = new ArrayList<>();
if(null != menuList && !menuList.isEmpty()){ Set<String> menus = new TreeSet<>();
for (User_Dept_Menu deptMenu : menuList) { if (null != menuList && !menuList.isEmpty()) {
String menuSysFlag = deptMenu.getSysFlag(); for (User_Dept_Menu deptMenu : menuList) {
if (StringUtils.isNotBlank(menuSysFlag) && menuSysFlag.equals(sysFlag)) { String menuSysFlag = deptMenu.getSysFlag();
list.add(deptMenu); if (StringUtils.isNotBlank(menuSysFlag) && menuSysFlag.equals(sysFlag)) {
if (StringUtils.isNotBlank(deptMenu.getMethod())) { list.add(deptMenu);
menus.add(deptMenu.getMenuUrl()); if (StringUtils.isNotBlank(deptMenu.getMethod())) {
menus.add(deptMenu.getMenuUrl());
}
} }
} }
} }
user.setMenuList(list);
user.setMenus(menus);
UserVo userVo = new UserVo();
BeanUtils.copyProperties(user, userVo);
//查询用户集合
List<User> userList = new ArrayList<>();
Integer roleId = userVo.getRoleId();
if (roleId == 0) {
userList = userMapper.selectUserIdAndUserNameList(null);
} else {
userList = userMapper.selectUserIdAndUserNameList(userVo.getUserId());
}
//设置用户集合
userVo.setUserList(userList);
CacheManager.addExcCount("noExc");
return Msg.success().add("user", userVo);
} }
user.setMenuList(list);
user.setMenus(menus);
UserVo userVo = new UserVo();
BeanUtils.copyProperties(user,userVo);
//查询用户集合
List<User> userList = new ArrayList<>();
Integer roleId = userVo.getRoleId();
if(roleId == 0){
userList = userMapper.selectUserIdAndUserNameList(null);
}else{
userList = userMapper.selectUserIdAndUserNameList(userVo.getUserId());
}
//设置用户集合
userVo.setUserList(userList);
CacheManager.addExcCount("noExc");
return Msg.success().add("user",userVo);
}else{
return Msg.fail("token已失效");
} }
return Msg.fail("token已失效");
} }
/** /**
@ -338,28 +336,31 @@ public class FontController {
*/ */
@RequestMapping(value = "getMenuByToken",method = RequestMethod.POST) @RequestMapping(value = "getMenuByToken",method = RequestMethod.POST)
@ResponseBody @ResponseBody
public Msg getMenuByToken(String token,String sysFlag) throws Exception{ public Msg getMenuByToken(String token,String sysFlag) throws Exception {
if(StringUtils.isBlank(token)){ if (StringUtils.isBlank(token)) {
return Msg.fail("token不能为空!"); return Msg.fail("token不能为空!");
} }
if(StringUtils.isBlank(sysFlag)){ if (StringUtils.isBlank(sysFlag)) {
return Msg.fail("sysFlag不能为空!"); return Msg.fail("sysFlag不能为空!");
} }
token = MD5.JM(Base64.decode(token));
Cache cacheInfo = CacheManager.getCacheInfo(token); Cache cacheInfo = CacheManager.getCacheInfo(token);
Power_UserVo user = (Power_UserVo) cacheInfo.getValue(); if (null != cacheInfo) {
List<User_Dept_Menu> menuList = user.getMenuList(); Power_UserVo user = (Power_UserVo) cacheInfo.getValue();
List<User_Dept_Menu> list = new ArrayList<>(); List<User_Dept_Menu> menuList = user.getMenuList();
if(null != menuList && !menuList.isEmpty()){ List<User_Dept_Menu> list = new ArrayList<>();
for (User_Dept_Menu dept_menu : menuList) { if (null != menuList && !menuList.isEmpty()) {
String menuSysFlag = dept_menu.getSysFlag(); for (User_Dept_Menu deptMenu : menuList) {
if (StringUtils.isNotBlank(menuSysFlag) && menuSysFlag.equals(sysFlag)) { String menuSysFlag = deptMenu.getSysFlag();
list.add(dept_menu); if (StringUtils.isNotBlank(menuSysFlag) && menuSysFlag.equals(sysFlag)) {
list.add(deptMenu);
}
} }
} }
CacheManager.addExcCount("noExc");
return Msg.success().add("list", list);
}else{
return Msg.fail("token已失效");
} }
CacheManager.addExcCount("noExc");
return Msg.success().add("list",list);
} }
/** /**
@ -395,9 +396,7 @@ public class FontController {
if(null == userVo){ if(null == userVo){
return Msg.fail("用户名或密码不正确"); return Msg.fail("用户名或密码不正确");
} }
String date = String.valueOf(DateUtils.getDate()); String token = UUID.randomUUID().toString();
String token = Base64.encode(MD5.KL(date));
List<Power_Menu> list = null; List<Power_Menu> list = null;
List<User_Dept_Menu> menuList = new ArrayList<>(); List<User_Dept_Menu> menuList = new ArrayList<>();
Set<String> menus = new TreeSet<>(); Set<String> menus = new TreeSet<>();
@ -434,9 +433,9 @@ public class FontController {
} }
} }
userVo.setRemark(powerDepts.toString()); userVo.setRemark(powerDepts.toString());
//移除缓存 ActionScopeUtils.setSessionAttribute("token",token,Integer.valueOf(String.valueOf(TOKEN_EXPIRE_TIME)));
CacheManager.removeCacheByObject(userVo); ActionScopeUtils.setSessionAttribute("CURRENT_USER",user,Integer.valueOf(String.valueOf(TOKEN_EXPIRE_TIME)));
CacheManager.putCache(date,new Cache(date,userVo,TOKEN_EXPIRE_TIME)); CacheManager.putCache(token,new Cache(user,System.currentTimeMillis(),TOKEN_EXPIRE_TIME*1000));
return Msg.success().add("token",token); return Msg.success().add("token",token);
} }

@ -54,31 +54,22 @@ public class LoginController {
Power_UserVo user = powerUserService.findPowerUserByUserNameAndUserPwd(powerUser); Power_UserVo user = powerUserService.findPowerUserByUserNameAndUserPwd(powerUser);
//添加进操作日志 //添加进操作日志
Power_Log log = new Power_Log(); Power_Log log = new Power_Log();
if( user != null){ if(user != null){
//如处于登录状态,先清除缓存 //如处于登录状态,先清除缓存
//CacheManager.removeCacheByObject(user); //CacheManager.removeCacheByObject(user);
//记住 //记住
MyCookieUtil.remember(request, response); MyCookieUtil.remember(request, response);
//清除用户登录错误次数缓存
CacheManager.clearOnly(powerUser.getUserName());
//存session密码置空 //存session密码置空
//是否记住密码功能 //是否记住密码功能
MyCookieUtil.remember(request, response); MyCookieUtil.remember(request, response);
//设置token缓存 //设置token缓存
String date = String.valueOf(DateUtils.getDate()); String token = UUID.randomUUID().toString();
String token = Base64.encode(MD5.KL(date));
//查询归属医院 //查询归属医院
/* long start5 = System.currentTimeMillis(); /*long start5 = System.currentTimeMillis();
Power_User_Dict powerUserDict = powerUserDictMapper.selectDictIdByUserId(user.getUserId()); Power_User_Dict powerUserDict = powerUserDictMapper.selectDictIdByUserId(user.getUserId());
long end5 = System.currentTimeMillis(); long end5 = System.currentTimeMillis();
System.out.println("查询医院时间="+(end5-start5)/1000.0+"s"); System.out.println("查询医院时间="+(end5-start5)/1000.0+"s");
user.setDictId(powerUserDict.getDictId());*/ user.setDictId(powerUserDict.getDictId());*/
//科室id科室名
ActionScopeUtils.setSessionAttribute("token",token,Integer.valueOf(String.valueOf(TOKEN_EXPIRE_TIME))/1000);
//设置用户登录次数缓存 //设置用户登录次数缓存
//CacheManager.addloginUserCount(fmt.format(new Date()),user.getUserName()); //CacheManager.addloginUserCount(fmt.format(new Date()),user.getUserName());
CacheManager.addExcCount("noExc"); CacheManager.addExcCount("noExc");
@ -106,7 +97,6 @@ public class LoginController {
} }
user.setMenuList(menuList); user.setMenuList(menuList);
user.setMenus(menus); user.setMenus(menus);
//设置科室 //设置科室
StringBuilder powerDepts = new StringBuilder(); StringBuilder powerDepts = new StringBuilder();
List<Power_Dept> powerDeptsList = power_deptService.selectByPrimaryKeys(user.getDeptId()); List<Power_Dept> powerDeptsList = power_deptService.selectByPrimaryKeys(user.getDeptId());
@ -118,10 +108,12 @@ public class LoginController {
} }
} }
user.setRemark(powerDepts.toString()); user.setRemark(powerDepts.toString());
//清除用户登录错误次数缓存
CacheManager.clearOnly(powerUser.getUserName());
//设置进缓存 //设置进缓存
CacheManager.putCache(date,new Cache(date,user,TOKEN_EXPIRE_TIME)); CacheManager.putCache(token,new Cache(user,System.currentTimeMillis(),TOKEN_EXPIRE_TIME*1000));
ActionScopeUtils.setSessionAttribute("CURRENT_USER",user,Integer.valueOf(String.valueOf(TOKEN_EXPIRE_TIME))/1000); ActionScopeUtils.setSessionAttribute("token",token,Integer.valueOf(String.valueOf(TOKEN_EXPIRE_TIME)));
ActionScopeUtils.setSessionAttribute("CURRENT_USER",user,Integer.valueOf(String.valueOf(TOKEN_EXPIRE_TIME)));
return "redirect:gatewayPage"; return "redirect:gatewayPage";
}else{ }else{
//登录失败 //登录失败
@ -133,9 +125,8 @@ public class LoginController {
//叠加1 //叠加1
wrongNum += currentNum; wrongNum += currentNum;
} }
//先清除后添加缓存 //添加缓存
CacheManager.clearOnly(powerUser.getUserName()); CacheManager.putCache(powerUser.getUserName(),new Cache(wrongNum));
CacheManager.putCache(powerUser.getUserName(),new Cache(powerUser.getUserName(),wrongNum));
log.setCreater(powerUser.getUserName()); log.setCreater(powerUser.getUserName());
log.setLogTitle("登录"); log.setLogTitle("登录");
log.setLogContent("用户密码错误"); log.setLogContent("用户密码错误");

@ -38,23 +38,18 @@ public class LoginInterceptor implements HandlerInterceptor {
if(!"/".equals(url)){ if(!"/".equals(url)){
parentUrl = "/"+s[1]; parentUrl = "/"+s[1];
} }
if(!"/getSessionRemainingTime".equals(parentUrl)){
request.getSession().setAttribute(request.getSession().getId(),System.currentTimeMillis());
}
if (excludes(parentUrl, Constant.RELEASE_REQUEST)) { if (excludes(parentUrl, Constant.RELEASE_REQUEST)) {
response.setHeader("Access-Control-Allow-Origin","*"); response.setHeader("Access-Control-Allow-Origin","*");
return true; return true;
}else{ }else{
String token = (String)request.getSession().getAttribute("token"); String token = (String)request.getSession().getAttribute("token");
if(StringUtils.isNoneBlank(token)){ if(StringUtils.isNoneBlank(token)){
token = MD5.JM(Base64.decode(token));
Cache cache = CacheManager.getCacheInfo(token); Cache cache = CacheManager.getCacheInfo(token);
if (cache != null) { if (cache != null) {
if(!"/getSessionRemainingTime".equals(parentUrl)) { if(!"/getSessionRemainingTime".equals(parentUrl)) {
//更新过期时间 //重新更新过期时间
Power_UserVo user = (Power_UserVo) cache.getValue(); Power_UserVo user = (Power_UserVo) cache.getValue();
String date = String.valueOf(DateUtils.getDate()); CacheManager.putCache(token, new Cache(user,System.currentTimeMillis(),TOKEN_EXPIRE_TIME*1000));
CacheManager.putCache(token, new Cache(date, user, TOKEN_EXPIRE_TIME));
} }
return true; return true;
} }

@ -34,7 +34,6 @@ public class PowerWebServiceImpl implements PowerWebService {
public String getInfosByUserId(String token,String sysFlag) { public String getInfosByUserId(String token,String sysFlag) {
Power_UserWebServiceVo userWebServiceVo = new Power_UserWebServiceVo(); Power_UserWebServiceVo userWebServiceVo = new Power_UserWebServiceVo();
if(StringUtils.isNotBlank(token)){ if(StringUtils.isNotBlank(token)){
token = MD5.JM(Base64.decode(token));
Cache cache = CacheManager.getCacheInfo(token); Cache cache = CacheManager.getCacheInfo(token);
if(cache != null){ if(cache != null){
Power_UserVo user = (Power_UserVo) cache.getValue(); Power_UserVo user = (Power_UserVo) cache.getValue();

@ -4,8 +4,8 @@
releaseRequest = /login,/logout,/services,/font,/refuse,/swagger-ui.html,/webjars,/swagger-resources,/v2 releaseRequest = /login,/logout,/services,/font,/refuse,/swagger-ui.html,/webjars,/swagger-resources,/v2
ajaxRequest = none ajaxRequest = none
#session\u8FC7\u671F\u65F6\u95F4 #session\u8FC7\u671F\u65F6\u95F4,\u5355\u4F4D\u79D2
TOKEN_EXPIRE_TIME = 1200000 TOKEN_EXPIRE_TIME = 7200
##################################################\u670D\u52A1\u5668ip########################################################## ##################################################\u670D\u52A1\u5668ip##########################################################
#\u901A\u7528\u670D\u52A1\u5668IP\u4E0E\u901A\u7528\u670D\u52A1\u5668\u7AEF\u53E3 #\u901A\u7528\u670D\u52A1\u5668IP\u4E0E\u901A\u7528\u670D\u52A1\u5668\u7AEF\u53E3

@ -148,7 +148,7 @@
} }
</style> </style>
</head> </head>
<body class="hold-transition skin-blue sidebar-mini"> <body class="hold-transition skin-blue">
<%--<a href="http://192.168.1.3:8080/emr_record/login?token=IxEQVDobAlREQlRFQk5HTE5BRFQ3JyBURkRFTQ==&userName=1137">hhhhhhhhhhhhhhhh</a>--%> <%--<a href="http://192.168.1.3:8080/emr_record/login?token=IxEQVDobAlREQlRFQk5HTE5BRFQ3JyBURkRFTQ==&userName=1137">hhhhhhhhhhhhhhhh</a>--%>
<input type="hidden" id="userId" value="${CURRENT_USER.userId}"> <input type="hidden" id="userId" value="${CURRENT_USER.userId}">
<input type="hidden" id="webSocketUrl" value="${WEBSOCKET_URLHEAD}"> <input type="hidden" id="webSocketUrl" value="${WEBSOCKET_URLHEAD}">
@ -210,9 +210,9 @@
<li> <li>
<div class="margin"> <div class="margin">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-default">操作</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-expanded="false" style="height:34px"> aria-expanded="false" style="height:34px">
操作
<span class="caret"></span> <span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span> <span class="sr-only">Toggle Dropdown</span>
</button> </button>

@ -31,7 +31,7 @@
overflow-y: hidden; overflow-y: hidden;
} }
</style> </style>
<body class="hold-transition skin-blue sidebar-mini" scroll="no"> <body class="hold-transition skin-blue" scroll="no">
<input type="hidden" id="userId" value="${CURRENT_USER.userId}"> <input type="hidden" id="userId" value="${CURRENT_USER.userId}">
<input type="hidden" id="webSocketUrl" value="${WEBSOCKET_URLHEAD}"> <input type="hidden" id="webSocketUrl" value="${WEBSOCKET_URLHEAD}">
<input type="hidden" id="strSplit" value="${STR_SPLIT}"> <input type="hidden" id="strSplit" value="${STR_SPLIT}">
@ -90,9 +90,9 @@
<li> <li>
<div class="margin"> <div class="margin">
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-default">操作</button>
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-expanded="false" style="height:34px"> aria-expanded="false" style="height:34px">
操作
<span class="caret"></span> <span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span> <span class="sr-only">Toggle Dropdown</span>
</button> </button>

@ -6,60 +6,40 @@ package com.manage.service.cache;
* @Created by ljx * @Created by ljx
*/ */
public class Cache { public class Cache {
private String key;//缓存ID
private Object value;//缓存数据 private Object value;//缓存数据
private long timeOut;//更新时间 private long loginMill;//登录毫秒数
private boolean expired; //是否终止 private long timeOut;//过期时间毫秒数
public Cache() {
super();
}
public Cache(String key, Object value, long timeOut, boolean expired) {
this.key = key;
this.value = value;
this.timeOut = timeOut;
this.expired = expired;
}
public Cache(String key, Object value, long timeOut) { public Cache(Object value,long loginMill,long timeOut) {
this.key = key;
this.value = value; this.value = value;
this.loginMill = loginMill;
this.timeOut = timeOut; this.timeOut = timeOut;
} }
public Cache(String key, Object value) { public Cache(Object value) {
this.key = key;
this.value = value; this.value = value;
} }
public String getKey() {
return key;
}
public long getTimeOut() {
return timeOut;
}
public Object getValue() { public Object getValue() {
return value; return value;
} }
public void setKey(String string) { public void setValue(Object value) {
key = string; this.value = value;
} }
public void setTimeOut(long l) { public long getLoginMill() {
timeOut = l; return loginMill;
} }
public void setValue(Object object) { public void setLoginMill(long loginMill) {
value = object; this.loginMill = loginMill;
} }
public boolean isExpired() { public long getTimeOut() {
return expired; return timeOut;
} }
public void setExpired(boolean b) { public void setTimeOut(long timeOut) {
expired = b; this.timeOut = timeOut;
} }
} }

@ -2,10 +2,7 @@ package com.manage.service.cache;
import com.manage.vo.Power_UserVo; import com.manage.vo.Power_UserVo;
import java.util.ArrayList; import java.util.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class CacheManager { public class CacheManager {
private static HashMap cacheMap = new HashMap(); private static HashMap cacheMap = new HashMap();
@ -21,84 +18,90 @@ public class CacheManager {
return (Cache) cacheMap.get(key); return (Cache) cacheMap.get(key);
} }
private static boolean hasCache(String key) { private static boolean hasCache(String key) {
return cacheMap.containsKey(key); return cacheMap.containsKey(key);
} }
public static void clearAll() { public static void clearAll() {
cacheMap.clear(); cacheMap.clear();
} }
public static void clearAll(String type) { public static void clearAll(String type) {
Iterator i = cacheMap.entrySet().iterator(); Iterator i = cacheMap.entrySet().iterator();
String key; String key;
ArrayList<String> arr = new ArrayList<String>();
try { try {
while (i.hasNext()) { while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next(); Map.Entry entry = (Map.Entry) i.next();
key = (String) entry.getKey(); key = (String) entry.getKey();
if (key.startsWith(type)) { if (key.equals(type)) {
arr.add(key); clearOnly(key);
} }
} }
for (String s : arr) {
clearOnly(s);
}
} catch (Exception ex) { } catch (Exception ex) {
ex.printStackTrace(); ex.printStackTrace();
} }
} }
public static void clearOnly(String key) { public static void clearOnly(String key) {
cacheMap.remove(key); cacheMap.remove(key);
} }
public static void putCache(String key, Cache obj) { public static void putCache(String key, Cache obj) {
cacheMap.put(key, obj); cacheMap.put(key, obj);
//移除不属于该token的
Power_UserVo powerUser = (Power_UserVo)obj.getValue();
CacheManager.removeCacheByObject(powerUser,key);
} }
public static Cache getCacheInfo(String key) { public static Cache getCacheInfo(String key) {
if (hasCache(key)) { if (hasCache(key)) {
Cache cache = getCache(key); Cache cache = getCache(key);
if (cacheExpired(cache)) { if (cacheExpired(cache)) {
cache.setExpired(true); //过期,移除
clearOnly(key);
return null;
} }
return cache; return cache;
}else }else {
return null; return null;
}
} }
//是否过期
private static boolean cacheExpired(Cache cache) { private static boolean cacheExpired(Cache cache) {
if (null == cache) { if (null == cache) {
return false; return false;
} }
long nowDt = System.currentTimeMillis(); long nowDt = System.currentTimeMillis();
long loginMill = cache.getLoginMill();
long cacheDt = cache.getTimeOut(); long cacheDt = cache.getTimeOut();
if (cacheDt <= 0||cacheDt>nowDt) { try {
return false; long checkTime = nowDt - loginMill;
} else { if (checkTime >= cacheDt) {
return true;
} else {
return false;
}
}catch (Exception e){
e.printStackTrace();
return true; return true;
} }
} }
//根据用户信息删除缓存 //根据用户信息删除缓存
public synchronized static void removeCacheByObject(Power_UserVo obj) { private static void removeCacheByObject(Power_UserVo obj,String token) {
ArrayList<String> arr = new ArrayList<String>();
try { try {
Iterator i = cacheMap.entrySet().iterator(); Iterator i = cacheMap.entrySet().iterator();
while (i.hasNext()) { while (i.hasNext()) {
Map.Entry entry = (Map.Entry) i.next(); Map.Entry<String,Object> entry = (Map.Entry) i.next();
Cache cache = CacheManager.getCacheInfo((String)entry.getKey()); Cache cache = CacheManager.getCacheInfo(entry.getKey());
Power_UserVo o = (Power_UserVo)cache.getValue(); Power_UserVo o = (Power_UserVo)cache.getValue();
if (obj.getUserName().equals(o.getUserName())) { if (obj.getUserName().equals(o.getUserName()) && !entry.getKey().equals(token)) {
arr.add((String)entry.getKey()); clearOnly(entry.getKey());
}
}
if(!arr.isEmpty()){
for (String s : arr) {
clearOnly(s);
} }
} }
} catch (Exception ignored) {} } catch (Exception e) {
e.printStackTrace();
}
} }
} }
Loading…
Cancel
Save