时间过得很快,转眼间,已经毕业入职一年半了。
工作之余写写博客,记录技术、问题和专业成长是我很久之前就有的一个想法。之前总是给自己找开脱的理由,工作忙,没有时间,没有找到好的记录的内容。现在一周年到了,觉得还是要记录点什么。
没错,我就是在主要是嵌入式开发的公司做Java软件开发,主要的开发方向是开发网管平台。之前在转正时,HR问过我知不知道我这个组到底是是干嘛嘚。我当时的理解是,平台接入大量的网络设备,然后在平台上进行统一的管理。只到做完这个项目,我才发现是我狭窄了,这个平台不仅能管理网络设备,还能管理一大堆乱七八糟的设备,什么网络摄像头、录像机、门禁设备、对讲设备,总之只要有IP的、有mac的都能管理,最后成为了一个大杂烩的设备管理平台。
最离谱的是,竟然要用Java作为后端语言,最后做成一个一体机产品。
而我负责的模块是系统配置模块,其中还包括了用户管理及登录相关功能,还算是比较常规的东西。其中登录采用session认证方式,常见的前后端的认证方式还有JWT认证。这里的认证指的是身份认证。
1、身份认证
身份认证:又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认,确保用户有权限进行指定的操作。例如:二维码登录、邮箱密码登录、手机验证码登录等等。
目的:是为了确认当前所声明为某种身份的用户,确实是其所宣称的用户。
不同开发模式下的身份认证
- 服务端渲染推荐使用Session认证机制
- 前后端分离推荐使用JWT认证机制
2、Session认证机制
1.HTTP协议的无状态性:指的是客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次HTTP请求的状态。 2.为了解决HTTP无状态的限制,我们可以使用Cookie。
2.1 Session和Cookie的区别
这里就有必要说明一下Session和Cookie的作用、区别和各自的应用范围。
2.2 cookie的工作原理
- 浏览器第一次发送请求到服务器端;
- 服务器端创建Cookie,该Cookie中包含用户的信息,然后将该Cookie发送到浏览器;
- 浏览器端再次访问服务器端时会携带服务器端创建的Coolie;
- 服务器端通过cookie中携带的数据来鉴权和区分不同的用户;
2.3 session的工作原理
- 浏览器端第一次发送请求到服务器端,服务器端通过验证后生成session,同时会创建一个特殊的cookie,然后通过reponse将Set-Cookie发送至浏览器端;
- 浏览器端发送第N次(N>1)请求到服务器端,浏览器端访问服务器端的时候会携带Cookie对象;
- 服务器端会根据Cookie中指定key的value值去存储的位置查询session对象,从而区分鉴权并区分不同用户;
cookie数据保存在客户端,session数据保存在服务端。
session: 简单的说,当你登录一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上,客户端每次请求服务器的时候发送当前会话sessionId,服务器根据当前sessionId判断相应的用户数据标志,以确定用户是否登录或具有某种权限。由于数据是存储在服务器上面,所以不能伪造。
cookie: sessionId是服务器和客户端连接时候随机分配的,如果浏览器使用的是cookie,那么所有数据都保存在浏览器端。比如你登录以后,服务器设置了cookie用户名,那么当你再次请求服务器的时候,浏览器会将用户名一块发送给服务器,这些变量有一定的特殊标记。服务器会解释为cookie变量,所以只要不关闭浏览器,那么cookie变量一直都是有效的,所以能够保证长时间不掉线。
注:由于cookie的特性,如果能截获某个用户的cookie变量,然后伪造一个数据包发送过去,那么服务器还是认为你是合法的。所以使用cookie被攻击的可能性比较大。
区别:
- cookie数据存放在客户的浏览器上,session数据放在服务器上。
- cookie不是很安全,别人可以分析存放在本地上的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session;
- session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE;
- 单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能超过3k;
- 所以将登录信息等重要信息存放在SESSION,其他信息如果需要保留,可以放在COOKIE中;
| 对比项 | Cookie | HttpSession |
|---|---|---|
| 存储位置 | 存放在客户端浏览器 | 存放在服务器内存 |
| 存储容量 | 单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie | 没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制 |
| 存储方式 | 只能保管ASCLL字符串,并需要通过编码方式存储为Unicode字符或二进制数据 | 能够存储任何类型的数据 |
| 隐私策略 | 对客户端是可见的,以明文保存 | 存储在服务器上,不存在敏感信息泄露的风险 |
| 有效期 | 通过设置cookie的属性,达到使cookie长期有效 | 依赖于名为JSESSIONID的cookie,ercookie JSESSIONID的过期时间默认为-1,只需关闭窗口该session就会失效,因而session不能达到长期有效的目的 |
| 服务器压力 | 保管在客户端,不占用服务器资源 | 保管在服务器端的,每个用户都会产生一个session,耗费大量的内存 |
| 跨域支持 | 支持跨域名访问 | 不支持跨域名访问 |
在本年的网管项目中我采用的是cookie中保存sessionId,sessionId对应的sessionId用户信息保存在缓存中的方式。
注:Session认证机制需要配合Cookie才能实现。由于Cookie默认不支持跨域访问,所以当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域Session认证。
- 当前端请求后端接口不存在跨域问题的时候,推荐使用Session身份认证机制
- 当前端需要跨域请求后端接口的时候,不推荐使用Session身份认证机制,推荐使用JWT认证机制
2.4、代码示例
2.4.1 Cookie示例
创建cookie
// 存入cookie标志,创建cookie
Cookie cookie = new Cookie("key", "remberme");
cookie.setMaxAge(3600); //设置cookie的有效时间,单位为秒
cookie.setPath("/");
//放入响应对象
response.addCookie(cookie);
登录页面设置,实现免登录功能
<%
// 尝试获取cookie
Cookie[] cookies = request.getCookies();
// 免登录功能
if(cookies != null){
for(Cookie c : cookies) {
if("remberme".equals(c.getValue())){
System.out.println(c.getName()+" = "+c.getValue());
reponse.sendRedirect("/music/musicListServlet");
}
}
}
>%
2.4.2 Session示例
创建session
// 获得HttpSession对象
HttpSession session = request.getSession();
// 设置session属性
session.setAttrubute("username", "wangxh");
// 响应重定向到JSP,到JSP中获取会话属性
response.sendRedirect("getSessionAttr.jsp");
在JSP页面中显示session中的数据
<body>
会话属性:<%=session.getAttribute("username")%>
</body>
3、JWT认证机制
3.1 工作原理
用户的信息通过Tokne字符串的形式,保存在客户端浏览器中。服务器通过还原Token字符串的形式来认证用户的身份。
组成部分
JWT通常由三部分组成,分别是Header(头部)、Payload(有效载荷)、Signature(签名)。三者之间使用英文的“.”分割,格式如Header.Payload.Signature
- Payload部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
- Header和Signature是安全性相关的部分,只是为了保证Token的安全性。 使用方式 客户端收到服务器返回的JWT之后,通常会将它储存在localStorage或sessionStorage中。此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证。
可以把JWT放在HTTP请求头的Authorization字段中,格式如下
Authorization: Bearer <token>
3.2 JWT优势
简洁:可以通过URL、POST参数或者在HTTP Header发送,因为数据量小,传输速度很快;
自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。不需要在服务端保存会话信息,特别适合于分布式微服务。
更适合于移动端:当客户端是非浏览器平台时,cookie是不被支持的,此时使用token认证方式会简单很多
单点登录友好:由于cookie无法跨域,难以实现单点登录。但是,使用token进行认证的话,token可以被保存在客户端的任意位置的内存中,不一定是cookie,所以不依赖cookie,不会存在这些问题。
3.2 代码示例
1、依赖引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt-jsonwebtoken.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>RELEASE</version>
</dependency>
2、生成Token
// 签名密钥
private static final String SECRET = "!Doker$";
public String createToken(Map<String, Object> claims, String subject) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, Algorithm.HMAC256(SECRET))
.compact();
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration * 1000);
}
3、刷新token
// 签名密钥
public String RefreshToken(String token) {
final Date createdDate = clock.now();
final Date expirationDate = calculateExpirationDate(createdDate);
final Claims claims = getAllClaimsFromToken(token);
claims.setIssuedAt(createdDate);
claims.setExpiration(expirationDate);
return Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, Secret)
.compact();
}
4、token发送给前端 传入当前用户的功能与用户信息,登录生成token,写入reponse的返回头中,前端获取后保存在前端的本地缓存职工,后续前端请求要把token放在头header里。
//登录成功之后
List<Object> functs=(List<Object>) authResult.getAuthorities();
//当前功能列表
String loginName=authResult.getName();//登录名
Users obj=(Users)authResult.getPrincipal();//用户信息
String token=JwtUtil.createToken(loginName,functs,obj);
//生成token TOKEN_HEADER= Authorization TOKEN_PREFIX=Bearer token值
response.setHeader(JwtUtil.TOKEN_HEADER,JwtUtil.TOKEN_PREFIX+token);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK); //个人编写的视图对象
DTO dto=new DTO<>();
dto.setCode("000000");
dto.setMessage("认证通过");
PrintWriter pw=response.getWriter();
pw.write(JsonUtil.set(dto));//写入json
pw.flush();//强制刷新
pw.close();//关闭流
5、验证用户请求携带token
String header = request.getHeader(JwtUtil.TOKEN_HEADER);
if (null == header || !header.toLowerCase().startsWith(JwtUtil.TOKEN_PREFIX)) {
// 如果头部 Authorization 未设置或者不是 basic 认证头部,则当前
// 请求不是该过滤器关注的对象,直接放行,继续filter chain 的执行
chain.doFilter(request, response);
return;
}
try {
String token = header.replace(JwtUtil.TOKEN_PREFIX, "");
// 验证token是否过期
if (JwtUtil.isExpiration(token)) {
throw new javax.security.sasl.AuthenticationException("token 验证不通过");
}
//檢查token是否能解析
Users user = (Users) JwtUtil.getUser(token);
if (null == user) {
throw new javax.security.sasl.AuthenticationException("token 验证不通过");
}
//验证成功