SaToken+SpringBoot+Redis前后端分离登录认证

SaToken+SpringBoot+Redis前后端分离登录认证

码农世界 2024-05-24 前端 66 次浏览 0个评论

目录

  • 前言
  • 一、创建工程项目🎍
    • 1.1 创建后端工程
    • 1.2 创建前端工程
    • 二、业务代码🎊
      • 后端代码
      • 前端代码
      • 三、测试
      • 参考资料

        前言

        Sa-Token 是一款 Java 语言的权限认证框架,提供了灵活、高效、易用的权限认证和会话管理功能。它是 SpringBoot、Spring MVC、Servlet 等 Java 技术体系下的轻量级权限认证组件,可以帮助开发者快速实现用户认证、授权和会话管理等功能。

        Sa-Token官方文档

        功能结构图

        认证流程图

        框架特性

        一、创建工程项目🎍

        1.1 创建后端工程

        引入依赖

        
            org.springframework.boot
            spring-boot-starter-parent
            2.6.13
             
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            cn.dev33
            sa-token-spring-boot-starter
            1.38.0
        
        
            cn.dev33
            sa-token-redis-fastjson2
            1.37.0
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            org.projectlombok
            lombok
            true
        
        

        注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。

        基础配置

        server:
          port: 8081
        spring:
          redis:
            database: 1
            host: 127.0.0.1
            port: 6379
            password:
            timeout: 5000
            jedis:
              pool:
                max-active: 8
                max-wait: -1
                max-idle: 8
                min-idle: 0
        #Sa-token相关配置(与官网一致)
        sa-token: 
            # token 名称(同时也是 cookie 名称)
            token-name: satoken
            # token 有效期(单位:秒) 默认30天,-1 代表永久有效
            timeout: 2592000
            # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
            active-timeout: -1
            # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
            is-concurrent: true
            # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
            is-share: true
            # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
            token-style: uuid
            # 是否输出操作日志 
            is-log: true
        

        1.2 创建前端工程

        本项目使用的vue3,创建一个vue项目,引入下方依赖即可

        "dependencies": {
          "axios": "^1.6.8",
          "element-plus": "^2.7.3",
          "pinia": "^2.1.7",
          "qs": "^6.12.1",
          "vue": "^3.2.37",
          "vue-router": "^4.2.5"
        },
        

        二、业务代码🎊

        后端代码

        User.java

        @Data
        public class User{
            //id
            @TableId(type = IdType.ASSIGN_ID)
            private Long id;
            //用户名
            private String username;
            //密码
            private String password;
            //账户是否锁住(1被锁0未被锁)
            private Integer isLocked;
            //账户是否被删除(1删除0未被删除)
            @TableLogic
            private Integer isDelete;
            //创建时间
            @TableField(fill = FieldFill.INSERT)
            private Date createTime;
            //更新时间
            @TableField(fill = FieldFill.INSERT_UPDATE)
            private Date updateTime;
        }
        

        UserDTO.java

        @EqualsAndHashCode(callSuper = true)
        @Data
        public class UserDTO extends User {
            //记住我
            private boolean rememberMe;
            //角色列表
            private List roleList;
            //登录的设备
            private String device;
            //图形验证码
            private String code;
            //图形验证码的key
            private String codeKey;
        }
        

        UserController.java

        注意:代码中定义了一些常量,可以自行替代。

        @RestController
        @RequestMapping("/user")
        @CrossOrigin
        public class UserController {
            
            @Autowired
            UserService userService;
            @Autowired
            StringRedisTemplate stringRedisTemplate;
            @PostMapping("/doLogin")
            public SaResult doLogin(@RequestBody UserDto userDto) {
                //从redis中取生成的验证码
                String validateCode = stringRedisTemplate.opsForValue().get("code:validate:" + userDto.getCodeKey());
                if (userDto.getCode().equals(validateCode)) {
                    //验证码正确才进行用户验证,先验证了用户存不存在,再验证了密码是否正确
                    if (userService.login(userDto.getUsername(), userDto.getPassword())) {
                        StpUtil.login(userDto.getUsername(), new SaLoginModel()
                                 //实现‘记住我’功能
                                .setIsLastingCookie(userDto.isRememberMe())
                                 //设置登录设备,主要用于实现同端互斥登录,此处没有实现该功能,可以不用管
                                .setDevice("PC"));
                        //验证成功就以json的形式返回token
                        HashMap resultMap = new HashMap<>();
                        resultMap.put("token", StpUtil.getTokenValue());
                        return SaResult
                                .ok(ResponseCode.LOGIN_SUCCESS.getMessage())
                                .setCode(ResponseCode.LOGIN_SUCCESS.getCode())
                                .setData(resultMap);
                    } else {
                        return SaResult
                                .error(ResponseCode.USERNAME_PASSWORD_ERROR.getMessage())
                                .setCode(ResponseCode.USERNAME_PASSWORD_ERROR.getCode());
                    }
                } else {
                    return SaResult
                            .error(ResponseCode.VALIDATE_CODE_ERROR.getMessage())
                            .setCode(ResponseCode.VALIDATE_CODE_ERROR.getCode());
                }
            }
        }
        

        StpUtils.login:

        1. 检查此账号是否之前已有登录;
        2. 为账号生成 Token 凭证与 Session 会话;
        3. 记录 Token 活跃时间;
        4. 通知全局侦听器,xx 账号登录成功;
        5. 将 Token 注入到请求上下文;
        6. 等等其它工作……

        SaResult:这个也是一个由Satoken封装的结果响应类,还是挺好用的。

        UserService.java

        public interface UserService {
            boolean login(String username, String password);
        }
        

        UserServiceImpl.java

        @Service
        public class UserServiceImpl implements UserService{
            @Autowired
            UserDao userDao;
            
                @Override
            public boolean login(String username, String password) {
                User user = userDao.selectOne(new QueryWrapper().eq("username", username));
                //数据库中的密码进行了加密BCrypt也是Satoken提供的一个工具类
                return user != null && BCrypt.checkpw(password, user.getPassword());
            }
        }
        

        UserDao.java

        /**
         * (User)表数据库访问层
         *
         * @author yzk
         * @since 2024-05-15 16:44:29
         */
        public interface UserDao extends BaseMapper {
        }
        

        SaAuthenticationConfigure.java

        @Configuration
        public class SaAuthenticationConfigure implements WebMvcConfigurer {
            // 注册 Sa-Token 拦截器,打开注解式鉴权功能 
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                // 注册 Sa-Token 拦截器,打开注解式鉴权功能
                registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin()))
             			//所有接口都会检查是否登录了
                        .addPathPatterns("/**")
                    	//以下接口不检查,直接放行
                        .excludePathPatterns("/user/doLogin")
            }
        }
        

        SaTokenFilter.java

        @Configuration
        public class SaTokenFilter implements WebMvcConfigurer {
            /**
             * 注册 [Sa-Token 全局过滤器] 
             */
            @Bean
            public SaServletFilter getSaServletFilter() {
                return new SaServletFilter()
                       
                       // 指定 [拦截路由] 与 [放行路由]
                       .addInclude("/**").addExclude("/favicon.ico")
                       
                       // 认证函数: 每次请求执行 
                       .setAuth(obj -> {
                        SaManager.getLog().debug("----- 请求path={}  提交token={}", SaHolder.getRequest().getRequestPath(), StpUtil.getTokenValue());
                          // ...
                       })
                       
                       // 异常处理函数:每次认证函数发生异常时执行此函数 
                       .setError(e -> {
                          return SaResult.error(e.getMessage());
                       })
                       
                       // 前置函数:在每次认证函数之前执行
                       .setBeforeAuth(obj -> {
                        SaHolder.getResponse()
                          // ---------- 设置跨域响应头 ----------
                          // 允许指定域访问跨域资源
                          .setHeader("Access-Control-Allow-Origin", "*")
                          // 允许所有请求方式
                          .setHeader("Access-Control-Allow-Methods", "*")
                          // 允许的header参数
                          .setHeader("Access-Control-Allow-Headers", "*")
                          // 有效时间
                          .setHeader("Access-Control-Max-Age", "3600")
                          ;
                          
                          // 如果是预检请求,则立即返回到前端 
                          SaRouter.match(SaHttpMethod.OPTIONS)
                             .free(r -> System.out.println("--------OPTIONS预检请求,不做处理"))
                             .back();
                       })
                       ;
            }
        }
        

        注意这个过滤器!!!!!!!!!!务必配置这个过滤器,用SaToken时,后端使用@CrossOrigin进行了跨域的配置,但是!前端发起请求还是会报跨域问题,而且后端会报'未读取到有效token',是因为前端发起请求时,先发起了预检请求,SaToken拦截了预检请求,预检请求的headers中没有token,所以一直报错。

        前端代码

        此处删减了部分代码,只保留了登录登录相关的代码

        login.vue

        
        
        
        

        前端点击登录按钮时,发后端接口发起请求,请求成功后,取出响应结果中的token存入localStorage。

        requests.js

        import axios from 'axios';
        import {router} from "@/router";
        axios.defaults.crossDomain = true
        export const requests = axios.create({
            baseURL: 'http://localhost:8081',
            timeout: 10000, // 请求超时时间
            headers: {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            }
        });
        // 请求拦截器
        requests.interceptors.request.use(
            (config) => {
                // 在发送请求之前做些什么
                const token = localStorage.getItem('token'); // 假设token存储在localStorage中
                if (token) {
                    config.headers.Token = `Bearer ${token}`; // 添加token到请求头
                }
                return config;
            },
            (error) => {
                // 对请求错误做些什么
                return Promise.reject(error);
            }
        );
        // 响应拦截器
        requests.interceptors.response.use(
            (response) => {
                console.log(response.data.code)
                const code=response.data.code
                if (code === 5006 ||code===5007|| code===5008) {
                    //返回登录界面
                    router.push('/').then(r =>{
                        location.reload()
                    } )
                    // router.go(0)
                    //删除当前localStorage中的token
                    localStorage.removeItem("token")
                }
                return response.data;
            },
            (error) => {
                // 对响应错误做点什么
                return Promise.reject(error);
            }
        );
        

        注意请求拦截器,每次发起请求时,都会从localStorage中取出token,并将其放入headers中

        三、测试

        集成了redis后,记得先开启redis服务,在登录时,框架会自动保存数据

        1. 开启前后端服务

          本文的代码是从完整项目中抽出的,上述代码只有登录功能

        输入用户名和密码后,进入主界面

        此时,登录的信息已经自动的被框架自动保存进redis

        IDEA控制台打印出相应的信息

        参考资料

        Sa-Token

        使用 Sa-Token 的全局过滤器解决跨域问题(三种方式全版) - 掘金 (juejin.cn)

        Sa-Token功能结构图

转载请注明来自码农世界,本文标题:《SaToken+SpringBoot+Redis前后端分离登录认证》

百度分享代码,如果开启HTTPS请参考李洋个人博客
每一天,每一秒,你所做的决定都会改变你的人生!

发表评论

快捷回复:

评论列表 (暂无评论,66人围观)参与讨论

还没有评论,来说两句吧...

Top