博客后台模块

一、后台模块-准备工作

1. 前端工程启动

前端工程下载链接

https://pan.baidu.com/s/1TdFs4TqxlHh4DXyLwYuejQ 
提取码:mfkw

项目sql文件下载链接

链接:https://pan.baidu.com/s/1DQCGN4wISSDlOkqnVWYwxA 
提取码:mfkw

命令行进入keke-vue-admin文件夹

依次执行

npm install
npm run dev

博客后台模块_第1张图片

博客后台模块_第2张图片

博客后台模块_第3张图片博客后台模块_第4张图片   

后台的前端工程启动完毕 

2. 前端工程启动bug解决

这里后台的前端工程启动如果启动失败,大概率是node的版本过高,建议换至14.的版本,我这里用的是node 14.21.3版本

下载链接:

node 14.21.3版本下载

如果下载出现安装失败问题,可以参见我这个博客,里面有详细的解决方案

node重装-解铃还须系铃人-CSDN博客

3. 后台模块准备工作

第一步: 在keke-admin工程的src/main/java目录新建com.keke.BlogAdminApplication类,作为引导类,写入如下

package com.keke;


import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.keke.mapper")
public class BlogAdminApplication {
     public static void main(String[] args) {
          SpringApplication.run(BlogAdminApplication.class,args);
     }
}

第二步: 在keke-admin工程的resources目录新建File,文件名为application.yml文件,写入如下

server:
  port: 8989

spring:
  # 数据库连接信息
  datasource:
    url: jdbc:mysql://localhost:3306/keke_blog?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password:
    driver-class-name: com.mysql.cj.jdbc.Driver

  servlet:
    # 文件上传
    multipart:
      # 单个上传文件的最大允许大小
      max-file-size: 20MB
      # HTTP请求中包含的所有文件的总大小的最大允许值
      max-request-size: 20MB

mybatis-plus:
  #  configuration:
  #    # 日志
  #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 逻辑删除的字段
      logic-delete-field: delFlag
      # 代表已删除的值
      logic-delete-value: 1
      # 代表未删除的值
      logic-not-delete-value: 0
      # 主键自增策略,以mysql数据库为准
      id-type: auto

第三步: 在keke-framework工程的src/main/java/com.keke.domain.entity目录新建Tag类,写入如下

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;

import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 标签(Tag)表实体类
 *
 * @author makejava
 * @since 2023-10-18 10:20:44
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("ke_tag")
public class Tag {
    private Long id;
    //标签名
    private String name;
    
    private Long createBy;
    
    private Date createTime;
    
    private Long updateBy;
    
    private Date updateTime;
    //删除标志(0代表未删除,1代表已删除)
    private Integer delFlag;
    //备注
    private String remark;

}

第四步:在keke-framework工程的src/main/java/com.keke.mapper目录新建TagMapper接口,写入如下

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Tag;


/**
 * 标签(Tag)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
public interface TagMapper extends BaseMapper {

}

第五步: 在keke-framework工程的src/main/java/com.keke.service目录新建TagService接口,写入如下

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Tag;


/**
 * 标签(Tag)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 10:21:06
 */
public interface TagService extends IService {

}

第六步: 在keke-framework工程的src/main/java/com.keke.service目录新建impl.TagServiceImpl类,写入如下

package com.keke.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.Tag;
import com.keke.mapper.TagMapper;
import com.keke.service.TagService;
import org.springframework.stereotype.Service;

/**
 * 标签(Tag)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 10:21:07
 */
@Service("tagService")
public class TagServiceImpl extends ServiceImpl implements TagService {

}

第七步: 在keke-admin工程的src/main/java目录新建com.keke.controller.TagController类,写入如下

package com.keke.controller;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.Tag;
import com.keke.service.TagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/tag")
public class TagController {

     @Autowired
     private TagService tagService;

     @GetMapping("/test")
     public ResponseResult test(){
          List list = tagService.list();
          return ResponseResult.okResult(list);
     }
}

第八步: 由于huanf-framework公共模块里面有security的相关依赖和配置,为了让 '博客后台模块' 在启动时不报错,我们需要把keke-blog工程的security的相关代码提前写道keke-admin工程里面,这些代码我们在huanf-blog工程里面已经学过了,只是简单地在huanf-admin工程里面也弄(复制)一份这样的代码。

在keke-admin工程的src/main/java/com.keke目录新建filter.JwtAuthenticationTokenFilter类,写入如下。不用管下面的代码什么意思,登录功能的时候会学,注意这里的key为login:

package com.keke.filter;

import com.alibaba.fastjson.JSON;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.utils.JwtUtil;
import com.keke.utils.RedisCache;
import com.keke.utils.WebUtils;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;


@Component
//博客前台的登录认证过滤器。OncePerRequestFilter是springsecurity提供的类
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    //RedisCache是我们在keke-framework工程写的工具类,用于操作redis
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //获取请求头中的token值
        String token = request.getHeader("token");
        //判断上面那行有没有拿到token值
        if(!StringUtils.hasText(token)){
            //说明该接口不需要登录,直接放行,不拦截
            filterChain.doFilter(request,response);
            return;
        }
        //JwtUtil是我们在keke-framework工程写的工具类。解析获取的token,把原来的密文解析为原文
        Claims claims = null;
        try {
            claims = JwtUtil.parseJWT(token);
        } catch (Exception e) {
            //当token过期或token被篡改就会进入下面那行的异常处理
            e.printStackTrace();
            //报异常之后,把异常响应给前端,需要重新登录。ResponseResult、AppHttpCodeEnum、WebUtils是我们在keke-framework工程写的类
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }
        String userid = claims.getSubject();

        //在redis中,通过key来获取value,注意key我们是加过前缀的,取的时候也要加上前缀
        LoginUser loginUser = redisCache.getCacheObject("login:" + userid);
        //如果在redis获取不到值,说明登录是过期了,需要重新登录
        if(Objects.isNull(loginUser)){
            ResponseResult result = ResponseResult.errorResult(AppHttpCodeEnum.NEED_LOGIN);
            WebUtils.renderString(response, JSON.toJSONString(result));
            return;
        }

        //把从redis获取到的value,存入到SecurityContextHolder(Security官方提供的类)
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request,response);

    }
}

在keke-admin工程的src/main/java/com.keke目录新建config.SecurityConfig类,写入如下。不用管下面的代码什么意思,登录功能的时候会学

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
//                .antMatchers("/login").anonymous()
//这里新增必须要是登录状态才能访问退出登录的接口,即是认证过的状态
//                .antMatchers("/logout").authenticated()
             //   为方便测试认证过滤器,我们把查询友链的接口设置为需要登录才能访问。然后我们去访问的时候就能测试登录认证功能了
             //   .antMatchers("/link/getAllLink").authenticated()
                //
//                .antMatchers("/user/userInfo").authenticated()
                // 除上面外的所有请求全部不需要认证即可访问
                .anyRequest().permitAll();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);



        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

第九步: 运行keke-admin工程的BlogAdminApplication类。在postman软件,使用GET请求访问如下

博客后台模块_第5张图片

二、后台模块-登录功能

后台跟前台模块,共用一个sys_user表,所以这里的实现方式,和之前我们在前台模块的实现差不多

1. 接口分析

使用SpringSecurity安全框架来实现登录功能,并且实现登录的校验,也就是把数据库的用户表跟页面输入的用户名密码做比较

使用SpringSecurity安全框架来实现登录功能,并且实现登录的校验,也就是把数据库的用户表跟页面输入的用户名密码做比较

请求方式

请求路径

POST

/user/login

请求体:

{
    "userName":"用户名",
    "password":"密码"
}

响应体:

{
    "code": 200,
    "data": {
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0ODBmOThmYmJkNmI0NjM0OWUyZjY2NTM0NGNjZWY2NSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTY0Mzg3NDMxNiwiZXhwIjoxNjQzOTYwNzE2fQ.ldLBUvNIxQCGemkCoMgT_0YsjsWndTg5tqfJb77pabk"
    },
    "msg": "操作成功"
}

2. 思路分析

登录

①自定义登录接口

调用ProviderManager的方法进行认证 如果认证通过生成jwt

把用户信息存入redis中

②自定义UserDetailsService(之前在前台模块的时候写的UserDetailsService)

在这个实现类中去查询数据库

注意配置passwordEncoder为BCryptPasswordEncoder

校验:

①定义Jwt认证过滤器(之前在前台模块的时候写的JwtAuthenticationTokenFilter)

获取token

解析token获取其中的userid

从redis中获取用户信息

存入SecurityContextHolder

3. 相关依赖



	org.springframework.boot
	spring-boot-starter-security




	org.springframework.boot
	spring-boot-starter-data-redis




	com.alibaba
	fastjson
1.2.33




	io.jsonwebtoken
	jjwt
	0.9.0

4.  登录+校验的代码实现

第一步: 在keke-framework工程的service目录新建SystemLoginService接口,写入如下

package com.keke.service;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;

public interface SystemLoginService {
     ResponseResult login(User user);
}

第二步: 在keke-framework工程的service目录新建impl.SystemLoginServiceImpl类,写入如下

package com.keke.service.impl;

import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.service.BlogLoginService;
import com.keke.service.SystemLoginService;
import com.keke.utils.JwtUtil;
import com.keke.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;


@Service
public class SystemLoginServiceImpl implements SystemLoginService {

     @Autowired
     private AuthenticationManager authenticationManager;

     @Autowired
     private RedisCache redisCache;

     @Override
     public ResponseResult login(User user) {
          UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
          Authentication authenticate = authenticationManager.authenticate(authenticationToken);
          //authenticationManager会默认调用UserDetailsService从内存中进行用户认证,我们实际需求是从数据库,因此我们要重新创建一个UserDetailsService的实现类
          //判断是否认证通过
          if(Objects.isNull(authenticate)){
               throw new RuntimeException("用户名或者密码错误");
          }
          //获取Userid,生成token
          LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
          String userId = loginUser.getUser().getId().toString();
          String jwt = JwtUtil.createJWT(userId);
          //把用户信息存入redis
          redisCache.setCacheObject("login:" + userId,loginUser);
          //把token和userInfo封装返回,因为响应回去的data有这两个属性,所以要封装Vo
          Map systemLoginVo = new HashMap<>();
          systemLoginVo.put("token",jwt);
          return ResponseResult.okResult(systemLoginVo);
     }
}

第三步: 把keke-admin工程的SecurityConfig类修改为如下,为了测试校验功能,我们把接口设置为只有通过登录校验才能访问,注意要放行登录接口,其他接口均需认证

package com.keke.config;

import com.keke.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
//WebSecurityConfigurerAdapter是Security官方提供的类
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //注入我们在keke-blog工程写的JwtAuthenticationTokenFilter过滤器
    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    AuthenticationEntryPoint authenticationEntryPoint;

    @Autowired
    AccessDeniedHandler accessDeniedHandler;



    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    //把官方的PasswordEncoder密码加密方式替换成BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                 //对于后台登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                //其他接口均需要认证
                .anyRequest().authenticated();

        //配置我们自己写的认证和授权的异常处理
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);



        http.logout().disable();
        //将自定义filter加入security过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //允许跨域
        http.cors();
    }

}

第四步: 在keke-admin工程的controller目录新建LoginController类,写入如下

package com.keke.controller;


import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.User;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.BlogLoginService;
import com.keke.service.SystemLoginService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags = "用户登录相关接口")
public class LoginController {

     @Autowired
     private SystemLoginService systemLoginService;

     @PostMapping("/user/login")
     @KekeSystemLog(businessName = "用户登录")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return systemLoginService.login(user);
     }

}

第五步: 本地打开你的redis,postman

post请求,请求参数如下

{
    "userName":"HFuser",
    "password":"123"
}

博客后台模块_第6张图片

可以看到控制台输出的日志信息

博客后台模块_第7张图片

再拿着token去访问之前写的/test接口,可以看到访问成功

博客后台模块_第8张图片

并且不携带token,接口无法访问

博客后台模块_第9张图片

那这样我们登录校验的功能就实现完毕了,跟前台的实现方式几乎一样

三、后台模块-权限控制

1. 接口分析

接口设计。对应用户只能使用自己的权限所允许使用的功能

请求方式

请求地址

请求头

GET

/getInfo

需要token请求头

响应格式如下。如果用户id为1代表管理员,roles 中只需要有admin,permissions中需要有所有菜单类型为C或者F的,状态为正常的,未被删除的权限

{
	"code":200,
	"data":{
		"permissions":[
			"system:user:list",
            "system:role:list",
			"system:menu:list",
			"system:user:query",
			"system:user:add"
            //此次省略1000字
		],
		"roles":[
			"admin"
		],
		"user":{
			"avatar":"http://r7yxkqloa.bkt.clouddn.com/2022/03/05/75fd15587811443a9a9a771f24da458d.png",
			"email":"[email protected]",
			"id":1,
			"nickName":"sg3334",
			"sex":"1"
		}
	},
	"msg":"操作成功"
}

之前在SpringSecurity的学习中就使用过RBAC权限模型。这里我们就是在RBAC权限模型的基础上去实现这个功能

RBAC权限模型最重要最难的就是设计好下面的5张表,有了5张表之后,就是简单的连表查询了

2. 权限表的字段

博客后台模块_第10张图片

3. 角色表的字段

博客后台模块_第11张图片

4. 用户表的字段

博客后台模块_第12张图片

5. 中间表-角色&用户

博客后台模块_第13张图片

6. 中间表-角色&权限

博客后台模块_第14张图片

7. 代码实现

权限表中权限类型中的M表示目录,目录其实不会进行页面跳转,所以不需要处理

权限控制其实就是Menu表,对应的是后台中的菜单和按钮,如果有这些菜单和按钮的权限,就可以进行相应的操作

第一步: 把keke-framework工程的SystemCanstants类修改为如下,增加了两个判断权限类型的成员变量

package com.keke.constants;


//字面值(代码中的固定值)处理,把字面值都在这里定义成常量
public class SystemConstants {

    /**
     * 文章是草稿
     */
    public static final int ARTICLE_STATUS_DRAFT = 1;

    /**
     * 文章是正常发布状态
     */
    public static final int ARTICLE_STATUS_NORMAL = 0;

    /**
     * 文章列表当前查询页数
     */
    public static final int ARTICLE_STATUS_CURRENT = 1;

    /**
     * 文章列表每页显示的数据条数
     */
    public static final int ARTICLE_STATUS_SIZE = 10;

    /**
     * 分类表的分类状态是正常状态
     */
    public static final String STATUS_NORMAL = "0";

    /**
     * 友联审核通过
     */
    public static final String Link_STATUS_NORMAL = "0";

    /**
     * 评论区的某条评论是根评论
     */
    public static final String COMMENT_ROOT = "-1";

    /**
     * 文章评论
     */
    public static final String ARTICLE_COMMENT = "0";

    /**
     * 友链评论
     */
    public static final String LINK_COMMENT = "1";

    /**
     * redis中的文章浏览量key
     */
    public static final String REDIS_ARTICLE_KEY = "article:viewCount";

    /**
     * 浏览量自增1
     */
    public static final int REDIS_ARTICLE_VIEW_COUNT_INCREMENT = 1;


    /**
     * 菜单权限
     */
    public static final String MENU = "C";

    /**
     * 按钮权限
     */
    public static final String BUTTON = "F";


}

第二步: 在keke-framework工程的domain/vo目录新建AdminUserInfoVo类,写入如下,负责把指定字段返回给前端

package com.keke.domain.vo;


import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AdminUserInfoVo {
     private List permissions;
     private List roles;
     private UserInfoVo user;
}

第三步: 在keke-framework工程的domain/entity目录新建Menu类,写入如下,让mybatisplus去查询我们的sys_menu权限表

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 菜单权限表(Menu)表实体类
 *
 * @author makejava
 * @since 2023-10-18 20:55:24
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_menu")
public class Menu {
    //菜单ID
    private Long id;
    //菜单名称
    private String menuName;
    //父菜单ID
    private Long parentId;
    //显示顺序
    private Integer orderNum;
    //路由地址
    private String path;
    //组件路径
    private String component;
    //是否为外链(0是 1否)
    private Integer isFrame;
    //菜单类型(M目录 C菜单 F按钮)
    private String menuType;
    //菜单状态(0显示 1隐藏)
    private String visible;
    //菜单状态(0正常 1停用)
    private String status;
    //权限标识
    private String perms;
    //菜单图标
    private String icon;
    //创建者
    private Long createBy;
    //创建时间
    private Date createTime;
    //更新者
    private Long updateBy;
    //更新时间
    private Date updateTime;
    //备注
    private String remark;
    
    private String delFlag;

}

第四步: 在keke-framework工程的domain目录新建Role类,写入如下,让mybatisplus去查询我们的sys_role角色表

package com.keke.domain.entity;

import java.util.Date;
import java.io.Serializable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableName;

/**
 * 角色信息表(Role)表实体类
 *
 * @author makejava
 * @since 2023-10-18 21:03:52
 */
@SuppressWarnings("serial")
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_role")
public class Role {
    //角色ID
    private Long id;
    //角色名称
    private String roleName;
    //角色权限字符串
    private String roleKey;
    //显示顺序
    private Integer roleSort;
    //角色状态(0正常 1停用)
    private String status;
    //删除标志(0代表存在 1代表删除)
    private String delFlag;
    //创建者
    private Long createBy;
    //创建时间
    private Date createTime;
    //更新者
    private Long updateBy;
    //更新时间
    private Date updateTime;
    //备注
    private String remark;

}

第五步: 在keke-framework工程的service目录新建RoleService接口,写入如下,用于查询用户的角色信息

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Role;


/**
 * 角色信息表(Role)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 21:04:06
 */
public interface RoleService extends IService {

}

第六步: 在keke-framework工程的service目录新建impl.RoleServiceImpl类,写入如下,是查询用户的角色信息的具体代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.domain.entity.Role;
import com.keke.mapper.RoleMapper;
import com.keke.service.RoleService;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 角色信息表(Role)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 21:04:06
 */
@Service("roleService")
public class RoleServiceImpl extends ServiceImpl implements RoleService {


     //根据用户id查询角色信息
     @Override
     public List selectRoleKeyByUserId(Long userId) {
          //如果userId为1,那么角色权限字符串就只需要返回一个admin
          if(userId==1L){
               List roles = new ArrayList<>();
               roles.add("admin");
               return roles;
          }
          //如果用户id不为1,那么需要根据userId连表查询对应的roleId,然后再去角色表中去查询
          //对应的角色权限字符串
          //这里我们期望RoleMapper中封装一个方法去帮我们实现这个复杂的操作
          RoleMapper roleMapper = getBaseMapper();
          return roleMapper.selectRoleKeyByUserId(userId);
     }
}

第七步: 在keke-framework工程的service目录新建MenuService接口,写入如下,用于查询超级管理员的权限信息

package com.keke.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表服务接口
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuService extends IService {


     List selectPermsByUserId(Long userId);
}

第八步: 在keke-framework工程的service目录新建impl.MenuServiceImpl类,写入如下,是查询用户的权限信息的具体代码

package com.keke.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.keke.constants.SystemConstants;
import com.keke.domain.entity.Menu;
import com.keke.mapper.MenuMapper;
import com.keke.service.MenuService;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 菜单权限表(Menu)表服务实现类
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
@Service("menuService")
public class MenuServiceImpl extends ServiceImpl implements MenuService {



     //根据用户id查询权限关键字
     @Override
     public List selectPermsByUserId(Long userId) {
          //如果用户id为1代表管理员,roles 中只需要有admin,
          // permissions中需要有所有菜单类型为C或者F的,状态为正常的,未被删除的权限
          if(userId==1L) {
               LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();
               lambdaQueryWrapper.in(Menu::getMenuType, SystemConstants.MENU, SystemConstants.BUTTON);
               lambdaQueryWrapper.eq(Menu::getStatus, SystemConstants.STATUS_NORMAL);
               //由于我们的逻辑删除字段已经配置了,所以无需封装lambdaQueryWrapper
               List menuList = list(lambdaQueryWrapper);
               //我们需要的是String类型的集合,这里我们要进行数据的处理,这里采用流的方式
               List permissions = menuList.stream()
                       .map(new Function() {
                            @Override
                            public String apply(Menu menu) {
                                 String perms = menu.getPerms();
                                 return perms;
                            }
                       })
                       .collect(Collectors.toList());
               return permissions;
          }
          //否则返回这个用户所具有的权限
          //这里我们需要进行连表查询,因为我们的用户先和角色关联,然后角色才跟权限关联
          MenuMapper menuMapper = getBaseMapper();
          //我们期望menuMapper中有一个方法可以直接帮我们去实现这个复杂的逻辑,这里直接返回
          return menuMapper.selectPermsByUserId(userId);
     }
}

第九步: 在huanf-framework工程的mapper目录新建MenuMapper接口,写入如下,封装查询非超级管理员的权限信息的具体逻辑

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Menu;

import java.util.List;


/**
 * 菜单权限表(Menu)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 20:55:48
 */
public interface MenuMapper extends BaseMapper {

     //Mapper的实现类对应xml映射文件
     List selectPermsByUserId(Long userId);
}

第十步: 在keke-framework工程的resources目录新建mapper/MenuMapper.xml文件,写入如下,查询非超级管理员的权限信息的具体逻辑,即sql语句




    

第十一步: 在keke-framework工程的mapper目录新建RoleMapper文件,写入如下,用于查询用户的角色信息

package com.keke.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.keke.domain.entity.Role;

import java.util.List;


/**
 * 角色信息表(Role)表数据库访问层
 *
 * @author makejava
 * @since 2023-10-18 21:04:06
 */
public interface RoleMapper extends BaseMapper {


     List selectRoleKeyByUserId(Long userId);
}

第十二步: 在keke-framework工程的resources/mapper目录新建RoleMapper.xml文件,写入如下,是查询用户的角色信息的具体代码,即实现功能的复杂sql语句




    

第十三步: 把keke-admin工程的LoginController类修改为如下,增加了查询角色信息、权限信息的接口

package com.keke.controller;


import com.keke.annotation.KekeSystemLog;
import com.keke.domain.ResponseResult;
import com.keke.domain.entity.LoginUser;
import com.keke.domain.entity.User;
import com.keke.domain.vo.AdminUserInfoVo;
import com.keke.domain.vo.UserInfoVo;
import com.keke.enums.AppHttpCodeEnum;
import com.keke.handler.exception.exception.SystemException;
import com.keke.service.BlogLoginService;
import com.keke.service.MenuService;
import com.keke.service.RoleService;
import com.keke.service.SystemLoginService;
import com.keke.utils.BeanCopyUtils;
import com.keke.utils.SecurityUtils;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@Api(tags = "用户登录相关接口")
public class LoginController {

     @Autowired
     private SystemLoginService systemLoginService;

     @Autowired
     private MenuService menuService;

     @Autowired
     private RoleService roleService;

     @PostMapping("/user/login")
     @KekeSystemLog(businessName = "后台用户登录")
     public ResponseResult login(@RequestBody User user){
          if(!StringUtils.hasText(user.getUserName())){
               //提示必须要传用户名
               throw new SystemException(AppHttpCodeEnum.REQUIRE_USERNAME);
          }
          return systemLoginService.login(user);
     }

     @GetMapping("/getInfo")
     public ResponseResult getInfo(){
          //获取当前登录用户,用我们封装的SecurityUtils
          LoginUser loginUser = SecurityUtils.getLoginUser();
          //根据用户id查询权限信息
          Long userId = loginUser.getUser().getId();
          List permissions = menuService.selectPermsByUserId(userId);
          //根据用户id查询角色信息
          List roles = roleService.selectRoleKeyByUserId(userId);
          //获取userInfo信息
          User user = loginUser.getUser();
          UserInfoVo userInfoVo = BeanCopyUtils.copyBean(user, UserInfoVo.class);
          //创建Vo,封装返回
          AdminUserInfoVo adminUserInfoVo = new AdminUserInfoVo(permissions,roles,userInfoVo);
          return ResponseResult.okResult(adminUserInfoVo);
     }

}

第十四步: 测试本地打开你的redis,postman

首先登录,拿到token

{
    "userName":"sg",
    "password":"1234"
}

博客后台模块_第15张图片

然后我们访问 /getInfo 接口

博客后台模块_第16张图片

至此,后台的权限控制接口就实现了,整体来说还是比较复杂的,但是我们把每一步都分解开来,一步一步的去完成,完善,就可以去实现 

你可能感兴趣的:(KekeBlog,java,spring,intellij-idea,spring,boot,maven)