在一个大部分是嵌入式开发的公司996.5工作一周年

79 阅读9分钟

时间过得很快,转眼间,已经毕业入职一年半了。

工作之余写写博客,记录技术、问题和专业成长是我很久之前就有的一个想法。之前总是给自己找开脱的理由,工作忙,没有时间,没有找到好的记录的内容。现在一周年到了,觉得还是要记录点什么。

没错,我就是在主要是嵌入式开发的公司做Java软件开发,主要的开发方向是开发网管平台。之前在转正时,HR问过我知不知道我这个组到底是是干嘛嘚。我当时的理解是,平台接入大量的网络设备,然后在平台上进行统一的管理。只到做完这个项目,我才发现是我狭窄了,这个平台不仅能管理网络设备,还能管理一大堆乱七八糟的设备,什么网络摄像头、录像机、门禁设备、对讲设备,总之只要有IP的、有mac的都能管理,最后成为了一个大杂烩的设备管理平台。

最离谱的是,竟然要用Java作为后端语言,最后做成一个一体机产品。

而我负责的模块是系统配置模块,其中还包括了用户管理及登录相关功能,还算是比较常规的东西。其中登录采用session认证方式,常见的前后端的认证方式还有JWT认证。这里的认证指的是身份认证。

1、身份认证

身份认证:又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认,确保用户有权限进行指定的操作。例如:二维码登录、邮箱密码登录、手机验证码登录等等。

目的:是为了确认当前所声明为某种身份的用户,确实是其所宣称的用户。

不同开发模式下的身份认证

  • 服务端渲染推荐使用Session认证机制
  • 前后端分离推荐使用JWT认证机制

2、Session认证机制

1.HTTP协议的无状态性:指的是客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次HTTP请求的状态。 2.为了解决HTTP无状态的限制,我们可以使用Cookie。

image.png

2.1 Session和Cookie的区别

这里就有必要说明一下Session和Cookie的作用、区别和各自的应用范围。

2.2 cookie的工作原理

  1. 浏览器第一次发送请求到服务器端;
  2. 服务器端创建Cookie,该Cookie中包含用户的信息,然后将该Cookie发送到浏览器;
  3. 浏览器端再次访问服务器端时会携带服务器端创建的Coolie;
  4. 服务器端通过cookie中携带的数据来鉴权和区分不同的用户;

image.png

2.3 session的工作原理

  1. 浏览器端第一次发送请求到服务器端,服务器端通过验证后生成session,同时会创建一个特殊的cookie,然后通过reponse将Set-Cookie发送至浏览器端;
  2. 浏览器端发送第N次(N>1)请求到服务器端,浏览器端访问服务器端的时候会携带Cookie对象;
  3. 服务器端会根据Cookie中指定key的value值去存储的位置查询session对象,从而区分鉴权并区分不同用户;

image.png cookie数据保存在客户端,session数据保存在服务端。

session: 简单的说,当你登录一个网站的时候,如果web服务器端使用的是session,那么所有的数据都保存在服务器上,客户端每次请求服务器的时候发送当前会话sessionId,服务器根据当前sessionId判断相应的用户数据标志,以确定用户是否登录或具有某种权限。由于数据是存储在服务器上面,所以不能伪造。

cookie: sessionId是服务器和客户端连接时候随机分配的,如果浏览器使用的是cookie,那么所有数据都保存在浏览器端。比如你登录以后,服务器设置了cookie用户名,那么当你再次请求服务器的时候,浏览器会将用户名一块发送给服务器,这些变量有一定的特殊标记。服务器会解释为cookie变量,所以只要不关闭浏览器,那么cookie变量一直都是有效的,所以能够保证长时间不掉线。

:由于cookie的特性,如果能截获某个用户的cookie变量,然后伪造一个数据包发送过去,那么服务器还是认为你是合法的。所以使用cookie被攻击的可能性比较大。

区别:

  1. cookie数据存放在客户的浏览器上,session数据放在服务器上。
  2. cookie不是很安全,别人可以分析存放在本地上的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session;
  3. session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE;
  4. 单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能超过3k;
  5. 所以将登录信息等重要信息存放在SESSION,其他信息如果需要保留,可以放在COOKIE中;
对比项CookieHttpSession
存储位置存放在客户端浏览器存放在服务器内存
存储容量单个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字符串的形式来认证用户的身份。

image.png 组成部分 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 验证不通过");
} 

//验证成功