登录那些事

  • Post author:
  • Post category:其他




Cookie


HTTP

是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过

cookie

或者

session

去实现。



cookie

存储在客户端



cookie

是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。


cookie

是不可跨域的: 每个

cookie

都会绑定单一的域名,无法在别的域名下获取使用,一级域名和二级域名之间是允许共享使用的(靠的是

domain



Cookie机制


cookie

的设置以及发送会经历以下4个步骤:

  1. 客户端发送一个请求到服务器
  2. 服务器发送一个

    HttpResponse

    响应到客户端,其中包含

    Set-Cookie

    的头部
  3. 客户端保存

    cookie

    ,之后向服务器发送请求时,

    HttpRequest

    请求中会包含一个

    Cookie

    的头部
  4. 服务器返回响应数据

Cookie机制



Cookie 重要属性

属性 说明
name=value 键值对,设置 Cookie 的名称及相对应的值,都必须是字符串类型
domain 指定 cookie 所属域名,默认是当前域名
path 指定 cookie 在哪个路径(路由)下生效,默认是 ‘/’
maxAge cookie 失效的时间,单位秒 如果为整数,则该 cookie 在 maxAge 秒后失效。如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie 。如果为 0,表示删除该 cookie 。默认为 -1
expires 过期时间,在设置的某个时间点后该 cookie 就会失效。
secure 该 cookie 是否仅被使用安全协议传输。安全协议有 HTTPS,SSL等,在网络上传输数据之前先将数据加密。默认为false。
httpOnly 如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本 读取到该 cookie 的信息,但还是能通过 Application 中手动修改 cookie,所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全



Cookie 的Java操作

public void writeCookie(HttpServletRequest req, HttpServletResponse resp) {
        int time = 10 * 24 * 60 * 60;
        Cookie cookie = new Cookie("DAIHAO", System.currentTimeMillis() + "");
        cookie.setPath("/");
        cookie.setDomain(req.getHeader("Origin"));
        cookie.setMaxAge(time);
        cookie.setHttpOnly(true);

        // 设置生命周期为MAX_VALUE,永久有效
        // cookie.setMaxAge(Integer.MAX_VALUE);

        // MaxAge为负数,是一个临时Cookie,不会持久化
        // cookie.setMaxAge(-1);

        resp.setCharacterEncoding("UTF-8");
        // 修改Cookie只能使用一个同名的Cookie来覆盖原先的Cookie。如果要删除某个Cookie,则只需要新建一个同名的Cookie,并将maxAge设置为0,并覆盖原来的Cookie即可。
        resp.addCookie(cookie);
    }



Session


session

是另一种记录服务器和客户端会话状态的机制


session

是基于

cookie

实现的,

session

存储在服务器端,

sessionId

会被存储到客户端的

cookie



Session机制

  1. 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的

    Session

  2. 请求返回时将此

    Session

    的唯一标识信息

    SessionID

    返回给浏览器

  3. 浏览器接收到服务器返回的

    SessionID

    信息后,会将此信息存入到

    Cookie

    中,同时

    Cookie

    记录此

    SessionID

    属于哪个域名

  4. 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在

    Cookie

    信息,如果存在自动将

    Cookie

    信息也发送给服务端,服务端会从

    Cookie

    中获取

    SessionID

    ,再根据

    SessionID

    查找对应的

    Session

    信息,如果没有找到说明用户没有登录或者登录失效,如果找到

    Session

    证明用户已经登录可执行后面操作。

Session机制



Session的Java操作

@WebServlet(name = "SessionServlet", urlPatterns = "/session")
public class SessionServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取 Session 对象
        HttpSession session = req.getSession();
        // 将用户名存储到 Session 中
        session.setAttribute("username", req.getParameter("username"));
        // 重定向到另一个页面
        resp.sendRedirect("anotherPage.jsp");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}



Cookie 和 Session 的区别

  1. 安全性:

    Session



    Cookie

    安全,

    Session

    是存储在服务器端的,

    Cookie

    是存储在客户端的

  2. 存取值的类型不同:

    Cookie

    只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,

    Session

    可以存任意数据类型

  3. 有效期不同:

    Cookie

    可设置为长时间保持,比如我们经常使用的默认登录功能,

    Session

    一般失效时间较短,客户端关闭(默认情况下)或者

    Session

    超时都会失效。

  4. 存储大小不同: 单个

    Cookie

    保存的数据不能超过

    4K



    Session

    可存储数据远高于

    Cookie

    ,但是当访问量过多,会占用过多的服务器资源。



Token


Token

,访问资源接口(

API

)时所需要的资源凭证 ,简单来说就是类似

cookie

的一种验证信息,客户端通过登录验证后,服务器会返回给客户端一个加密的

token

,然后当客户端再次向服务器发起连接时,带上

token

,服务器直接对

token

进行校验即可完成权限校验。



Token机制

1、客户端使用用户名跟密码请求登录

2、服务端收到请求,验证用户名与密码,验证成功后,服务端签发

token

发送给客户端

3、客户端收到

token

以后,会把它存储起来,比如放在

cookie

里或者

localStorage

4、客户端每次向服务端请求资源的时候需要带着服务端签发的

token

5、服务端收到请求,然后去验证客户端请求里面带着的

token

,如果验证成功,就向客户端返回请求的数据

Token机制



Token 和 Session 的区别


  1. Session

    是一种记录服务器和客户端会话状态的机制,使服务端有状态化,可以记录会话信息。而

    Token

    是令牌,访问资源接口(

    API

    )时所需要的资源凭证。

    Token

    使服务端无状态化,不会存储会话信息。


  2. Session



    Token

    并不矛盾,作为身份认证

    Token

    安全性比

    Session

    好,因为每一个请求都有签名还能防止监听以及重放攻击,而

    Session

    就必须依赖链路层来保障通讯安全了。如果你需要实现有状态的会话,仍然可以增加

    Session

    来在服务器端保存一些状态。



JWT


JSON Web Token (JWT)

是一个开放标准

(RFC 7519)

,它定义了一种紧凑的、自包含的方式,用于作为

JSON

对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。



JWT机制

JWT机制

1、用户使用账号、密码登录应用,登录的请求发送到服务端。

2、服务端进行用户验证,然后创建

JWT

字符串返回给客户端。

3、客户端请求接口时,在请求头带上

JWT

4、服务端验证

JWT

合法性,如果合法则继续调用应用接口返回结果。



JWT数据结构


JWT

一般是这样一个字符串,分为三个部分,以

“.”

隔开:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c



Header


JWT

第一部分是头部分,它是一个描述

JWT

元数据的

Json

对象,通常如下所示。

{
  "alg": "HS256",
  "typ": "JWT"
}


alg

属性表示签名使用的算法,默认为

HMAC SHA256(写为HS256)



typ

属性表示令牌的类型,

JWT

令牌统一写为

JWT

最后,使用

Base64 URL

算法将上述

JSON

对象转换为字符串保存。



Payload


JWT

第二部分是

Payload

,也是一个

Json

对象,除了包含需要传递的数据,还有七个默认的字段供选择。


  • iss (issuer):

    签发人/发行人


  • sub (subject):

    主题


  • aud (audience):

    用户


  • exp (expiration time):

    过期时间


  • nbf (Not Before):

    生效时间,在此之前是无效的


  • iat (Issued At):

    签发时间


  • jti (JWT ID):

    用于标识该

    JWT

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

默认情况下

JWT

是未加密的,任何人都可以解读其内容,因此一些敏感信息不要存放于此,以防信息泄露。


JSON

对象也使用

Base64 URL

算法转换为字符串后保存,是可以反向反编码回原样的,这也是为什么不要在

JWT

中放敏感数据的原因。



Signature


JWT

第三部分是签名。生成首先需要指定一个

secret

,该

secret

仅仅保存在服务器中,保证不能让其他用户知道。这个部分需要

base64URL

加密后的

header



base64URL

加密后的

payload

使用 . 连接组成的字符串,然后通过

header

中声明的加密算法 进行加盐

secret

组合加密,然后就得出一个签名哈希,也就是

Signature

,且无法反向解密。

那么

Application Server

如何进行验证呢?可以利用

JWT

前两段,用同一套哈希算法和同一个

secret

计算一个签名值,然后把计算出来的签名值和收到的

JWT

第三段比较,如果相同则认证通过。



JWT的优点


  • json

    格式的通用性,所以

    JWT

    可以跨语言支持,比如

    Java



    JavaScript



    PHP



    Node

    等等。

  • 可以利用

    Payload

    存储一些非敏感的信息。

  • 便于传输,

    JWT

    结构简单,字节占用小。

  • 不需要在服务端保存会话信息,易于应用的扩展。



JWT的Java操作

<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>4.3.0</version>
</dependency>


JwtUtils 工具类

import cn.hutool.core.codec.Base64Decoder;
import cn.hutool.core.codec.Base64Encoder;
import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import org.apache.commons.lang.StringUtils;


import javax.servlet.http.HttpServletResponse;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


public class JwtUtils {
    //过期时间 15分钟
    private static final long EXPIRE_TIME = 15 * 60 * 1000;
    //私钥
    private static final String TOKEN_SECRET = "DAIHAO";
    //会话连接的有效时长,单位分钟
    public static final int SCOKET_EFFECTIVE_TIME_MINUTE = 120;

    /**
     * 生成签名
     */
    public static String sign(Map<String, Object> map) {
        try {
            // 设置过期时间
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            // 私钥和加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("typ", "jwt");
            // 返回token字符串
            JWTCreator.Builder builder = JWT.create()
                    .withHeader(header)
                    .withIssuedAt(new Date()) //发证时间
                    .withExpiresAt(date);  //过期时间
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                if (entry.getValue() instanceof Integer) {
                    builder.withClaim(entry.getKey(), (Integer) entry.getValue());
                } else if (entry.getValue() instanceof Long) {
                    builder.withClaim(entry.getKey(), (Long) entry.getValue());
                } else if (entry.getValue() instanceof Boolean) {
                    builder.withClaim(entry.getKey(), (Boolean) entry.getValue());
                } else if (entry.getValue() instanceof String) {
                    builder.withClaim(entry.getKey(), String.valueOf(entry.getValue()));
                } else if (entry.getValue() instanceof Double) {
                    builder.withClaim(entry.getKey(), (Double) entry.getValue());
                } else if (entry.getValue() instanceof Date) {
                    builder.withClaim(entry.getKey(), (Date) entry.getValue());
                }
            }
            return builder.sign(algorithm);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }


    /**
     * 检验token是否正确
     *
     * @param **token**
     * @return
     */
    public static boolean verify(String token) {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        JWTVerifier verifier = JWT.require(algorithm).build();
        verifier.verify(token);
        return true;
    }

    /**
     * 获取用户自定义Claim集合
     *
     * @param token
     * @return
     */
    public static Map<String, Claim> getClaims(String token) {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        JWTVerifier verifier = JWT.require(algorithm).build();
        Map<String, Claim> jwt = verifier.verify(token).getClaims();
        return jwt;
    }

    /**
     * 获取过期时间
     *
     * @param token
     * @return
     */
    public static Date getExpiresAt(String token) {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        return JWT.require(algorithm).build().verify(token).getExpiresAt();
    }

    /**
     * 获取jwt发布时间
     */
    public static Date getIssuedAt(String token) {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        return JWT.require(algorithm).build().verify(token).getIssuedAt();
    }

    /**
     * 验证token是否失效
     *
     * @param token
     * @return true:过期   false:没过期
     */
    public static boolean isExpired(String token) {
        try {
            final Date expiration = getExpiresAt(token);
            return expiration.before(new Date());
        } catch (TokenExpiredException e) {
            // e.printStackTrace();
            return true;
        }

    }

    /**
     * 直接Base64解密获取header内容
     *
     * @param token
     * @return
     */
    public static String getHeaderByBase64(String token) throws Exception {
        if (StringUtils.isEmpty(token)) {
            return null;
        } else {
            byte[] header_byte = decryBASE64(token);
            return new String(header_byte);
        }

    }

    /**
     * 直接Base64解密获取payload内容
     *
     * @param token
     * @return
     */
    public static JSONObject getPayloadByBase64(String token) throws Exception {
        if (StringUtils.isEmpty(token)) {
            return null;
        } else {
            byte[] payload_byte = decryBASE64(token);
            String decode = new String(payload_byte, "UTF-8");

            int i = decode.lastIndexOf("{");
            int j = decode.lastIndexOf("}");
            decode = decode.substring(i, j + 1);

            return JSONObject.parseObject(decode);
        }
    }

    public static Boolean refreshToken(String token, HttpServletResponse response) {
        try {
            String[] tokenSplit = token.split("\\.");

            JSONObject tokenPayloadJson = getPayloadByBase64(tokenSplit[1]);

            Long expL = tokenPayloadJson.getLong("exp");


            Date date = new Date();
            Date expDate = new Date(expL);

            double millisecond = date.getTime() - expDate.getTime() * 1000;
            double time = millisecond / (60 * 1000);

            if (time > SCOKET_EFFECTIVE_TIME_MINUTE) {
                return false;
            }

            Map<String, Object> map = new HashMap<>();
            for (Map.Entry entry : tokenPayloadJson.entrySet()) {
                if (!"exp".equals(entry.getKey()))
                    map.put((String) entry.getKey(), entry.getValue());
            }
            String newToken = sign(map);

            response.setHeader("DAIHAO_TOKEN", newToken);
            response.setHeader("Access-Control-Expose-Headers", "DAIHAO_TOKEN");
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /***
     * BASE64解密
     * @param key
     * @return
     * @throws Exception
     */
    public static byte[] decryBASE64(String key) throws Exception {
        return (new Base64Decoder()).decode(key);
    }

    /***
     * BASE64加密
     * @param key
     * @return
     * @throws Exception
     */
    public static String encryptBASE64(byte[] key) throws Exception {
        return (new Base64Encoder()).encode(key);
    }

    public static void main(String[] args) throws Exception {
        Map<String, Object> map = new HashMap<>();
        map.put("userId", "1");
        map.put("rose", "DAIHAO");
        map.put("integer", 1111);
        map.put("double", 112.222);
        map.put("Long", 112L);
        map.put("bool", true);
        map.put("date", new Date());
        String token = sign(map); //生成token
        System.out.println("token:" + token);

        System.out.println("校验token:" + verify(token));//验证token是否正确

        String rose = getClaims(token).get("rose").asString(); //使用方法
        System.out.println("rose:" + rose);

        System.out.println("获取签发token时间:" + getIssuedAt(token));
        System.out.println("获取过期时间:" + getExpiresAt(token));

        //Thread.sleep(1000*40);
        System.out.println("检查是否已过期:" + isExpired(token));

        System.out.println("获取头:" + getHeaderByBase64(token));

        System.out.println("获取负荷:" + getPayloadByBase64(token));
    }
}


Token刷新

拦截器中判断失效,返回

token

public class TokenInteceptor extends HandlerInterceptorAdapter {

  
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }

 
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            R<?> r = null;//自定义返回结果
            String token = request.getHeader("DAIHAO_TOKEN");
            try {
                JwtUtils.verify(token);
            } catch (SignatureVerificationException e) {
                e.printStackTrace();
                r = R.error(401, "invalid signature");
            } catch (TokenExpiredException e) {
                e.printStackTrace();
                if (JwtUtils.refreshToken(token, response)) {
                    return super.preHandle(request, response, handler);
                }
                r = R.error(401, "token expired");
            } catch (AlgorithmMismatchException e) {
                e.printStackTrace();
                r = R.error(401, "inconsistent token algorithm");
            } catch (Exception e) {
                e.printStackTrace();
                r = R.error(401, "token invalidation");
            }
            if (r != null) {
                String json = new ObjectMapper().writeValueAsString(r);
                response.setContentType("application/json;charset=UTF-8");
                Writer out = new BufferedWriter(new OutputStreamWriter(response.getOutputStream()));
                out.write(json);
                out.flush();
                out.close();
                return false;
            }

        return super.preHandle(request, response, handler);
    }
}



版权声明:本文为D_A_I_H_A_O原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。