状态管理:如何理解和区分Cookie、Session以及Token?

1.1. 状态管理

1.1.1. 为什么需要状态管理?
Web应用程序使用HTTP协议作为传输数据的标准协议,而HTTP协议是无状态协议,即一次请求对应一次响应,响应结束后连接即断开,同一个用户的不同请求对于服务器端来讲并不会认为这两个请求有什么关联性,并不会以此区分不同的客户端。但实际情况中还是需要服务器端能够区分不同的客户端以及记录与客户端相关的一些数据,所以状态管理能够做到不同客户端的身份识别。

1.1.2. 什么是状态管理?
将客户端与服务器之间多次交互当做一个整体来看待,并且将多次交互中涉及的数据保存下来,提供给后续的交互进行数据的管理即状态管理。

这里的状态指的是当前的数据,管理指的是在这个多次交互的过程中对数据的存储、修改、删除。比如:车主每次携带卡片洗车后由商家修改次数,车主即可带走这张记录数据的卡片,商家不会保存任何数据,客户自己负责携带需要维护的数据。

1.1.3. 状态管理两种常见模式
状态管理的过程中重要的是数据的保存,只有存下来的数据才能在多次交互中起到记录的作用,所以可以按照管理的数据的存储方式和位置的不同来区分状态管理的模式。

如果将数据存储在客户端,每次向服务器端发请求时都将存在客户端的数据随着请求发送到服务器端,修改后再发回到客户端保存的这种模式叫做Cookie。

如果将数据存储在服务器端,并且为这组数据标示一个编号,只将编号发回给客户端。当客户端向服务器发送请求时只需要将这个编号发过来,服务器端按照这个编号找到对应的数据进行管理的这种模式叫做Session——会话。

1.2. Cookie

1.2.1. 什么是Cookie?
一小段文本信息随着请求和响应,在客户端和服务器端之间来回传递。根据设定的时间来决定该段文本在客户端保存时长的这种工作模式叫做Cookie。最初服务器将信息发给客户端时是通过响应数据的Set-Cookie头信息来完成的。

1.2.2. Cookie的原理

13566833-c9fe81ac52cd3ac0.png

如果客户端向服务器端AddServlet发送请求,遇到创建Cookie的代码时,那么一小段文本信息就会随着response响应中的头信息被传递回客户端。如图中Set-Cookie:uname=xxx就是从服务器端传递回客户端的文本信息。当文本信息到达客户端以后,会被保存在客户端的内存或硬盘上,存在内存中会随着内存的释放而消失,存在硬盘上则会保存更长的时间。

一旦客户端存有服务器发回的文本信息,那么当浏览器再次向服务器发起请求时,如请求FindServlet这个组件,那么存储的文本信息会随着请求数据包的消息头以Cookie:uname=xxx这样的形式将文本信息发送到服务器端。只要Cookie的生命周期没有结束,那么不管是存在内存还是硬盘上的信息都会在客户端向服务器端发出请求时自动的随着消息头发送过去。

1.2.3. 如何创建Cookie
Servlet API提供了javax.servlet.http.Cookie这种类型来解释Cookie。其中存储的文本以name-value对的形式进行区分,所以创建Cookie时指定name-value对即可。这个name-value最终是以Set-Cookie这种消息头的形式跟随相应数据包到达客户端,所以要想将数据添加到消息头中需要使用response对象提供的方法。创建Cookie的代码如下所示:

Cookie  c = new Cookie(String name,String value);  //文本的创建
response.addCookie( c );    /**在响应数据包中追加一个Set-Cookie的消息头,如果发送了相同name的Cookie数据,那么之前的数据会被覆盖。能够创建多少个Cookie存放在客户端与当前浏览器的种类相关。**/
//实现创建两个Cookie代码示例:
package Paint;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.*;
import javax.servlet.*;
public class AddCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        // 创建cookie
        Cookie c = new Cookie("username", "Lisa");
        Cookie c2 = new Cookie("city", "NewYork");
        response.addCookie(c);
        response.addCookie(c2);
        out.close();
    }
}

1.2.4. 如何查询Cookie
当客户端向服务器发出请求时,服务器端可以尝试着从请求数据包的消息头中获取是否携带了Cookie信息。实现这一功能的代码如下:

Cookie[] request.getCookies();

由于客户端是可以存放多个Cookie的,所以request提供的获取Cookie的方法的返回值是Cookie数组,如果想进一步获取某一个Cookie信息可以通过遍历数组,分别获取每一个Cookie的name和value。代码如下:

Cookie[] cookies =  request.getCookies();
    if(cookies!=null){
        for (Cookie c : cookies) {
            String cookieName = c.getName();
            String cookieValue = c.getValue();
        }
    }

查询Cookie的完整代码如下:

package Paint;

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import javax.servlet.*;
import javax.servlet.http.*;

public class FindCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response)
                          throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for(int i=0;i<cookies.length;i++){
                Cookie c = cookies[i];
                String name = c.getName();
                String value = c.getValue();
                out.println(name + ":"  value + "<br/>");
            }
        }else{
            out.println("没有找到cookie");
        }
        out.close();
    }
}

1.2.5. 如何修改Cookie
所谓Cookie的修改,本质是获取到要变更值的Cookie,通过setValue方法将新的数据存入到cookie中,然后由response响应对象发回到客户端,对原有旧值覆盖后即实现了修改。主要实现代码:

/**
其中response.addCookie(c)是非常重要的语句,如果没有这一行代码,那么就算是使用setValue方法修改了Cookie的值,但是不发回到客户端的话,也不会实现数值的改变。所以只要见到response.addCookie这行代码,即服务器端发回了带有Set-Cookie消息头的信息.
***/
Cookie[] cookies =  request.getCookies();
if(cookies!=null){
        for(Cookie c : cookies){
            String cookieName = c.getName();
            if(name.equals(“uname”)){
                c.setValue(“Mark”);
                response.addCookie( c );
        }
}

1.2.6. Cookie的生存时间
默认情况下,Cookie会被浏览器保存在内存中,此时Cookie的生命周期由浏览器决定,只要不关闭浏览器Cookie就会一直存在。

如果希望关闭浏览器后Cookie仍存在,则可以通过设置过期时间使得Cookie存在硬盘上得以保存更长的时间。设置Cookie的过期时间使用如下代码:

/**
该方法是Cookie提供的实例方法。参数seconds的单位为秒,但精度不是很高。
seconds > 0 :代表Cookie保存在硬盘上的时长
seconds = 0 : 代表Cookie的生命时长为现在,而这一刻稍纵即逝,所以马上Cookie就等同于过了生存时间,所以会被立即删除。这也是删除Cookie的实现方式。
seconds < 0 :缺省值,浏览器会将Cookie保存在内存中。
**/
void setMaxAge(int seconds);

//以下代码实现了Cookie保存在硬盘上40秒:
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.*;
import javax.servlet.*;

public class AddCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        // 创建cookie
        Cookie c = new Cookie("username", "Lisa");
        c.setMagAge(40);
        Cookie c2 = new Cookie("city", "NewYork");
        response.addCookie(c);
        response.addCookie(c2);
        out.close();
    }
}

1.2.7. Cookie编码
Cookie作为在网络传输的一段字符串文本,只能保存合法的ASCII字符,如果要保存中文需要将中文变成合法的ASCII字符,即编码。使用如下代码可以实现将中文保存到Cookie中。

Cookie c = new Cookie("city",URLEncoder.encode("北京","utf-8"));

//完整实现保存用户名和城市信息的代码如下
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import javax.servlet.*;
import javax.servlet.*;

public class AddCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response)
                            throws ServletException, IOException {
        response.setContentType(
                "text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        //创建cookie
        Cookie c = new Cookie("username",URLEncoder.encode("女神",”utf-8”));             
        Cookie c2 = new Cookie("city",URLEncoder.encode(“北京”,"utf-8"));
        response.addCookie(c);
        response.addCookie(c2);
        out.close();
    }
}

1.2.8. Cookie解码
服务器读取客户端经过编码之后的信息时,要想能够正确显示需要将信息解码后才能输出。使用URLDecoder的decode()方法即可。实现解码的完整代码如下:

import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLDecoder;
import javax.servlet.*;
import javax.servlet.*;

public class FindCookieServlet extends HttpServlet {
    public void service(HttpServletRequest request, HttpServletResponse response)
                           throws ServletException, IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter out = response.getWriter();
        Cookie[] cookies = request.getCookies();
        if(cookies != null){
            for(int i=0;i<cookies.length;i++){
                Cookie c = cookies[i];
                String name = c.getName();
                String value = c.getValue();
                out.println(name + “:” + URLDecoder.decode(value,"utf-8"));
            }
        }else{
            out.println("没有找到cookie");
        }
        out.close();
    }
}

1.3. Cookie的路径问题

1.3.1. 什么是Cookie的路径问题?
客户端存储Cookie之后,并不是针对同一个应用访问任何资源时都自动发送Cookie到服务器端,而是会进行路径的判断。只有符合路径规范的请求才会发送Cookie到服务器端。

客户端在接受Cookie时会为该Cookie记录一个默认路径,这个路径记录的是添加这个Cookie的Web组件的路径。如,当客户端向 http://localhost:8080/test/file/addCookie.jsp发送请求时创建了cookie,那么该cookie的路径就是 /test/file.

1.3.2. 什么时候发送Cookie?
只有当访问的地址是Cookie的路径或者其子路径时,浏览器才发送Cookie到服务器端。如Cookie的路径是 /test/file,那么如果访问的是 /test/file/a.jsp 或者 /test/file/b/c.jsp时,都会发送Cookie。
如果访问的是 /test/d.jsp,则浏览器不会发送Cookie。

1.3.3. 如何设置Cookie的路径?
设置Cookie的路径可以使用Cookie的API方法,setPath(String uri);
如以下代码就实现了设置Cookie的路径为应用的顶级目录,这样所有资源路径要么与此路径相等,要么是子路径,从而实现了客户端发送任何请求时都会发送Cookie。

Cookie c  = new Cookie(“uname”,“jack”);
c.setPath(“/test”);
response.addCookie(c);

1.3.4. Cookie的限制
Cookie由于存放的位置在客户端,所以可以通过修改设置被用户禁止。Cookie本质就是一小段文本,只能保存少量数据,长度是有限制的,一般为4kb左右。文本说的是只能保存字符串,不能保留复杂的对象类型数据。

作为网络中传输的内容,Cookie安全性很低,非常容易通过截取数据包来获取,在没有加密的情况下不要用于存放敏感数据。就算是能够存放的长度很短,但作为网络中传输的内容也会增加网络的传输量影响带宽。在服务器处理大量请求的时候,Cookie的传递无疑会增加网络的负载量。

2.1. Session

2.1.1. 什么是Session?
服务器为不同的客户端在内存中创建了用于保存数据的Session对象,并将用于标识该对象的唯一Id发回给与该对象对应的客户端。当浏览器再次发送请求时,SessionId也会被发送过来,服务器凭借这个唯一Id找到与之对应的Session对象。在服务器端维护的这些用于保存与不同客户端交互时的数据的对象叫做Session。

2.1.2. Session工作原理

13566833-f1918cc8ed8ead4b.png

浏览器第一次访问服务器时,服务器会为该客户端分配一块对象空间,并且使用不同的SID来进行标识,该标识SID会随着响应发回到客户端,且被保存在内存中。当同一个客户端再次发送请求时,标识也会被同时发送到服务器端,而服务器判断要使用哪一个Session对象内的数据时,就会根据客户端发来的这个SID来进行查找。

2.1.3. 如何获得Session
获得session有两种情况
(1)请求中没有SID,则需要创建;
(2)请求中包含一个SID,根据SID去找对应的对象,但也存在找到找不到的可能。
不管哪种情况都依赖于请求中的这个唯一标识,虽然对于编程人员来讲不需要去查看这个基本不会重复、编号很长的标识,但要想获取到与客户端关联的这个session对象一定要基于请求,所以在Request类型的API中包含获取到session对象的方法,代码如下所示:

/**
使用第一种获取session对象的方法时——

flag = true:先从请求中找找看是否有SID,没有会创建新Session对象,有SID会查找与编号对应的对象,找到匹配的对象则返回,找不到SID对应的对象时则会创建新Session对象。所以,填写true就一定会得到一个Session对象。

flag= false:不存在SID以及按照SID找不到Session对象时都会返回null,只有根据SID找到对应的对象时会返回具体的Session对象。所以,填写false只会返回已经存在并且与SID匹配上了的Session对象。

使用第二种获取session对象的方法——request.getSession()方法不填写参数时等同于填写true,提供该方法主要是为了书写代码时更方便,大多数情况下还是希望能够返回一个Session对象的。
**/
HttpSession s = request.getSession(boolean flag);
HttpSession s = request.getSession( );

2.1.4. 如何使用Session绑定对象
Session作为服务器端为各客户端保存交互数据的一种方式,采用name-value对的形式来区分每一组数据。向Session添加数据绑定的代码如下:

void session.setAttribute(String name,Object obj);
//获取绑定数据或移除绑定数据的代码如下:
void session.getAttribute(String name);
void session.removeAttribute(String name);

Session对象可以保存更复杂的对象类型数据了,不像Cookie只能保存字符串。

2.1.5. 如何删除Session对象
如果客户端想删除SID对应的Session对象时,可以使用Session对象的如下方法:
void invalidate()
该方法会使得服务器端与该客户端对应的Session对象不再被Session容器管理,进入到垃圾回收的状态。对于这种立即删除Session对象的操作主要应用于不再需要身份识别的情况下,如登出操作。

2.2. Session超时

2.2.1. 什么是Session超时?
Session会以对象的形式占用服务器端的内存,过多的以及长期的消耗内存会降低服务器端的运行效率,所以Session对象存在于内存中时会有默认的时间限制,一旦Session对象存在的时间超过了这个缺省的时间限制则认为是Session超时,Session会失效,不能再继续访问。

Web服务器缺省的超时时间设置一般是30分钟。

2.2.2. 如何修改Session的缺省时间限制
有两种方式可以修改Session的缺省时间限制,编程式和声明式。

void  setMaxInactiveInterval(int seconds)  //编程式
//声明式:
<session-config>
        <session-timeout>30</session-timeout>
</session-config>

使用声明式来修改缺省时间,那么该应用创建的所有Session对象的生命周期都会应用这个规定的时间,单位为分钟。

使用编程式来修改缺省时间只会针对调用该方法的Session对象应用这一原则,不会影响到其他对象,所以更灵活。通常在需要特殊设置时使用这种方式。时间单位是秒,与声明式的时间单位不同。

2.2.3. Session验证
Session既然区分不同的客户端,所以可以利用Session来实现对访问资源的保护。如,可以将资源划分为登录后才能访问。Session多用于记录身份信息,在保护资源被访问前可以通过判断Session内的信息来决定是否允许。使用Session实现验证的步骤如下:

//步骤一、为Session对象绑定数据,代码如下:
HttpSession s = request.getSession();
s.setAttribute(“uname”,“Rose”);

//步骤二、读取Session对象中的绑定值,读取成功代表验证成功,读取失败则跳转回登录页面,代码如:
HttpSession s = request.getSession();
if(s.getAttribute(“uname”)==null){
        response.sendRedirect(“logIn.jsp”);
}else{
        //… … 
}

2.2.4. Session优缺点
优:Session对象的数据由于保存在服务器端,并不在网络中进行传输,所以安全一些,并且能够保存的数据类型更丰富,同时Session也能够保存更多的数据,Cookie只能保存大约4kb的字符串。

缺:Session的安全性是以牺牲服务器资源为代价的,如果用户量过大,会严重影响服务器的性能。

2.2.5. 浏览器禁用Cookie的后果
Session对象的查找依靠的是SID,而这个ID保存在客户端时是以Cookie的形式保存的。一旦浏览器禁用Cookie,那么SID无法保存,Session对象将不能再使用。

为了在禁用Cookie后依然能使用Session,那么将使用其他的存储方法来完成SID的保存。URL地址在网络传输过程中不仅仅能够起到标示地址的作用,还可以在其后携带一些较短的数据,SID就可以通过URL来实现保存,及URL重写。

2.2.6. 什么是URL重写?
浏览器在访问服务器的某个地址时,会使用一个改写过的地址,即在原有地址后追加SessionID,这种重新定义URL内容的方式叫做URL重写。

如原有地址的写法为http://localhost:8080/test/some
而重写后的地址写法为http://localhost:8080/test/some;jsessionid=4E113CB3

2.2.7. 如何实现URL重写?
生成链接地址和表单提交时,使用如下代码:

<a href=”<%=response.encodeURL(String url)>”>链接地址</a>

//如果是重定向,使用如下代码代替response.sendRedirect()
response.encodeRedirectURL(String url);

3.1 Cookie和Session对比

3.1.1 两者区别
(1)session 能够存储任意的 java 对象,cookie 只能存储 String 类型的对象
(2)cookie在客户端而session在服务端,因Cookie在客户端所以可以编辑伪造,不是十分安全。
(3)Session过多时会消耗服务器资源,大型网站会有专门Session服务器,Cookie存在客户端不存在过多的问题。
(4)域的支持范围不一样,比方说a.com的Cookie在a.com下都能用,而www.a.com的Session在api.a.com下都不能用,解决这个问题的办法是JSONP或者跨域资源共享。
(5)单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

3.1.2 所以一般情况:
将登陆信息等重要信息存放为SESSION
其他信息如果需要保留,可以放在COOKIE中

补充

什么是Token?和Cookie和Session有什么区别?
传统身份验证一般采用的方法是,当用户请求登录的时候,会在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的 ID 号发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie ,这样服务端会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。
上面说的就是 Session,我们需要在服务端存储为登录的用户生成的 Session ,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session 。

基于 Token 的身份验证方法:
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

 

13566833-9b048700c094156b.jpg

1)客户端使用用户名跟密码请求登录
2)服务端收到请求,去验证用户名与密码
3)验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
4)客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
5)客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
6)服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

看图示,很容易理解Token登录机制,我们可以把Token理解为令牌。一般生成token用的是一个spring控制器[基于项目和项目之间的调用秘钥生成之后放redis,两小时后失效],我们看代码:

import java.security.MessageDigest;
 
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
 
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
 
import com.csair.openapi.basic.annotation.WEBApi;
import com.csair.openapi.qo.sub.TokenCredential;
import com.csair.openapi.vo.sub.TokenSuccess;
 
@RestController
@RequestMapping("/credential")
public class TokenCredentialController {
 
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
 
    private Map<String, String> key = new HashMap<String, String>();
 
    @PostConstruct
    public void init() {
        key.put("lps", "lrKvmMg3h9c8UQsvzDn0S4X");
        
    }
 
     @RequestMapping(value = "/getToken")
     @ResponseBody
     @WEBApi
     public Object export(HttpServletRequest request,HttpServletResponse response,@RequestBody TokenCredential limitsAuthority) throws Exception {
         TokenSuccess tokenSuccess   =  new TokenSuccess();
         if (limitsAuthority!=null&&limitsAuthority.getAppid()!=null&&limitsAuthority.getSecret()!=null) {//校验用户是否有权限
             String appid= limitsAuthority.getAppid();
             String secretPass =(String) key.get(appid);
             String secret = limitsAuthority.getSecret();
             if (secret.equals(secretPass)) {
                 String Timestamp= System.currentTimeMillis()+"";
                 String token = md5Password(appid+secretPass+System.currentTimeMillis()+Timestamp);
                 redisTemplate.opsForValue().set(token, Timestamp,7200, TimeUnit.SECONDS);//token和验证码对应的放到redis里面 ,2小时秒过期
                 tokenSuccess.setAccess_token(token);
                 tokenSuccess.setExpires_in("7200");
                 return tokenSuccess;
             }else{
                 throw new RuntimeException("invalid secret");          
            }
         }
         throw new RuntimeException("invalid appid");
     }

    /**
     * 生成32位md5码
     */
    public static String md5Password(String password) {
 
        try {
            // 得到一个信息摘要器
            MessageDigest digest = MessageDigest.getInstance("md5");
            byte[] result = digest.digest(password.getBytes());
            StringBuffer buffer = new StringBuffer();
            // 把每一个byte 做一个与运算 0xff;
            for (byte b : result) {
                // 与运算
                int number = b & 0xff;// 加盐
                String str = Integer.toHexString(number);
                if (str.length() == 1) {
                    buffer.append("0");
                }
                buffer.append(str);
            }
            // 标准的md5加密后的结果
            return buffer.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return "";
        }
    }
    
}

用java自定义注解引入Aop来鉴权

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthToken {
    
 
}

import javax.servlet.http.HttpServletRequest;
 
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
 
import com.csair.cocc.basic.constant.EnvironmentEnum;
import com.csair.openapi.basic.annotation.AuthToken;
 
@Component
@Aspect
public class AuthTokenDecorator implements Ordered {
    
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Value("${environment}")
    private String environment;
 
    @Around("within(com.csair.**.controller.**.*) && @annotation(authToken)")
    public Object decorate(ProceedingJoinPoint pjp, AuthToken authToken) throws Throwable {
         
      if (EnvironmentEnum.DEV.getValue().equals(environment)) {//如果是开发环境
            return pjp.proceed();//这个是可以继续传输对象到Controller的逻辑
      }
        
      Object[] obj = pjp.getArgs();
      HttpServletRequest request = (HttpServletRequest) obj[0];
      String accessToken = request.getParameter("accessToken");
      logger.info("accessToken值為:"+accessToken);
      
      if (StringUtils.isEmpty(accessToken)) {
             throw new RuntimeException("token is null");       
        }else {
            String timestamp = redisTemplate.opsForValue().get(accessToken); 
            if (StringUtils.isEmpty(timestamp)) {
             throw new RuntimeException("Invalid token");       
            }
        }
        return pjp.proceed();
    }
 
    public int getOrder() {
        return 9;
    }
    
}

引用Redis配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 
  http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
 
    <context:property-placeholder location="classpath:redis.properties" />
 
    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxTotal" value="5000" />
        <property name="maxIdle" value="2000" />
        <property name="maxWaitMillis" value="4000" />
        <property name="testOnBorrow" value="true" />
        <property name="testOnReturn" value="true" />
        <property name="testWhileIdle" value="true" />
    </bean>
 
    <bean id="redisSentinelConfiguration"
        class="org.springframework.data.redis.connection.RedisSentinelConfiguration">
 
        <property name="master">
            <bean class="org.springframework.data.redis.connection.RedisNode">
                <property name="name" value="${redis.master.name}"></property>
            </bean>
        </property>
 
        <property name="sentinels">
            <set>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel1.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel1.port}"></constructor-arg>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel2.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel2.port}"></constructor-arg>
                </bean>
                <bean class="org.springframework.data.redis.connection.RedisNode">
                    <constructor-arg name="host" value="${redis.sentinel3.host}"></constructor-arg>
                    <constructor-arg name="port" value="${redis.sentinel3.port}"></constructor-arg>
                </bean>
            </set>
        </property>
 
    </bean>
 
    <bean id="jedisConnectionFactory"
        class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="password" value="${redis.password}" />
        <property name="poolConfig" ref="jedisPoolConfig" />
        <constructor-arg ref="redisSentinelConfiguration" />
    </bean>
 
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory" />
        <property name="keySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="valueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashKeySerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="hashValueSerializer">
            <bean
                class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
    </bean>
 
</beans>

最重要的是Controller的入参要加上HttpServletRequest request
@RequestMapping(value = "/saveCargoPlaneUploadLpsInfo", method = RequestMethod.POST)
@ResponseBody
@WEBApi
@AuthToken
public Object saveCargoPlaneUploadLpsInfo(HttpServletRequest request,@RequestBody CargoPlaneUploadLpsInfoDto param)

我们再看一个简单的例子:

import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang.StringUtils;


public class TokenUtil {

    private static final String[] codeBase= {"0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"};

    private static Random rand= new Random();

    /** XXTEA加密解密的密钥 */
    private static String secKey = "captcha";

    /** token超时门限(天) */
    private static long expire = 30;


    /** 验证码字符数 */
    private static int charCount = 4;

    public static final  String  genToken() {
        StringBuffer sb= new StringBuffer();
        for(int i=0; i<charCount; i++){
            int randInt= Math.abs(rand.nextInt());
            sb.append(codeBase[randInt % codeBase.length]);
        }
        long timestamp= System.currentTimeMillis();
        String token= null;
        token= String.format("%s_%d", sb.toString(), timestamp);
        System.out.println("未加密的token:"+token);
        token= XXTEAUtil.encrypt(token, secKey);
        return token;
    }

    public static final boolean verificationToken(String token) throws StatusInfoException{
        String plainText= XXTEAUtil.decrypt(token, secKey);
        if (StringUtils.isBlank(plainText)){
                throw new IllegalStateException("解密失败,token可能遭到篡改");
            }
            String[] plainTextArr= plainText.split("_");
            if (plainTextArr.length!=2){
                throw new IllegalStateException("token数据格式错误");
            }
            long timestamp= 0;
            try{
                timestamp= Long.parseLong(plainTextArr[1]);
            }catch(NumberFormatException e){
                throw new IllegalStateException("时间戳无效");
            }
            if ((System.currentTimeMillis() - timestamp)>TimeUnit.MILLISECONDS.convert(expire+5, TimeUnit.DAYS)){
                throw new IllegalStateException("token已过期");
            }
        return true;
    }
}

引入加密解密Util工具类,代码如下:

import org.apache.commons.codec.binary.Base64;


public class XXTEAUtil {

    /**
     * 使用密钥加密数据
     * @param plain
     * @param key
     * @return
     */
    public static byte[] encrypt(byte[] plain, byte[] key) {
        if (plain.length == 0) {
            return plain;
        }
        return toByteArray(encrypt(toIntArray(plain, true), toIntArray(key, false)), false);
    }

    /**
     * 使用密钥解密
     * @param cipher
     * @param key
     * @return
     */
    public static byte[] decrypt(byte[] cipher, byte[] key) {
        if (cipher.length == 0) {
            return cipher;
        }
        return toByteArray(decrypt(toIntArray(cipher, false), toIntArray(key, false)), true);
    }

    /**
     * 使用密钥加密数据
     * @param v
     * @param k
     * @return
     */
    public static int[] encrypt(int[] v, int[] k) {
        int n = v.length - 1;

        if (n < 1) {
            return v;
        }
        if (k.length < 4) {
            int[] key = new int[4];

            System.arraycopy(k, 0, key, 0, k.length);
            k = key;
        }
        int z = v[n], y = v[0], delta = 0x9E3779B9, sum = 0, e;
        int p, q = 6 + 52 / (n + 1);

        while (q-- > 0) {
            sum = sum + delta;
            e = sum >>> 2 & 3;
            for (p = 0; p < n; p++) {
                y = v[p + 1];
                z = v[p] += (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
            }
            y = v[0];
            z = v[n] += (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
        }
        return v;
    }

    /**
     * 使用密钥解密数据
     * @param v
     * @param k
     * @return
     */
    public static int[] decrypt(int[] v, int[] k) {
        int n = v.length - 1;

        if (n < 1) {
            return v;
        }
        if (k.length < 4) {
            int[] key = new int[4];

            System.arraycopy(k, 0, key, 0, k.length);
            k = key;
        }
        int z = v[n], y = v[0], delta = 0x9E3779B9, sum, e;
        int p, q = 6 + 52 / (n + 1);

        sum = q * delta;
        while (sum != 0) {
            e = sum >>> 2 & 3;
        for (p = n; p > 0; p--) {
            z = v[p - 1];
            y = v[p] -= (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
        }
        z = v[n];
        y = v[0] -= (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z);
        sum = sum - delta;
        }
        return v;
    }

    /**
     * 字节数组转换为整型数组
     * @param data
     * @param includeLength
     * @return
     */
    private static int[] toIntArray(byte[] data, boolean includeLength) {
        int n = (((data.length & 3) == 0) ? (data.length >>> 2) : ((data.length >>> 2) + 1));
        int[] result;

        if (includeLength) {
            result = new int[n + 1];
            result[n] = data.length;
        } else {
            result = new int[n];
        }
        n = data.length;
        for (int i = 0; i < n; i++) {
            result[i >>> 2] |= (0x000000ff & data[i]) << ((i & 3) << 3);
        }
        return result;
    }

    /**
     * 整型数组转换为字节数组
     * @param data
     * @param includeLength
     * @return
     */
    private static byte[] toByteArray(int[] data, boolean includeLength) {
        int n = data.length << 2;
        if (includeLength) {
            int m = data[data.length - 1];

            if (m > n) {
                return null;
            } else {
                n = m;
            }
        }
        byte[] result = new byte[n];

        for (int i = 0; i < n; i++) {
            result[i] = (byte) ((data[i >>> 2] >>> ((i & 3) << 3)) & 0xff);
        }
        return result;
    }

    /**
     * 先XXXTEA加密,后Base64加密
     * @param plain
     * @param key
     * @return
     */
    public static String encrypt(String plain, String key) {
        String cipher = "";
        byte[] k = key.getBytes();
        byte[] v = plain.getBytes();
        cipher = new String(Base64.encodeBase64(XXTEAUtil.encrypt(v, k)));
        cipher = cipher.replace('+', '-');
        cipher = cipher.replace('/', '_');
        cipher = cipher.replace('=', '.');
        return cipher;
    }

    /**
     * 先Base64解密,后XXXTEA解密
     * @param cipher
     * @param key
     * @return
     */
    public static String decrypt(String cipher, String key) {
        String plain = "";
        cipher = cipher.replace('-', '+');
        cipher = cipher.replace('_', '/');
        cipher = cipher.replace('.', '=');
        byte[] k = key.getBytes();
        byte[] v = Base64.decodeBase64(cipher);
        plain = new String(XXTEAUtil.decrypt(v, k));
        return plain;
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值