簡單回顧cookie和session
- cookie和session都是回話管理的方式
- Cookie
- cookie是瀏覽器端存儲信息的一種方式
- 服務端可以通過響應瀏覽器set-cookie標頭(header),瀏覽器接收到這個標頭信息后,將以文件形式將cookie信息保存在瀏覽器客戶端的計算機上。之后的請求,瀏覽器將該域的cookie信息再一並發送給服務端
- cookie默認的存活期限關閉瀏覽器后失效,即瀏覽器在關閉時清除cookie文件信息。我們可以在服務端響應cookie時,設置其存活期限,比如設為一周,這樣關閉瀏覽器后也cookie還在期限內沒有被清除,下次請求瀏覽器就會將其發送給服務端了
- Session
- session的使用是和cookie緊密關聯的
- cookie存儲在客戶端(瀏覽器負責記憶),session存儲在服務端(在Java中是web容器對象,服務端負責記憶)
- 每個session對象有一個sessionID,這個ID值還是用cookie方式存儲在瀏覽器,瀏覽器發送cookie,服務端web容器根據cookie中的sessionID得到對應的session對象,這樣就能得到各個瀏覽器的“會話”信息
- 正是因為sessionID實際使用的cookie方式存儲在客戶端,而cookie默認的存活期限是瀏覽器關閉,所以session的“有效期”即是瀏覽器關閉
開發環境
- JDK8、Maven3.5.3、springboot2.1.6、STS4
- node10.16、npm6.9、vue2.9、element-ui、axios
springboot后端提供接口
- demo 已放置 Gitee
- 本次 demo 只需要 starter-web
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 后台接口只提供接口服務,端口8080
application.properties
server.port=8080
- 只有一個controller,里面有3個handle,分別是登錄、注銷和正常請求
TestCtrller.java
@RestController
public class TestCtrller extends BaseCtrller{
//session失效化-for功能測試
@GetMapping("/invalidateSession")
public BaseResult invalidateSession(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session != null &&
session.getAttribute(SysConsts.Session_Login_Key)!=null) {
request.getSession().invalidate();
getServletContext().log("Session已注銷!");
}
return new BaseResult(true);
}
//模擬普通ajax數據請求(待登錄攔截的)
@GetMapping("/hello")
public BaseResult hello(HttpServletRequest request) {
getServletContext().log("登錄session未失效,繼續正常流程!");
return new BaseResult(true, "登錄session未失效,繼續正常流程!");
}
//登錄接口
@PostMapping("/login")
public BaseResult login(@RequestBody SysUser dto, HttpServletRequest request) {
//cookie信息
Cookie[] cookies = request.getCookies();
if(null!=cookies && cookies.length>0) {
for(Cookie c:cookies) {
System.out.printf("cookieName-%s, cookieValue-%s, cookieAge-%d%n", c.getName(), c.getValue(), c.getMaxAge());
}
}
/**
* session處理
*/
//模擬庫存數據
SysUser entity = new SysUser();
entity.setId(1);
entity.setPassword("123456");
entity.setUsername("Richard");
entity.setNickname("Richard-管理員");
//驗密
if(entity.getUsername().equals(dto.getUsername()) && entity.getPassword().equals(dto.getPassword())) {
if(request.getSession(false) != null) {
System.out.println("每次登錄成功改變SessionID!");
request.changeSessionId(); //安全考量,每次登陸成功改變 Session ID,原理:原來的session注銷,拷貝其屬性建立新的session對象
}
//新建/刷新session對象
HttpSession session = request.getSession();
System.out.printf("sessionId: %s%n", session.getId());
session.setAttribute(SysConsts.Session_Login_Key, entity);
session.setAttribute(SysConsts.Session_UserId, entity.getId());
session.setAttribute(SysConsts.Session_Username, entity.getUsername());
session.setAttribute(SysConsts.Session_Nickname, entity.getNickname());
entity.setId(null); //敏感數據不返回前端
entity.setPassword(null);
return new BaseResult(entity);
}
else {
return new BaseResult(ErrorEnum.Login_Incorrect);
}
}
}
- 全局跨域配置和登陸攔截器注冊
MyWebMvcConfig.java
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer{
//全局跨域配置
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //添加映射路徑
.allowedOrigins("http://localhost:8081") //放行哪些原始域
.allowedMethods("*") //放行哪些原始域(請求方式) //"GET","POST", "PUT", "DELETE", "OPTIONS"
.allowedHeaders("*") //放行哪些原始域(頭部信息)
.allowCredentials(true) //是否發送Cookie信息
// .exposedHeaders("access-control-allow-headers",
// "access-control-allow-methods",
// "access-control-allow-origin",
// "access-control-max-age",
// "X-Frame-Options") //暴露哪些頭部信息(因為跨域訪問默認不能獲取全部頭部信息)
.maxAge(1800);
}
//注冊攔截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyLoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login")
.excludePathPatterns("/invalidateSession");
//.excludePathPatterns("/static/**");
}
}
- 登錄攔截器
MyLoginInterceptor.java
public class MyLoginInterceptor implements HandlerInterceptor{
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
request.getServletContext().log("MyLoginInterceptor preHandle");
HttpSession session = request.getSession();
request.getServletContext().log("sessionID: " + session.getId());
Optional<Object> token = Optional.ofNullable(session.getAttribute(SysConsts.Session_Login_Key));
if(token.isPresent()) { //not null
request.getServletContext().log("登錄session未失效,繼續正常流程!");
} else {
request.getServletContext().log(ErrorEnum.Login_Session_Out.msg());
// Enumeration<String> enumHeader = request.getHeaderNames();
// while(enumHeader.hasMoreElements()) {
// String name = enumHeader.nextElement();
// String value = request.getHeader(name);
// request.getServletContext().log("headerName: " + name + " headerValue: " + value);
// }
//尚未弄清楚為啥全局異常處理返回的響應中沒有跨域需要的header,於是乎強行設置響應header達到目的 XD..
//希望有答案的伙伴可以留言賜教
response.setHeader("Access-Control-Allow-Origin", request.getHeader("origin"));
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
// PrintWriter writer = response.getWriter();
// writer.print(new BaseResult(ErrorEnum.Login_Session_Out));
// return false;
throw new BusinessException(ErrorEnum.Login_Session_Out);
}
return true;
}
}
- 全局異常處理
MyCtrllerAdvice.java
@ControllerAdvice(
basePackages = {"com.**.web.*"},
annotations = {Controller.class, RestController.class})
public class MyCtrllerAdvice {
//全局異常處理-ajax-json
@ExceptionHandler(value=Exception.class)
@ResponseBody
public BaseResult exceptionForAjax(Exception ex) {
if(ex instanceof BusinessException) {
return new BaseResult((BusinessException)ex);
}else {
return new BaseResult(ex.getCause()==null?ex.getMessage():ex.getCause().getMessage());
}
}
}
- 后端項目包結構
vue-cli(2.x)前端
- demo 已放置 Gitee
- 前端項目包結構-標准的 vue-cli
- 路由設置,登錄('/')和首頁
router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/Home'
import Login from '@/components/Login'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/home',
name: 'Home',
component: Home
}
]
})
- 設置端口為8081(后端則是8080)
config/index.js
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8081, // can be overwritten by
//...
- 簡單的登錄和首頁組件(完整代碼-見demo-Gitte鏈)
- 登錄
- 登錄后首頁
- 登錄
- axios ajax請求全局設置、響應和異常處理
src/main.js
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:8080'
//axios.defaults.timeout = 3000
axios.defaults.withCredentials = true //請求發送cookie
// 添加請求攔截器
axios.interceptors.request.use(function (config) {
// 在發送請求之前做些什么
console.log('in interceptor, request config: ', config);
return config;
}, function (error) {
// 對請求錯誤做些什么
return Promise.reject(error);
});
// 添加響應攔截器
axios.interceptors.response.use(function (response) {
// 對響應數據做點什么
console.log('in interceptor, response: ', response);
if(!response.data.success){
console.log('errCode:', response.data.errCode, 'errMsg:', response.data.errMsg);
Message({type:'error',message:response.data.errMsg});
let code = response.data.errCode;
if('login02'==code){ //登錄session失效
//window.location.href = '/';
console.log('before to login, current route path:', router.currentRoute.path);
router.push({path:'/', query:{redirect:router.currentRoute.path}});
}
}
return response;
}, function (error) {
// 對響應錯誤做點什么
console.log('in interceptor, error: ', error);
Message({showClose: true, message: error, type: 'error'});
return Promise.reject(error);
});
- 路由URL跳轉攔截(
sessionStorage
初級版)src/main.js
//URL跳轉(變化)攔截
router.beforeEach((to, from, next) => {
//console.log(to, from, next) //
if(to.name=='Login'){ //本身就是登錄頁,就不用驗證登錄session了
next()
return
}
if(!sessionStorage.getItem('username')){ //沒有登錄/登錄過期
next({path:'/', query:{redirect:to.path}})
}else{
next()
}
})
- 測試過程
前端進入即是login頁,用戶名和密碼正確則后端保存登錄的Session,前端登錄成功跳轉home頁,點擊'功能測試'則是正常json響應(Session有效)。如果在本頁中主動將Session失效,再次功能測試則會被攔截,跳轉登錄頁。
碰到的問題
- 全局異常處理返回的響應中沒有跨域需要的
header
這里使用的是后端全局跨域配置,所以前端請求都支持跨域。但是當主動將Session失效,點擊“功能測試”觸發登錄Session失效攔截,由全局異常處理塊返回的響應中卻少了console
中提示的響應頭:
XMLHttpRequest cannot load http://localhost:8080/hello. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8081' is therefore not allowed access.
//PS:查看network可以看到請求是200的,但是前端不能拿到響應
后端強行塞入指定響應頭可以達到目的的(見后端攔截器),這樣做不優雅,原因還不知道 XD..
@20190808 更新
真正上線,代理轉發交給nginx,則不會采用后端配置方式,也就不會有這個問題。
可以繼續的話題(鏈接坑待填)
- cookie被清理,sessionID對應的session對象怎么回收?
暴脾氣用戶禁掉瀏覽器cookie? - springboot-vue-nginx前后端分離跨域配置
- axios 輔助配置
- 過濾器與攔截器
過濾器是在servlet.service()請求前后攔截,springmvc攔截器則是在handle方法前后攔截,粒度不一樣。 - vue-URL跳轉路由攔截,vuex狀態管理
- 集群session與redis