• 04 动力云客之登录后获取用户信息+JWT存进Redis+Filter验证Token + token续期


    在这里插入图片描述

    1. 登录后获取用户信息

    非常好实现. 只要新建一个controller, 并调用SS提供的Authentication对象即可

    package com.sunsplanter.controller;
    
    @RestController
    public class UserController {
    
        @GetMapping(value = "api/login/info")
        public R loginInfo(Authentication authentication) {
            TUser tUser = (TUser)authentication.getPrincipal();
    
            return R.OK(tUser);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    未登录状态下可以直接访问 api/login/info吗?

    不可以. 因为在安全配置类已经写明了, 仅登陆界面允许任何人访问, 其他所有界面都需要认证

    <由于未写JWT, 默认使用Session 保存会话,> ???好像不对
    因此只要我们先通过登录接口登录, 然后再直接访问获取用户信息接口即可
    在这里插入图片描述

    2. 使用JWT打通登录后各个页面的认证

    前后端分离的项目一般会使用token(jwt)实现登录状态的保持;(java web : session)

    token其实就是一个随机字符串(字符串要求是唯一的,不同人的token都不能相同),当用户在登录页面输入账号和密码后,前端将账号密码发送给后端,后端检验完账号和密码后,会生成一个随机不重复的字符串即(token),并将其响应给前端,前端拿到token后,需要在客户端进行持久化存储(一般会写在localStorage或者sessionStorage中),那么下次在向后端数据接口发送请求的时候,一般需要将token一并发送给后端数据接口,后端数据接口会对token进行校验,如果合法则正常响应请求,如果不合法,则提示未登录。

    2.1 sessionStorage与localStorage

    它们是javascript对象,浏览器支持这两个对象,可以直接使用. 属于前端范畴
    localStorage和sessionStorage都是用来在浏览器客户端存储临时信息的对象;

    sessionStorage、localStorage区别?

    sessionStorage只在一个浏览器页面有效,比如你打开一个新的tab浏览器页会失效,你关闭浏览器后,再打开浏览器也会失效;
    localStorage在整个浏览器中都有效,重启浏览器也有效;除非你手动删除了localStorage,才会失效;

    2.2 修改登录成功拦截器

    根据流程图, 在查询到对象并返回给SS后, SS会调用成功拦截器,
    就在这个拦截器中, 根据查询到的对象生成JWT并同时存进Redis和返回前端.

    //登录成功会自动执行这个类中的onAuthenticationSuccess方法, 该方法返回自定义的Json给前端
    @Component
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
        @Resource
        private RedisService redisService;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            //登录成功,执行该方法,在该方法中返回json给前端,就行了
            TUser tUser = (TUser) authentication.getPrincipal();
    
            //1.生成JWT
            //由于createJWT方法定义的参数是序列化后的对象, 因此先调用JSONUtils序列化对象
            String userJSON = JSONUtils.toJSON(tUser);
            String jwt = JWTUtils.createJWT(userJSON);
    
            //2.写入Redis
            redisService.setValue(Constants.REDIS_JWT_KEY + tUser.getId(), jwt);
    
            //3. 设置JWT的过期时间(如果选择记住我, 过期时间是7天, 否则30分钟)
            String rememberMe = request.getParameter("rememberMe");
            //勾选了记住我就设置为7天, 其中EXPIRE_TIME就是7天,DEFAULT_EXPIRE_TIME是半小时
            if (Boolean.parseBoolean(rememberMe)){
                redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId() , Constants.EXPIRE_TIME, TimeUnit.SECONDS);
            } else {
                redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId() , Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
            }
    
            //登录成功的统一结果
            R result = R.OK(jwt);
    
            //把R对象转成json
            String resultJSON = JSONUtils.toJSON(result);
    
            //把R以json返回给前端
            ResponseUtils.write(response, resultJSON);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    常量类为

    package com.sunsplanter.constant;
    
    /**
     * 常量类
     */
    public class Constants {
    
        public static final String LOGIN_URI = "/api/login";
    
        //redis的key的命名规范: 项目名:模块名:功能名:唯一业务参数(比如用户id)
        public static final String REDIS_JWT_KEY = "dlyk:user:login:";
    
        //redis中负责人的key
        public static final String REDIS_OWNER_KEY = "dlyk:user:owner";
    
        //jwt过期时间7天
        public static final Long EXPIRE_TIME = 7 * 24 * 60 * 60L;
    
        //jwt过期时间30分钟
        public static final Long DEFAULT_EXPIRE_TIME = 30 * 60L;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    redis类 为

    package com.sunsplanter.service
    
    public interface RedisService {
    
        void setValue(String key, Object value);
    
        Object getValue(String key);
    
        Boolean removeValue(String key);
    
        //给jwt设置过期时间
        Boolean expire(String key, Long timeOut, TimeUnit timeUnit);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    package com.sunsplanter.service.impl;
    
    @Service
    public class RedisServiceImpl implements RedisService {
    
        @Resource
        private RedisTemplate<String, Object> redisTemplate;
    
        @Override
        public void setValue(String key, Object value) {
            redisTemplate.opsForValue().set(key, value);
        }
    
        @Override
        public Object getValue(String key) {
            return redisTemplate.opsForValue().get(key);
        }
    
        @Override
        public Boolean removeValue(String key) {
            return redisTemplate.delete(key);
        }
    
        //给JWT设置过期时间
        @Override
        public Boolean expire(String key, Long timeOut, TimeUnit timeUnit) {
            return redisTemplate.expire(key, timeOut, timeUnit);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在网页登录后可以看到已经存到了localstorage
    在这里插入图片描述

    3 Filter验证Token

    如流程图所示, 第一次登录成功过后, 往后每次登录会携带token登录,

    因此, 在config.filter文件夹下新建一个token过滤器类, 该类实现OncePerRequestFilter
    并在SS的安全配置类中中添加进去

    package com.sunsplanter.config.filter;
    
    @Component
    public class TokenVerifyFilter extends OncePerRequestFilter{
    
        @Resource
        private RedisService redisService;
    
        //spring boot框架的ioc容器中已经创建好了该线程池,可以注入直接使用
        @Resource
        private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            if (request.getRequestURI().equals(Constants.LOGIN_URI)) { //如果是登录请求,此时还没有生成jwt,那不需要对登录请求进行jwt验证
                //验证jwt通过了 ,让Filter链继续执行,也就是继续执行下一个Filter
                filterChain.doFilter(request, response);
    
            } else {
                String token = null;
                if (request.getRequestURI().equals(Constants.EXPORT_EXCEL_URI)) {
                    //从请求路径的参数中获取token
                    token = request.getParameter("Authorization");
                } else {
                    //其他请求都是从请求头中获取token
                    token = request.getHeader("Authorization");
                }
    
                if (!StringUtils.hasText(token)) {
                    //token验证未通过的统一结果类
                    R result = R.FAIL(CodeEnum.TOKEN_IS_EMPTY);
                    //把R对象转成json
                    String resultJSON = JSONUtils.toJSON(result);
                    //把R以json返回给前端
                    ResponseUtils.write(response, resultJSON);
                    return;
                }
    
                //验证token有没有被篡改过
                if (!JWTUtils.verifyJWT(token)) {
                    //token验证未通过统一结果类
                    R result = R.FAIL(CodeEnum.TOKEN_IS_ERROR);
    
                    //把R对象转成json
                    String resultJSON = JSONUtils.toJSON(result);
    
                    //把R以json返回给前端
                    ResponseUtils.write(response, resultJSON);
    
                    return;
                }
    
                TUser tUser = JWTUtils.parseUserFromJWT(token);
                String redisToken = (String) redisService.getValue(Constants.REDIS_JWT_KEY + tUser.getId());
    
                //验证token非空
                if (!StringUtils.hasText(redisToken)) {
                    //token验证未通过统一结果类
                    R result = R.FAIL(CodeEnum.TOKEN_IS_EXPIRED);
    
                    //把R对象转成json
                    String resultJSON = JSONUtils.toJSON(result);
    
                    //把R以json返回给前端
                    ResponseUtils.write(response, resultJSON);
    
                    return;
                }
    
                //验证token是否与redis中的一致
                if (!token.equals(redisToken)) {
                    //token验证未通过的统一结果类
                    R result = R.FAIL(CodeEnum.TOKEN_IS_NONE_MATCH);
    
                    //把R对象转成json
                    String resultJSON = JSONUtils.toJSON(result);
    
                    //把R以json返回给前端
                    ResponseUtils.write(response, resultJSON);
                    return;
                }
    
                //jwt验证通过了,那么在spring security的上下文环境中要设置一下,设置当前这个人是登录过的,你后续不要再拦截他了
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(tUser, tUser.getLoginPwd(), tUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    
                //刷新一下token(异步处理,new一个线程去执行)
                /*new Thread(() -> {
                    //刷新token
                    String rememberMe = request.getHeader("rememberMe");
                    if (Boolean.parseBoolean(rememberMe)) {
                        redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
                    } else {
                        redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
                    }
                }).start();*/
    
                //异步处理(更好的方式,使用线程池去执行)
                threadPoolTaskExecutor.execute(() -> {
                    //刷新token
                    String rememberMe = request.getHeader("rememberMe");
                    if (Boolean.parseBoolean(rememberMe)) {
                        redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
                    } else {
                        redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
                    }
                });
    
                //验证jwt通过了 ,让Filter链继续执行,也就是继续执行下一个Filter
                filterChain.doFilter(request, response);
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114

    4. token续期

    现在一个问题是

    token一旦发布, 是7天/30分钟 .

    原来没有续期时 , token只能获得与提前删除.

    然而一个现实需求是

    假如用户获得是30分钟的token. 在这三十分钟内, 只要用户有任何操作, 我们应当自动刷新token到30分钟 , 否则假如不管用户怎么操作, token都固定30分钟刷新 , 这显然不符合逻辑

    目标为, 任何除登出的用户登录都会重置token过期时间

    然后是代码放在哪里的问题

    我们知道, 即使登录过后 , 我们在每一次操作时仍会用TokenVerifyFilter检查token的有效期.

    因此直接把续期代码放到该类中即可, 每次有操作->执行TokenVerifyFilter检查token有效期->有效则续期token

    在TokenVerifyFilter中新增刷新代码

    //刷新一下token(异步处理,new一个线程去执行)
    //没必要因为续期token而阻塞整个进程, 毕竟此时已经校验过了不影响本次执行
                new Thread(() -> {
                    //刷新token
                    String rememberMe = request.getHeader("rememberMe");
                    if (Boolean.parseBoolean(rememberMe)) {
                        redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.EXPIRE_TIME, TimeUnit.SECONDS);
                    } else {
                        redisService.expire(Constants.REDIS_JWT_KEY + tUser.getId(), Constants.DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
                    }
                }).start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    JMeter的使用
    异步编程-线程实现异步编程
    Parasoft与 IAR Systems开发工具集成
    ElasticSearch集群配置
    折半查找的判定树
    学习格式化dedecms模版里格式化时间标签pubdate的方法
    【单目标优化求解】基于matlab平衡算法求解单目标优化问题【含Matlab源码 2114期】
    纯电小型领军者 奇瑞无界Pro
    非线性有限元:基本理论与算法及基于Python、Fortran程序实现与案例分析实践技术
    linux 查看可支持的shell
  • 原文地址:https://blog.csdn.net/m0_46671240/article/details/136195509