token与幂等性问题

1、token

1、token的特点

1、token是一种客户端认证机制,是一个经过加密的字符串,安全性强,支持跨域

2、用户第一次登录,服务器通过数据库校验其UserId和Password合法,则再根据随机数字+userid+当前时间戳再经过DES加密生成一个token串 ⑴当然具体生成token的方式是开发自己定义的 3、token的生成一般是采用uuid保证唯一性,当用户登录时为其生成唯一的token,存储一般保存在数据库中 ⑴token过期时间采用把token二次保存在cookie或session里面,根据cookie和session的过期时间去维护token的过期时间

4、Token是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回Token给前端。前端可以在每次请求的时候带上Token证明自己的合法地位

5、Token,就是令牌,最大的特点就是随机性,不可预测。一般黑客或软件无法猜测出来

2、token的实现过程

建立一个token令牌,在用户登录时候给用户一个独特得令牌值,登录时候嘚赋值这个令牌

在SpringBoot项目中建立一个Util文件夹

文件夹下建立TokenUtil.java文件

public class TokenUtil {
 
 
    private static Map tokenMap = new HashMap<>();
 
 
    public static String generateToken(User user){
        //生成唯一不重复的字符串
        String token = UUID.randomUUID().toString();
        tokenMap.put(token,user);
        return token;
    }
 
    /**
     * 验证token是否合法
     * @param token
     * @return
     */
    public static  boolean verify(String token){
        //map中的方法 用来判断存不存在这个传入的key,也就是token
        return tokenMap.containsKey(token);
    }
 
    public static User gentUser(String token){
        //通过token返回用户信息
        return tokenMap.get(token);
    }
 
    public static void main(String[] args) {
        for (int i = 0; i < 20; i++){
            System.out.println(UUID.randomUUID().toString());
        }
    }
 
}

用户登录

@Api( tags = {"用户模块接口"})
@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    private UserService userService;
 
    @Autowired
    private HttpSession session;
    @ApiOperation("登录接口")
    @RequestMapping(value = "login",method ={RequestMethod.POST,RequestMethod.GET})
    public Map login(User user){
        Map map = new HashMap<>();
        map.put("code",0);
        //用.length != 0的方式代替isEmpty更好
        //判断账户密码是否为空
        if(StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword()) ){
            map.put("msg","用户或者密码为空!");
            return map;
        }
        QueryWrapper queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("username",user.getUsername())
                .eq("password",user.getPassword());
        User userDb = userService.getOne(queryWrapper);
        //得到的前端数据不为空
        if(userDb != null){
            String token= TokenUtil.generateToken(userDb);
            map.put("code",1);
            map.put("data",userDb);
            map.put("token",token);
            //前端获取的数据用sessionStorage
            session.setAttribute("username",userDb.getUsername());
        }else{
            map.put("msg","用户名或密码错误!");
        }
        return map;
    }
    @ApiImplicitParams(
            {
            @ApiImplicitParam(name = "id",
                    value = "用户id", required = true,
                    dataType = "Long"),
            @ApiImplicitParam(name = "name",
                    value = "测试名字",
                    dataType = "string")
            }
    )
    @ApiOperation("根据id查询用户信息")
    @RequestMapping(value="getById",method ={RequestMethod.POST,RequestMethod.GET})
    public  User getById(Long id ,String name){
        System.out.println(name);
        return userService.getById(id);
    }
 
}

在拦截器上操作 interceptor下面LoginInterceptor.java

public class LoginInterceptor implements HandlerInterceptor {
 
    @Autowired
    private HttpSession httpSession;
 
    //Controller逻辑执行之前
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("preHandle....");
        String uri = request.getRequestURI();
        System.out.println("当前路径:"+uri);
        /**
         * HandlerMethod=>Controller中标注@RequestMapping的方法
         *  需要配置静态资源不拦截时,添加这块逻辑  => 前后端分离项目
         *  怕通过url进行访问,所以当进行访问的时候在filter中进行判断获取到唯一标识token才放行
         *  要不就返回登录界面
         */
        // 是我们的conrtoller中的方法
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //把前端返回的数据放重写http协议的协议头里面
        String token = request.getHeader("qcby-token");
        //看二者token是否一致
        if (!TokenUtil.verify(token)) {
            // 未登录跳转到登录界面
           throw  new RuntimeException("no login!");
        } else {
            return true;
        }
    }
 
    //Controller逻辑执行完毕但是视图解析器还未进行解析之前
    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        System.out.println("postHandle....");
    }
 
    //Controller逻辑和视图解析器执行完毕
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        System.out.println("afterCompletion....");
    }
}


2、接口幂等性

1、什么是接口幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条...,这就没有保证接口的幂等性

2、幂等性分为两种情况:

第一种是服务器好使但是网络有问题导致用户没有接收到想要的信息,再次点击按钮

第二种是服务器不好使导致用户没有接收到想要的信息,再次点击按钮

3、什么情况下需要幂等

用户多次点击按钮用户页面回退再次提交微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制

4、解决办法

1)token机制

①、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。

②、然后前端调用业务接口请求时,把 token 携带过去,一般放在请求头部。

③、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业务。

④、如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行

这种方式存在的危险性

【注意】保证:从redis取值,比较,删除redis的key是一个原子操作加入分布式锁,要不然的话当下一次请求过来的时候,第一个执行到查找到token然后时间片进行轮转,没删除,第二个就来进行判断了,发现有服务器生成的token,这个时候还会出现幂等性问题,怎么绑定呢就是将上述操作放进一个zookeeper中 生成一个节点,、这样的话,当生成第二个token之后,是会放另一个新节点当中,从而实现原子操作

先分析业务是否需要token

进入支付页面,服务器自动生成一个token,存于redis中,,然后当输入完账户密码之后,点击支付,这个时候会再次生成一个token,这个token会去redis中那个服务器生成的token进行比较,如果说存在,则证明是第一次进行支付,然后删除token,继续执行操作,

如果说不存在则证明支付过了,那么就证明是重复操作,这种情况之下,我们就返回重复标记给客户端,这样就保证了幂等操作,

redis中取出token,比较token,删除token,是一个原子性操作,一起进行

2)数据库中加一列

用CAS的方式进行判断,访问支付页面时生成一个预值存于redis中,然后第一次支付时传一个值与预值是否一样,如果一样证明第一次支付,然后进行删除预值,这样第二次来的情况下看redis中还有没有预值,发现没有,那么证明不是第一次,这样就解决了幂等性的问题

你可能感兴趣的:(java,java,开发语言)