Session、Cookie、Token的区别
网上这部分的资料很多。比如可以参考https://www.cnblogs.com/moyand/p/9047978.html等等。对于一些概念简单归纳一下:
- 由于HTTP都是无状态请求,这些都是为了让服务端“记住”用户而发明出来的方法。为确保用户会话的独立性和安全性,不能使用明文保存用户信息(如userId),而应使用加密后的信息储存。
- Session即会话,存放在服务端,用于标识用户的一次登录访问的信息。当某一个用户第一次调用一个Servlet时,服务器会生成一个HttpSession的对象,里面使用sessionId来作为该用户的唯一标识码。
- Cookie为存储在客户端的“小型文本文件”,保存用户的登录信息。使用Cookie+Session的认证方法,一般是在用户登录后获得服务器返回的sessionId存放到浏览器的Cookie里,然后浏览器在会话访问期间把Cookie放在Http请求头里完成认证。
- Cookie+Session的认证方法有以下缺点:①用户的每次访问生成的Session都要存放在服务器中,增大了服务端开销,此外存放在一个服务器中的sessionId不属于其他服务器,不便于分布式系统的水平扩展。②Cookie容易被截获,服务端易遭受跨站伪造请求攻击(CSRF)。③Cookie适用于浏览器,而对于移动端App等客户端不太友好。
- Token也是由服务端生成的标识码,但区别于Session,Token是一种无状态的、由服务端签发但并不存放在服务端的认证方法。对于客户端传来的token服务端只需通过算法解码进行认证,以此来保持与用户的会话。相当于使用一部分的CPU计算资源换取了实际的存储开销,同时也解决了扩展性、跨域等等的问题。
SpringBoot项目中使用Token
使用Token做登录认证以及用户授权的最佳方案是使用JWT+Shiro,这里可以参考下面两篇文章:
- 使用JWT:https://www.jianshu.com/p/e88d3f8151db。JWT(Json Web Token)生成的Token具有非常强的安全性,此外网上搜SpringBoot+JWT出来的结果基本上都跟这篇文章一样。
- 使用Shiro:https://blog.csdn.net/dghkgjlh/article/details/90145603。Shiro是一个安全框架,主要用于认证和授权,这里主要用到了Shiro的拦截器等功能。
由于本人的项目不涉及这么多,就没有采用上述的JWT+Shiro的方法(其实是自己菜),使用的是随机生成的token+Spring的拦截器简单实现的。。下面是代码:
1.Spring拦截器
拦截器用于对请求进行拦截,为SpringAOP思想的主要实践之一。主要用在登录之后检查token的合法性,从而判断请求是否合法,也可以进行权限访问控制等等(也可以用fiter)。
InterceptorConfig.java
@Configuration
class InterceptorConfig implements WebMvcConfigurer
{
@Bean
public TokenInterceptor initAuthInterceptor()
{
return new TokenInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(initAuthInterceptor()).addPathPatterns("/**").excludePathPatterns("/login");//login放行,其他拦截
}
}
对于/login放行,不检查token,其他请求均进行拦截。
TokenInterceptor.java
public class TokenInterceptor implements HandlerInterceptor
{
@Autowired
private JedisPool jedisPool;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
Jedis jedis = jedisPool.getResource();
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=utf-8");
String token = request.getHeader("token");
if (token.isEmpty()) {
response.getWriter().print("用户未登录,请登录后操作");
return false;
}
if(!jedis.exists(token)){
response.getWriter().print("登录状态已过期,请重新登录");
return false;
}
int identity = Integer.parseInt(jedis.hget(token,"auth"));
String url = request.getRequestURI();
if(identity==2){
if(url.startsWith("/course")||url.startsWith("/elective")||url.startsWith("/group")||url.startsWith("/module")
||url.startsWith("/project")||url.startsWith("/school")||url.startsWith("/class")||url.startsWith("/students")
||url.startsWith("/teachers")||url.startsWith("/test")){
response.getWriter().print("你没有权限,无法操作");
return false;
}
}
jedis.close();
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
在preHandle里做请求的拦截,作用于Controller调用之前,返回true为放行,false为立即返回response。
其实可以看出我生成的token是存放在redis里的。。。(这根session有什么区别啊喂!!)(溜)
2.登录
Login.java
@RestController
public class Login
{
@Autowired
private IAccountService accountService;
@Autowired
private ITeacherService teacherService;
@Autowired
private JedisPool jedisPool;
/**
* 登录功能
* @param username 账号
* @param password 密码
*/
@RequestMapping(value = "login",method = RequestMethod.POST)
public Object login(String username,String password)
{
Jedis jedis = jedisPool.getResource();
if(!accountService.existAccount(username)) return "账号不存在";//账号不存在
Account account = accountService.getAccount(username);
String psw = account.getPassword();
if(!password.equals("123")){
password = new Md5Hash(password).toString();
}
Map<String,String> map = new HashMap<>();
Integer identity = account.getIdentity();
String token = UUID.randomUUID().toString();
if(identity==1){
if(password.equals(psw)) {
map.put("token",token);
map.put("identity","student");
jedis.hset(token,"id",account.getUsername());
jedis.hset(token,"auth",account.getIdentity().toString());
}
else return "密码错误,请重新输入";
}else{
if(password.equals(psw)){
jedis.hset(token,"id",teacherService.queryTeacherByUsername(account.getUsername()).getId().toString());
jedis.hset(token,"auth",account.getIdentity().toString());
map.put("token",token);
map.put("identity","teacher");
}
else return "密码错误,请重新输入";
}
jedis.expire(token,6*3600);
jedis.save();
jedis.close();
return map;
}
/**
* 退出登录
*/
@RequestMapping(value = "logout",method = RequestMethod.POST)
public String logout(@RequestHeader("token")String token)
{
Jedis jedis = jedisPool.getResource();
jedis.del(token);
jedis.close();
return "0";
}
}
登录时生成随机token,一边作为key保存用户数据放在redis里,设置过期时间;一边返回给前端,前端储存在localStorage里。登出时直接删除key即可。
对于任意的Controller,无需传入userId,只需用@RequestHeader(“token”)拿到token后在redis中查询数据即可。