【40000字】最适合新手的Springboot+Vue项目2

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 Tair(兼容Redis),内存型 2GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 【40000字】最适合新手的Springboot+Vue项目2

Cookie、Token和Session区别

这里写一篇文章


hello,我是索奇~


精心写了一篇Cookie、Session和Token的 vivid 文章,并分享给大家


我们可以把Cookie、Token和Session看作是三个好基友,它们都是用来跟踪用户的身份和状态的,但是它们之间有一些区别和使用场景。


Cookie


Cookie:曲奇饼,小甜饼;……样的人;(浏览网页后存储在计算机的)缓存文件;<苏格兰>淡面包;漂亮的女孩


啊,不是让你翻译~ 是让你介绍计算机中Cookie~(不过也学会了一个单词)


9a3eec3e1c3039f65776a34006ceff20[0].png


3c8831ecf053c5c99d2898e903c56b80[0].png


Cookie就像是你的小秘书,它的主要作用是保存用户的偏好和浏览历史。比如说,你在网上买了一件衣服,但是还没决定是否买下,这时候你可以把这件衣服放进购物车,Cookie就会帮你记住这个购物车里有什么。等到你下次再来这个网站的时候,Cookie就会帮你把购物车里的东西显示出来,让你可以继续购物。


Cookie的数据存储在客户端的浏览器中,不会占用服务器的资源


在浏览器的控制台中,可以直接输入:document.Cookie来查看Cookie。Cookie是一个由键值对构成的字符串,出于安全考虑


httponly类型的获取不到的,不要找半天httponly发现找不到


又多一个名词,再探索一下?


httponly又是什么啊?


HttpOnly就是一个设置在HTTP响应头中的标志,它可以防止某些类型的客户端脚本(如JavaScript)访问cookie。当服务器向客户端发送带有HttpOnly标志的cookie时,客户端的JavaScript代码将无法通过document.cookie访问该cookie,这可以有效地提高Web应用程序的安全性。


如果给某个 cookie 设置了 httponly 属性,则无法通过 JS 脚本


读取到该 cookie 的信息,但还是能通过Application 中手动修改 cookie, 所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全


Cookie主要用于跟踪用户的偏好和行为,以便提供个性化的体验。例如,在网站上保存用户的登录状态、购物车信息等。


啊,平时刷视频、逛tb、个性化广告等等的信息居然就是这样被页面记录、推送的


30927c3248a5f08361d24abb697786d2[0].jpeg


还有一个大家都在讨论(众说纷纭)的话题就是-我们平时的浏览记录等信息会被记录吗?


答案是不确定(不保证一定不被记录,不保证一定被记录)


Cookie本身是存储在客户端的,而不是服务器端,所以服务器不需要把


Cookie记录保存到数据库中


但至于记录个人的爱好、浏览记录等信息是否被记录到数据库,如何被记录到数据库,这取决于具体的软件、网站、隐私政策和数据收集方式..


Session


Session就像是你的个人档案,它的主要作用是保存用户的状态和权限。比如说,你在网站上登录之后,服务器就会为你创建一个Session,里面保存了你的登录状态和购物车信息等等。这样,当你在浏览网站的时候,服务器就会根据Session来提供个性化的体验,比如显示你的购物车里有什么,或者显示你最近浏览过的商品。


也可以理解为是一个比较特殊的map ,除了可以像其它map一样存取数据,它还有过期时间、唯一的id区分不同session,


创建该session时,会同时创建一个Cookie,Cookie的key为JSESSIONID,而Cookie的value是该session的id。


又遇到不懂的了吗?Cookie的key是啥东西?


JSESSIONID是一种用于在客户端和服务器端之间传递会话信息的Cookie名称。当用户在浏览器中访问一个需要登录的网站时,服务器会


在后台创建一个会话,并生成一个唯一的Session ID,将其存储在服务器端的Session中,同时,服务器会将Session ID通过Cookie的方式发送给客户端,通常使用的Cookie名称就是OBSESSION


ea1d4a5634fcaf544eb78780c1c90af5[0].png


Session的数据信息存放在服务器上,Session的数据只能被服务器访问,因此相对来说比较安全,但是需要占用服务器的资源,


Session主要用于跟踪用户的状态和权限,以便提供个性化的体验。例如,你搜索的内容、在网站上保存用户的登录状态、购物车信息等。


对于Session并没有上限,但出于对服务器端的性能考虑,Session内不要存放过多的东西


Token


6f3afec9957f13e340b218bde94849ce[0].jpeg


Token就像是你的身份证,它的主要作用是用于身份验证和授权。比如说,你在使用某个APP的时候,需要登录才能使用一些功能,这时候APP就会颁发给你一个Token(令牌),你需要在每个请求中携带这个Token,服务器会通过验证Token来确定你的身份和权限,以确保你只能访问你有权访问的内容。


5bd797307d5f86ad58b3186d341a76b4[0].jpeg


比如用户已经登录了系统, 我给他发一个token, 里边包含了这个用户的 user id, 下一次这个用户再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来就可以了。


但是这时候感觉和session没区别啊,万一有人伪造做假攻击呢?于是就用算法对数据做了签名,用签名+数据 = token ,签名不知道,也就无法伪造token了


这个token 不保存, 当用户把这个token 给我发过来的时候,我再用同样的算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道用户已经登录过了,并且可以直接取到用户的user id , 如果不相同, 数据部分肯定被人篡改过, 就知道这个人是冒充货,返给它没有认证的信息


0eb01568616ebc93c2f719edd9792879[0].jpeg


Token是一种无状态的身份验证机制,意味着服务器不需要保存Token的状态(这不是大大的减轻了服务器的压力~),前后端分离架构中前端无法直接访问后端的Session。但是,前后端分离架构中依然可以使用Session来存储应用程序的其他状态信息,例如购物车数据等,只是不能用来保存用户的登录状态。


既可以保存在服务器也可以在客户端


Token是一种无状态的身份验证机制,它可以在多个服务器之间共享,而Session则需要在每个服务器上都保存一份。使用Token可以避免Session共享和Session过期等问题,同时也可以降低服务器的负担。


Token 中的数据是明文保存的, 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息


基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。


大多数使用Web API的互联网公司中,它是Tokens多用户下处理认证的最佳方式


被攻击是不是很烦恼! Token通常用于API身份验证等场景,可以有效避免跨站请求伪造(CSRF)等攻击~


拓展一下Token的身份验证过程


用户在客户端进行登录操作,将用户名和密码发送到服务器端。


服务器端通过验证用户名和密码的正确性,生成一个Token,并将Token返回给客户端。


客户端将Token保存在本地,例如在浏览器的Cookie或localStorage中。


客户端在后续的请求中,将Token发送给服务器端进行身份验证。


服务器端接收到请求后,从请求中获取Token,并对Token进行解密和验证。


如果Token验证通过,服务器端将响应请求并返回所需的数据,否则返回身份验证失败的错误信息。


在身份验证过程中,服务器端通常会对Token进行解密、验证签名、检查Token是否过期等操作,以确保Token的有效性和安全性


栩栩如生、通俗易懂~ 重点讲完了!


简单记一些知识


看完了没,啥也没懂?好吧,无奈,简单记一下区别吧,面试时候不能哑口无言吧


29bf79bfd1ff3456819a4eb42cc18362[0].jpeg


Session和Token是在服务器端保存数据的机制,而Cookie是在客户端保存数据的机制


通常情况单个Cookie保存的数据在4KB以内(面试官:这都知道,给你offer!欣喜若狂的自己:太好了!)


Session和Token通常用于身份验证和状态管理,而Cookie通常用于跟踪用户的偏好和行为


Session和Token通常用于敏感数据的存储和传输,而Cookie通常用于非敏感数据的存储和传输。


Session和Token需要服务器端进行管理和维护,而Cookie可以由客户端自行管理和维护。


Token可以跨域使用,而Session通常只能在同一个域名下使用;Token可以在分布式系统中使用,而Session通常只能在单一服务器上使用。


(可以忽略)写着写着又想要拓展了,哈哈哈,想要探索的伙伴们,一定想要知道单个站点可以存储的Cookie数量,


这里有疑惑?


国际互联网标准是每个网站可以存储的 Cookie 数量不得超过 300 个,具体还是根据不同的浏览器来定,


发现部分博主说单个站点最多保存20个Cookie,这是不合理的,也有近100点赞


网上一连串的信息是复制的,有时我们不能轻易的相信,要学会自己去探索,去验证!不然就误人耳目了


ecb49d8a1c2ec7d4da795e5b8f55076f[0].png

46bba6a28a2be8469525d9a1d339dcd1[0].png



这里是仅仅是为了说明下Cookie的数量,帮助更多伙伴学会探索知识,对原博主没有任何恶意哈


4. 登录相关接口

4.1 登录

登录的信息放到Redis中


接口属性                           值

url                       /user/login

method                         post

请求参数                    username password

返回参数 55494037ae2f90281f35435cc2fec7c2[0].png



controller

/**
      *
      * @param user
      * SpringMVC默认情况下,请求体中的数据是以 JSON 格式传输的。如果你不使用 @RequestBody 注解,
      * SpringMVC 在处理请求时就不会将请求体中的数据解析成 JSON 对象,也就无法将请求体中的数据转化为 User 对象。
      * @return
      */
     @PostMapping("/login")
     public Result<Map<String,Object>> login(@RequestBody User user){
         // 根据用户名和密码在数据库中遍历,如果存在表示信息正确,具体的登录逻辑在UserServiceImpl业务层中实现
         Map<String,Object> data = userService.login(user);
         if (data!= null){
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20002,"用户名和密码错误");
     }


service


@Autowired
 private RedisTemplate redisTemplate;
     /**
      * @param user
      * @return
      * @description 根据用户名和密码查询
      */
     @Override
     public Map<String, Object> login(User user) {
         // 结果不为空,生成token,为空,则将信息写入Redis
         LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(User::getUsername, user.getUsername());
         wrapper.eq(User::getPassword, user.getPassword());
         User loginUser = this.baseMapper.selectOne(wrapper);
         if (loginUser != null) {
             // 简单项目用UUID,可以改为更好的jwt方案
             String key = "user:" + UUID.randomUUID();
             // 存入redis
             // 防止密码存入redis中
             loginUser.setPassword(null);
             /*
             redisTemplate.opsForValue()是RedisTemplate提供的一个操作字符串类型数据的方法
             它返回一个ValueOperations对象,
             可以用来对Redis中的字符串类型数据进行操作可以使用Redis中的set、get、delete等操作字符串类型数据的命令。
              */
             redisTemplate.opsForValue().set(key, loginUser, 30, TimeUnit.MINUTES);
             //返回数据
             Map<String, Object> data = new HashMap<>();
             data.put("token", key);
             return data;
         }
         return null;
     }
 }


测试的时候用post方法,由于浏览器发送的是get请求,会报错


Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported]

我们需要用postman这个工具进行post请求


9397b86555b7efe07486c02b5f6ac8f7[0].png


0a0fbf3a7f72e99a62311fe7891d8b59[0].png


整合redis

整合redis需要用启动redis服务


这里只是简单的redis功能,具体看其它项目实现,也可以系统的学习redis




pom


<!-- redis -->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

yml


spring:
   redis:
     host: localhost
     port: 6379

配置类


代码中的StringRedisSerializer 是 RedisTemplate 默认的 key 和 value 的序列化器。在使用 StringRedisSerializer 进行序列化时,它会将字符串对象转换为字节数组,并将其存储到 Redis 中。在读取数据时,它会将字节数组反序列化为字符串对象。


Jackson2JsonRedisSerializer可以将 Java 对象序列化为 JSON 格式的字符串,并将其存储到 Redis 中。在读取数据时,它可以将 JSON 格式的字符串反序列化为 Java 对象;


@Configuration
 public class MyRedisConfig {
     //用于创建Redis连接的接口,不需要关心底层 Redis 连接实现的细节,就直接可以使用 Redis 进行数据存储和缓存。
     @Resource
     private RedisConnectionFactory factory;
     @Bean
     public RedisTemplate redisTemplate(){
         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
          //改变序列化器
         redisTemplate.setKeySerializer(new StringRedisSerializer());
         //设置为RedisTemplate的连接工厂可以让这个对象与Redis服务器进行交互
         redisTemplate.setConnectionFactory(connectionFactory);
         // 类型不确定所以用Object
         Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
         redisTemplate.setValueSerializer(serializer);
         ObjectMapper om = new ObjectMapper();
         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
         om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
         om.setTimeZone(TimeZone.getDefault());
         om.configure(MapperFeature.USE_ANNOTATIONS, false);
         om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
         om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
         om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
         om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
         serializer.setObjectMapper(om);
         return redisTemplate;
     }
 }


5f2230e4b35ecfca4b1ca75aec9797ac[0].png


4.2 获取用户信息

76bac18b6ad7da400baffb11f8dde191[0].png


接口属性                         值

url                       /user/info?token=xxx

method                             get

请求参数                       token

返回参数 ba6fa3f2b3604f838a02eca7819b2f3f[0].png

其中的roles角色表我们在下一节才会讲到


avator(化身):头像的地址


name:登录的用户名


controller


返回值 Result<Map<String,Object>> 在这里写具体的或者?都可以


@GetMapping("/info")
     public Result<Map<String,Object>> getUserInfo(@RequestParam("token") String token){
         // 根据token获取用户信息
         Map<String,Object> data = userService.getUserInfo(token);
         if (data!= null){
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20003,"用户信息无效,请重新登陆");
     }

service


@Override
     public Map<String, Object> getUserInfo(String token) {
         // 根据token获取用户信息,redis
         Object obj = redisTemplate.opsForValue().get(token);
         // 在 redisConfig中已经做了序列化,所以需要用抽象类JSON反序列化取出来,转换成User对象(也可以用其它的实现)
         if(obj!=null){
             User loginUser = JSON.parseObject(JSON.toJSONString(obj), User.class);
             Map<String,Object> data = new HashMap<>();
             data.put("name",loginUser.getUsername());
             data.put("avatar",loginUser.getAvatar());
             List<String> roleList = this.baseMapper.getRoleNameByUserId(loginUser.getId());
             //角色,一个人可能有多个角色
             data.put("roles", roleList);
             return data;
         }
         return null;
     }


视频项目中用的是SQL来进行多表联查


UserMapper


public interface UserMapper extends BaseMapper<User> {
     public List<String> getRoleNameByUserId(Integer userId);
 }

UserMapper.xml


<select id="getRoleNamesByUserId" parameterType="Integer" resultType="String">
     SELECT
     b.role_name
     FROM x_user_role a,x_role b
     WHERE a.`user_id` = #{userId}
     AND a.`role_id` = b.`role_id`
 </select>

用postman测试时候先http://localhost:9999/user/login请求token,复制token新建get请求http://localhost:9999/user/info?token写入token成功证明你没错~


4.3 注销

接口属性                 值

url                         /user/logout

method                        post

请求参数            

返回参数           [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwV5GD4c-1675952251553)(C:\Users\dacai\AppData\Roaming\Typora\typora-user-images\image-20230203171855151.png)]

controller


前面是把token保存到了Redis中,把它清除掉即可


前端设置的token名叫x-token


@PostMapping("/logout")
 public Result<?> logout(@RequestHeader("X-Token") String token){
     userService.logout(token);
     return Result.success("注销成功");
 }
service
 public void logout(String token) {
     redisTemplate.delete(token);
 }

前端中有这一个代码


before: require('./mock/mock-server.js')

前后端对接的时候应该被移除,或者注释掉


这段代码是在Vue项目中使用mock数据的一种方式。mock数据是指在前端开发过程中,模拟后端接口返回的数据,用于前端开发和调试。在这段代码中,require('./mock/mock-server.js')表示引入mock-server.js文件,该文件中定义了mock数据的生成规则和接口拦截规则。在开发环境中,通过这种方式可以使用mock数据来替代后端接口,方便前端开发和测试。在生产环境中,这段代码应该被移除,以避免不必要的性能损耗。


6. 跨域处理

跨域是指在浏览器中,当前网页所在的域名与当前请求所访问的域名不同,即跨域请求。


举个栗子:如果当前网页的URL为https://www.example.com,则同源策略要求发送请求的URL也必https://www.example.com,否则就会被拦截。


Access-Control-Allow-Origin


c8e651171ef61358fe77975387d5c770[0].png


这里设置的全局跨域处理,不建议使用注解局部方式


@Configuration
 public class MyCorsConfig {
 //     当前跨域请求最大有效时长,这里默认1天
 //    private static final long MAX_AGE = 24 * 60 * 60;
     @Bean
     public CorsFilter corsFilter() {
         //1.添加CORS配置信息
         CorsConfiguration config = new CorsConfiguration();
         //1) 允许的域,不要写*,否则Cookie就无法使用了
         //这里填写请求的前端服务器
         config.addAllowedOrigin("http://localhost:8888");
         //2) 是否发送Cookie信息
         config.setAllowCredentials(true);
         //3) 允许的请求方式
         config.addAllowedMethod("*");
 //        config.setMaxAge(MAX_AGE);
         // 4)允许的所有的请求头
         config.addAllowedHeader("*");
         //2.添加映射路径,我们拦截一切请求
         UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
 //        接收的是接收CorsConfiguration类型的参数,
         urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", config);
         //3.返回新的CorsFilter.
         return new CorsFilter(urlBasedCorsConfigurationSource);
     }
 }



CorsFilter源码大致如下


public CorsFilter(CorsConfigurationSource configSource, CorsProcessor processor) {
     Assert.notNull(configSource, "CorsConfigurationSource must not be null");
     Assert.notNull(processor, "CorsProcessor must not be null");
     this.configSource = configSource;
     this.processor = processor;
 }

注意:


如果使用的是 Spring Boot 2.4 及以上版本,您还需要在 application.properties 或 application.yml 文件中添加以下配置项,以允许跨域请求携带 Cookie:


spring:
   mvc:
     cors:
       allow-credentials: true

这个配置项会告诉 Spring Boot 在跨域请求中允许携带 Cookie。


提示:脚手架版本用的是:2.13.2 ,所以建议把element更改为2.13.2版本,避免bug


23cfb2969dc74e288769051e2847c613[0].png


如果分页面是英文,可在main.js下面更改为中文zh-CN


b58fa0db0aeb0324fa2d354d4a0eacbe[0].png


7. 用户管理接口

接口                 说明

查询用户列表         分页查询

新增用户

根据id查询用户

修改用户

删除用户               逻辑删除

7.1 查询用户列表

1.controller


@GetMapping("/list")
 public Result<?> getUserListPage(@RequestParam(value = "username", required = false) String username,
                                  @RequestParam(value = "phone", required = false) String phone,
                                  @RequestParam("pageNo") Long pageNo,
                                  @RequestParam("pageSize") Long pageSize) {
     LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper();
     wrapper.eq(username != null, User::getUsername, username);
     wrapper.eq(phone != null, User::getPhone, phone);
     Page<User> page = new Page<>(pageNo, pageSize);
     userService.page(page, wrapper);
     Map<String, Object> data = new HashMap<>();
     data.put("total", page.getTotal());
     data.put("rows", page.getRecords());
     return Result.success(data);
 }


IService源码

default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper) {
         return this.getBaseMapper().selectPage(page, queryWrapper);
     }

2.分页拦截器配置


复制过来的别忘了把new PaginationInnerInterceptor(DbType.MYSQL) 这里改为我们的MYSQL数据库


4509f21de95b3260923a86ac84bc0cf3[0].png


@Configuration
 public class MpConfig {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
         interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
         return interceptor;
     }
 }

测试


9d6c39a8cffaa490b9082a5272e8f1ca[0].png


对接前后端

更改api文件中进行对接后端


7.2 新增用户

密码不能是明文,需要加密处理,用BCryptPasswordEncoder,涉及登录逻辑改动


 

@PostMapping
     public Result<?> addUser(@RequestBody User user){
         user.setPassword(passwordEncoder.encode(user.getPassword()));
         userService.save(user);
         return Result.success("新增用户成功");
     }

7.3 修改用户

此处不提供密码更新,大家自行扩展,可以去实现前端右上角菜单的个人信息功能


修改展示


注意这里response.data不加括号,它不是方法!这点容易忽略


saveUser() {
       // 触发表单验证
       this.$refs.userFormRef.validate((valid) => {
         if (valid) {
           // 提交给后台
           userApi.saveUser(this.userForm).then(response => {
           // 成功提示
             this.$message({
               message: response.message,
               type: 'success'
             })
             // 关闭对话框
             this.dialogFormVisible = false
             // 刷新表格
             this.getUserList()
           })
         } else {
           console.log('error submit!!')
           return false
         }
       })
     },


saveUser(user) {
     if (user.id == null || user.id === undefined) {
       return this.addUser(user)
     }
     return this.updateUser(user)
   }
   getUserById(id) {
     return request({
       url: `/user/'+${id}`,
       method: 'get',
       data: user
     })
   }

4045261e0a0d07a4e35b9b0f0ac05424[0].png

7.4 删除用户

利用MyBatisPlus做逻辑删除处理(MybatisPlus官网上有配置也可以复制哈)


yml(别忘记重启项目)

mybatis-plus:
   global-config:
     db-config:
       logic-delete-field: delted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
       logic-delete-value: 1 # 逻辑已删除值(默认为 1)
       logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

Controller


@DeleteMapping("/{id}")
     public Result<User> deleteUserById(@PathVariable("id") Integer id) {
         userService.removeById(id);
         return Result.success("删除成功");
     }

我们要学会善于查阅文档,不能局限于笔记、视频中的说明,也要结合别人的理解,善于查阅官方文档,这样才能够进一步的打通自己的任通二脉,找到自己的路~ 加油,未来可期


补充

便于大家快速查阅,这里留存一些整个类的文档


Entity


User

@TableName("x_user")
 public class User implements Serializable {
     private static final long serialVersionUID = 1L;
     //主键字段名为id,主键生成策略为自增长。
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
     private String username;
     private String password;
     private String email;
     private String phone;
     private Integer status;
     private String avatar;
     private Integer deleted;
     public Integer getId() {
         return id;
     }
     public void setId(Integer id) {
         this.id = id;
     }
     public String getUsername() {
         return username;
     }
     public void setUsername(String username) {
         this.username = username;
     }
     public String getPassword() {
         return password;
     }
     public void setPassword(String password) {
         this.password = password;
     }
     public String getEmail() {
         return email;
     }
     public void setEmail(String email) {
         this.email = email;
     }
     public String getPhone() {
         return phone;
     }
     public void setPhone(String phone) {
         this.phone = phone;
     }
     public Integer getStatus() {
         return status;
     }
     public void setStatus(Integer status) {
         this.status = status;
     }
     public String getAvatar() {
         return avatar;
     }
     public void setAvatar(String avatar) {
         this.avatar = avatar;
     }
     public Integer getDeleted() {
         return deleted;
     }
     public void setDeleted(Integer deleted) {
         this.deleted = deleted;
     }
     @Override
     public String toString() {
         return "User{" +
             "id=" + id +
             ", username=" + username +
             ", password=" + password +
             ", email=" + email +
             ", phone=" + phone +
             ", status=" + status +
             ", avatar=" + avatar +
             ", deleted=" + deleted +
         "}";
     }
 }


UserRole

@TableName("x_user_role")
 public class UserRole implements Serializable {
     private static final long serialVersionUID = 1L;
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
     private Integer userId;
     private Integer roleId;
     public Integer getId() {
         return id;
     }
     public void setId(Integer id) {
         this.id = id;
     }
     public Integer getUserId() {
         return userId;
     }
     public void setUserId(Integer userId) {
         this.userId = userId;
     }
     public Integer getRoleId() {
         return roleId;
     }
     public void setRoleId(Integer roleId) {
         this.roleId = roleId;
     }
     @Override
     public String toString() {
         return "UserRole{" +
             "id=" + id +
             ", userId=" + userId +
             ", roleId=" + roleId +
         "}";
     }
 }



Controller


UserController

@RestController
 @RequestMapping("/user")
 public class UserController {
     @Autowired
     private IUserService userService;
     @Autowired
     private PasswordEncoder passwordEncoder;
     @GetMapping("/all")
     public Result<List<User>> getAllUser() {
         List<User> list = userService.list();
         return Result.success(list, "查询成功");
     }
     /**
      * @param user SpringMVC默认情况下,请求体中的数据是以 JSON 格式传输的。如果你不使用 @RequestBody 注解,
      *             SpringMVC 在处理请求时就不会将请求体中的数据解析成 JSON 对象,也就无法将请求体中的数据转化为 User 对象。
      * @return
      */
     @PostMapping("/login")
     public Result<Map<String, Object>> login(@RequestBody User user) {
         // 根据用户名和密码在数据库中遍历,如果存在表示信息正确,具体的登录逻辑在UserServiceImpl业务层中实现
         Map<String, Object> data = userService.login(user);
         if (data != null) {
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20002, "用户名和密码错误");
     }
     /**
      * @param token
      * @return
      * @description 将名为 "token" 的 HTTP 请求参数绑定到方法参数 token 上。
      */
     @GetMapping("/info")
     public Result<Map<String, Object>> getUserInfo(@RequestParam("token") String token) {
         // 根据token获取用户信息
         Map<String, Object> data = userService.getUserInfo(token);
         if (data != null) {
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20003, "用户信息无效,请重新登陆");
     }
     @PostMapping("logout")
     public Result<?> logout(@RequestHeader("X-Token") String token) {
         userService.logout(token);
         return Result.success();
     }
     @GetMapping("/list")
     public Result<Map<String, Object>> getUserList (@RequestParam(value = "username", required = false) String username,
                                                @RequestParam(value = "phone", required = false) String phone,
                                                @RequestParam("pageNo") Long pageNo,
                                                @RequestParam("pageSize") Long pageSize) {
         LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper();
         wrapper.eq(StringUtils.hasLength(username),User::getUsername, username);
         wrapper.eq(StringUtils.hasLength(phone),User::getPhone, phone);
         wrapper.orderByDesc(User::getId);
         Page<User> page = new Page<>(pageNo,pageSize);
         userService.page(page,wrapper);
         Map<String,Object> data = new HashMap<>();
         data.put("total",page.getTotal());
         data.put("rows",page.getRecords());
         return Result.success(data);
     }
     /**
      * 新增用户
      * @return
      */
     @PostMapping
     public Result<?> addUser(@RequestBody User user){
         user.setPassword(passwordEncoder.encode(user.getPassword()));
         userService.save(user);
         return Result.success("新增用户成功");
     }
     @PutMapping
     public Result<?> updateUser(@RequestBody User user){
         user.setPassword(null);
         userService.updateById(user);
         return Result.success("修改用户成功");
     }
     @GetMapping("/{id}")
     public Result<User> getUserById(@PathVariable("id") Integer id) {
         User user = userService.getById(id);
         return Result.success(user);
     }
     @DeleteMapping("/{id}")
     public Result<User> deleteUserById(@PathVariable("id") Integer id) {
         userService.removeById(id);
         return Result.success("删除成功");
     }
 }


Service

IUserService

IUserService


public interface IUserService extends IService<User> {
     Map<String, Object> login(User user);
     Map<String, Object> getUserInfo(String token);
     void logout(String token);
 }

UserServiceImpl

UserServiceImpl


@Service
 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
     @Autowired
     private RedisTemplate redisTemplate;
     @Autowired
     private PasswordEncoder passwordEncoder;
     /**
      * @param user
      * @return
      * @description 根据用户名查询,加密后处理
      */
     @Override
     public Map<String, Object> login(User user) {
         // 结果不为空并且匹配传入的密码,生成token,为空,则将信息写入Redis
         LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(User::getUsername, user.getUsername());
         User loginUser = this.baseMapper.selectOne(wrapper);
         if (loginUser != null && passwordEncoder.matches(user.getPassword(),loginUser.getPassword())) {
             // 简单项目用UUID,可以改为更好的jwt方案
             String key = "user:" + UUID.randomUUID();
             // 存入redis2
             // 防止密码存入redis中
             loginUser.setPassword(null);
 //            redisTemplate.opsForValue()是RedisTemplate提供的一个操作字符串类型数据的方法
 //            它返回一个ValueOperations对象,
 //            可以用来对Redis中的字符串类型数据进行操作可以使用Redis中的set、get、delete等操作字符串类型数据的命令。
             redisTemplate.opsForValue().set(key, loginUser, 30, TimeUnit.MINUTES);
             //返回数据
             Map<String, Object> data = new HashMap<>();
             data.put("token", key);
             return data;
         }
         return null;
     }
 //    /**
 //     * @param user
 //     * @return
 //     * @description 根据用户名和密码查询
 //     */
 //    @Override
 //    public Map<String, Object> login(User user) {
 //        // 结果不为空,生成token,为空,则将信息写入Redis
 //        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
 //        wrapper.eq(User::getUsername, user.getUsername());
 //        wrapper.eq(User::getPassword, user.getPassword());
 //        User loginUser = this.baseMapper.selectOne(wrapper);
 //        if (loginUser != null) {
 //            // 简单项目用UUID,可以改为更好的jwt方案
 //            String key = "user:" + UUID.randomUUID();
 //            // 存入redis2
 //            // 防止密码存入redis中
 //            loginUser.setPassword(null);
 //
 //            /*redisTemplate.opsForValue()是RedisTemplate提供的一个操作字符串类型数据的方法
 //            它返回一个ValueOperations对象,
 //            可以用来对Redis中的字符串类型数据进行操作可以使用Redis中的set、get、delete等操作字符串类型数据的命令。*/
 //
 //            redisTemplate.opsForValue().set(key, loginUser, 30, TimeUnit.MINUTES);
 //            //返回数据
 //            Map<String, Object> data = new HashMap<>();
 //            data.put("token", key);
 //            return data;
 //        }
 //
 //        return null;
 //    }
     @Override
     public Map<String, Object> getUserInfo(String token) {
         // 根据token获取用户信息,redis
         Object obj = redisTemplate.opsForValue().get(token);
         // 在 redisConfig中已经做了序列化,所以需要用抽象类JSON反序列化取出来,转换成User对象(也可以用其它的实现)
         if (obj != null) {
             //将一个Java对象转换为JSON字符串,然后再将JSON字符串转换回Java对象可以将数据格式标准化
             // JSON.parseObject第一个参数是要转换的JSON字符串,第二个参数是要转换成的Java对象类型。
             User loginUser = JSON.parseObject(JSON.toJSONString(obj), User.class);
             Map<String, Object> data = new HashMap<>();
             data.put("name", loginUser.getUsername());
             data.put("avatar", loginUser.getAvatar());
             List<String> roleList = this.baseMapper.getRoleNameByUserId(loginUser.getId());
             //角色,一个人可能有多个角色
             data.put("roles", roleList);
             return data;
         }
         return null;
     }
     @Override
     public void logout(String token) {
         redisTemplate.delete(token);
     }
 }


Result

Result

@Data
 @NoArgsConstructor
 @AllArgsConstructor
 public class Result<T> {
     private Integer code;
     private String message;
     private T data;
     public static <T>  Result<T> success(){
         return new Result<>(20000,"success",null);
     }
     public static<T>  Result<T> success(T data){
         return new Result<>(20000,"success",data);
     }
     public static<T>  Result<T> success(T data, String message){
         return new Result<>(20000,message,data);
     }
     public static<T>  Result<T> success(String message){
         return new Result<>(20000,message,null);
     }
     public static<T>  Result<T> fail(){
         return new Result<>(20001,"fail",null);
     }
     public static<T>  Result<T> fail(Integer code){
         return new Result<>(code,"fail",null);
     }
     public static<T>  Result<T> fail(Integer code, String message){
         return new Result<>(code,message,null);
     }
     public static<T>  Result<T> fail( String message){
         return new Result<>(20001,message,null);
     }
 }


config

MpConfig

@Configuration
 public class MpConfig {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
         interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
         return interceptor;
     }
 }

MyCorsConfig

@Configuration
 public class MyCorsConfig {
 //     当前跨域请求最大有效时长,这里默认1天
 //    private static final long MAX_AGE = 24 * 60 * 60;
     @Bean
     public CorsFilter corsFilter() {
         //1.添加CORS配置信息
         CorsConfiguration config = new CorsConfiguration();
         //1) 允许的域,不要写*,否则Cookie就无法使用了
         //这里填写请求的前端服务器
         config.addAllowedOrigin("http://localhost:8888");
         //2) 是否发送Cookie信息
         config.setAllowCredentials(true);
         //3) 允许的请求方式
         config.addAllowedMethod("*");
 //        config.setMaxAge(MAX_AGE);
         // 4)允许的所有的请求头
         config.addAllowedHeader("*");
         //2.添加映射路径,我们拦截一切请求
         UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
 //        接收的是接收CorsConfiguration类型的参数,
         urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", config);
         //3.返回新的CorsFilter.
         return new CorsFilter(urlBasedCorsConfigurationSource);
     }
 }



MyRedisConfig

@Configuration
 public class MyRedisConfig {
     @Resource
     private RedisConnectionFactory factory;
     @Bean
     public RedisTemplate redisTemplate(){
         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
         redisTemplate.setConnectionFactory(factory);
         redisTemplate.setKeySerializer(new StringRedisSerializer());
         Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
         redisTemplate.setValueSerializer(serializer);
         ObjectMapper om = new ObjectMapper();
         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
         om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
         om.setTimeZone(TimeZone.getDefault());
         om.configure(MapperFeature.USE_ANNOTATIONS, false);
         om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
         om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
         om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
         om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
         serializer.setObjectMapper(om);
         return redisTemplate;
     }
 }


yml

yml

server:
   port: 9999
 spring:
   datasource:
     username: root
     password: 123456
     url: jdbc:mysql:///xdb
   redis:
     port: 6379
     host: localhost
 logging:
   level:
     com.suoqi: debug
 mybatis-plus:
   global-config:
     db-config:
       logic-delete-field: delted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
       logic-delete-value: 1 # 逻辑已删除值(默认为 1)
       logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

mapper

UserMapper


public interface UserMapper extends BaseMapper<User> {
     public List<String> getRoleNameByUserId(Integer userId);
 }

xml

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.suoqi.sys.mapper.UserMapper">
     <select id="getRoleNameByUserId" parameterType="Integer" resultType="String">
         SELECT b.role_name
         FROM x_user_role a,
              x_role b
         WHERE a.`user_id` = #{userId}
           AND a.`role_id` = b.`role_id`
     </select>
 </mapper>


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
23天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个具有动态路由和菜单功能的前后端分离应用。首先,创建并配置 Spring Boot 项目,实现后端 API;然后,使用 Ant Design Pro Vue 创建前端项目,配置动态路由和菜单。通过具体案例,展示了如何快速搭建高效、易维护的项目框架。
97 62
|
24天前
|
数据采集 监控 JavaScript
在 Vue 项目中使用预渲染技术
【10月更文挑战第23天】在 Vue 项目中使用预渲染技术是提升 SEO 效果的有效途径之一。通过选择合适的预渲染工具,正确配置和运行预渲染操作,结合其他 SEO 策略,可以实现更好的搜索引擎优化效果。同时,需要不断地监控和优化预渲染效果,以适应不断变化的搜索引擎环境和用户需求。
|
10天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
19天前
|
Java 应用服务中间件
SpringBoot获取项目文件的绝对路径和相对路径
SpringBoot获取项目文件的绝对路径和相对路径
56 1
SpringBoot获取项目文件的绝对路径和相对路径
|
10天前
|
存储 运维 安全
Spring运维之boot项目多环境(yaml 多文件 proerties)及分组管理与开发控制
通过以上措施,可以保证Spring Boot项目的配置管理在专业水准上,并且易于维护和管理,符合搜索引擎收录标准。
21 2
|
14天前
|
分布式计算 关系型数据库 MySQL
SpringBoot项目中mysql字段映射使用JSONObject和JSONArray类型
SpringBoot项目中mysql字段映射使用JSONObject和JSONArray类型 图像处理 光通信 分布式计算 算法语言 信息技术 计算机应用
36 8
|
21天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
38 2
|
21天前
|
JavaScript 前端开发 Java
SpringBoot项目的html页面使用axios进行get post请求
SpringBoot项目的html页面使用axios进行get post请求
40 2
|
21天前
|
前端开发 Java Spring
SpringBoot项目thymeleaf页面支持词条国际化切换
SpringBoot项目thymeleaf页面支持词条国际化切换
51 2
|
21天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
34 1