第七节 Vue+SpringSecurity实现前后端分离权限管理

亮子 2022-08-22 09:08:11 10676 0 0 0

1、准备数据库

/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50727
 Source Host           : localhost:3306
 Source Schema         : db_security_demo

 Target Server Type    : MySQL
 Target Server Version : 50727
 File Encoding         : 65001

 Date: 22/08/2022 16:50:39
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_permission
-- ----------------------------
DROP TABLE IF EXISTS `tb_permission`;
CREATE TABLE `tb_permission`  (
  `permission_id` int(11) NOT NULL AUTO_INCREMENT,
  `permission_name` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '权限名',
  `deleted` int(2) NULL DEFAULT 0 COMMENT '删除状态0:未删除1:已删除',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`permission_id`) USING BTREE,
  INDEX `permission_name`(`permission_name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_permission
-- ----------------------------
INSERT INTO `tb_permission` VALUES (1, 'select', 0, '2022-08-22 16:47:53', '2022-08-22 16:47:53');
INSERT INTO `tb_permission` VALUES (2, 'add', 0, '2022-08-22 16:48:02', '2022-08-22 16:48:02');
INSERT INTO `tb_permission` VALUES (3, 'delete', 0, '2022-08-22 16:48:12', '2022-08-22 16:48:12');
INSERT INTO `tb_permission` VALUES (4, 'update', 0, '2022-08-22 16:48:20', '2022-08-22 16:48:20');

-- ----------------------------
-- Table structure for tb_role
-- ----------------------------
DROP TABLE IF EXISTS `tb_role`;
CREATE TABLE `tb_role`  (
  `role_id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色名',
  `deleted` int(2) NULL DEFAULT 0 COMMENT '删除状态0:未删除1:已删除',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`role_id`) USING BTREE,
  INDEX `role_name`(`role_name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_role
-- ----------------------------
INSERT INTO `tb_role` VALUES (1, 'admin', 0, '2022-08-22 16:47:16', '2022-08-22 16:47:16');
INSERT INTO `tb_role` VALUES (2, 'user', 0, '2022-08-22 16:47:23', '2022-08-22 16:47:23');

-- ----------------------------
-- Table structure for tb_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `tb_role_permission`;
CREATE TABLE `tb_role_permission`  (
  `role_permission_id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` int(11) NOT NULL DEFAULT 0 COMMENT '角色ID',
  `permission_id` int(11) NOT NULL DEFAULT 0 COMMENT '权限ID',
  `deleted` int(2) NULL DEFAULT 0 COMMENT '删除状态0:未删除1:已删除',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`role_permission_id`) USING BTREE,
  INDEX `role_id`(`role_id`) USING BTREE,
  INDEX `permission_id`(`permission_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_role_permission
-- ----------------------------
INSERT INTO `tb_role_permission` VALUES (1, 1, 1, 0, '2022-08-22 16:48:55', '2022-08-22 16:48:55');
INSERT INTO `tb_role_permission` VALUES (2, 1, 2, 0, '2022-08-22 16:49:03', '2022-08-22 16:49:03');
INSERT INTO `tb_role_permission` VALUES (3, 1, 3, 0, '2022-08-22 16:49:11', '2022-08-22 16:49:11');
INSERT INTO `tb_role_permission` VALUES (4, 1, 4, 0, '2022-08-22 16:49:18', '2022-08-22 16:49:18');
INSERT INTO `tb_role_permission` VALUES (5, 1, 1, 0, '2022-08-22 16:49:32', '2022-08-22 16:49:32');
INSERT INTO `tb_role_permission` VALUES (6, 2, 4, 0, '2022-08-22 16:49:53', '2022-08-22 16:49:53');

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user`  (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '登录账号',
  `user_pass` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '登录密码',
  `user_mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号',
  `user_sex` tinyint(1) NULL DEFAULT 0 COMMENT '性别,0未知1男2女',
  `deleted` int(2) NULL DEFAULT 0 COMMENT '删除状态0:未删除1:已删除',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`user_id`) USING BTREE,
  INDEX `user_mobile`(`user_mobile`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, 'admin', '$2a$10$VkXygAeexQvVVvFJrl86IusTls.xErakvSMf1rOYjKxDkNoYycvzK', '', 0, 0, '2022-08-22 16:45:45', '2022-08-22 16:45:45');
INSERT INTO `tb_user` VALUES (2, 'andy', '$2a$10$VkXygAeexQvVVvFJrl86IusTls.xErakvSMf1rOYjKxDkNoYycvzK', '', 0, 0, '2022-08-22 16:45:55', '2022-08-22 16:45:55');

-- ----------------------------
-- Table structure for tb_user_role
-- ----------------------------
DROP TABLE IF EXISTS `tb_user_role`;
CREATE TABLE `tb_user_role`  (
  `user_role_id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL DEFAULT 0 COMMENT '用户ID',
  `role_id` int(11) NOT NULL DEFAULT 0 COMMENT '角色ID',
  `deleted` int(2) NULL DEFAULT 0 COMMENT '删除状态0:未删除1:已删除',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
  PRIMARY KEY (`user_role_id`) USING BTREE,
  INDEX `user_id`(`user_id`) USING BTREE,
  INDEX `role_id`(`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of tb_user_role
-- ----------------------------
INSERT INTO `tb_user_role` VALUES (1, 1, 1, 0, '2022-08-22 16:50:09', '2022-08-22 16:50:09');
INSERT INTO `tb_user_role` VALUES (2, 2, 2, 0, '2022-08-22 16:50:23', '2022-08-22 16:50:23');

SET FOREIGN_KEY_CHECKS = 1;

2、添加依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

3、配置文件

# 应用名称
spring.application.name=server-security-demo

# 应用服务 WEB 访问端口
server.port=9090


#下面这些内容是为了让MyBatis映射
#指定Mybatis的Mapper文件
mybatis.mapper-locations=classpath:mappers/*xml
#指定Mybatis的实体目录
mybatis.type-aliases-package=com.shenmazong.demo.pojo

# 数据库驱动:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据源名称
spring.datasource.name=defaultDataSource
# 数据库连接地址
spring.datasource.url=jdbc:mysql://localhost:3306/db_security_demo?autoReconnect= true&useUnicode= true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

# 数据库用户名&密码:
spring.datasource.username=root
spring.datasource.password=123456

3、添加Knife4j

package com.shenmazong.user.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

/**
 * @author 军哥
 * @version 1.0
 * @description: Swagger文档配置类
 * @date 2022/6/11 10:54
 */

@Configuration
@EnableSwagger2
public class Knife4jConfiguration {

    @Bean(value = "defaultApi2")
    public Docket defaultApi2() {
        String groupName="1.0版本";
        Docket docket=new Docket(DocumentationType.OAS_30)
                .apiInfo(new ApiInfoBuilder()
                        .title("用户中心API")
                        .description("# 用户中心相关API的定义以及描述")
                        .termsOfServiceUrl("https://www.shenmazong.com")
                        .contact(new Contact("亮子说编程","https://www.shenmazong.com","3350996729@qq.com"))
                        .version("3.0")
                        .build())
                //分组名称
                .groupName(groupName)
                .select()
                //这里指定Controller扫描包路径
                .apis(RequestHandlerSelectors.basePackage("com.shenmazong.user.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
}

4、文件访问过滤器

一般情况下,我们如果需要自定义权限拦截,则需要涉及到FilterInvocationSecurityMetadataSource这个接口了。

这里有个坑爹的地方。如果用户未登录,但是已经设置了拦截白名单的URL,仍然会进入到权限验证里面来。起初,我以为不会进来,但后来跟踪源代码发现,还是会进来。只是此时的身份是一个匿名用户。其默认的实现为DefaultFilterInvocationSecurityMetadataSource。

spring security的认证和权限流程,大概就是有多个过滤器,一步步调用filter chain。它的身份认证其实是始于访问资源开始。如果一个用户已登录,那么访问受保护的资源,则会校验该用户是否有权限访问。如果没有权限,则会调用权限拒绝的处理器进行处理。如果有权限,则能顺利访问该资源;

一个用户未登录情况下,也即匿名用户,访问受保护的资源时,spring security会首先检查该资源是否需要权限,如果需要权限,然后再检查,该资源是否是白名单里面。如果是白名单,也能正常访问。如果是受保护的资源,则会提示该用户需要登录。

也即,当一个匿名用户,访问受保护的资源时,就会提示该用户需要登录。

package com.shenmazong.user.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;

/**
 * @author 军哥
 * @version 1.0
 * @description: 文件访问过滤器
 * @date 2022/8/29 16:54
 */

@Component
@Slf4j
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    AntPathMatcher pathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
        // 用户请求地址
        String requestUrl = ((FilterInvocation)o).getRequestUrl();
        log.info("url =" + requestUrl);

        // 白名单
        // 放过swagger
        if(pathMatcher.match("/doc.html", requestUrl)) {
            return SecurityConfig.createList("ROLE_OK");
        }
        if(pathMatcher.match("/webjars/**", requestUrl)) {
            return SecurityConfig.createList("ROLE_OK");
        }
        if(pathMatcher.match("/favicon.ico", requestUrl)) {
            return SecurityConfig.createList("ROLE_OK");
        }
        if(pathMatcher.match("/swagger-resources", requestUrl)) {
            return SecurityConfig.createList("ROLE_OK");
        }
        if(pathMatcher.match("/v3/**", requestUrl)) {
            return SecurityConfig.createList("ROLE_OK");
        }
        // 放过登录、注册、验证码
        if(pathMatcher.match("/user/userLogin", requestUrl)) {
            return SecurityConfig.createList("ROLE_OK");
        }


        // 检查token
        String token = ((FilterInvocation) o).getHttpRequest().getHeader("token");
        if(token == null) {
            log.info("ROLE_NO");
            return SecurityConfig.createList("ROLE_NO");
        }

        // 解析token
        log.info("ROLE_OK");
        return SecurityConfig.createList("ROLE_OK");
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

4、角色和权限判断

AccessDecisionManager 顾名思义,访问决策管理器。做出最终的访问控制(授权)决定。

Makes a final access control (authorization) decision

package com.shenmazong.user.security;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * @author 军哥
 * @version 1.0
 * @description: 角色和权限判断
 * @date 2022/8/29 16:37
 */

@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {

        for (ConfigAttribute configAttribute : collection) {
            // 没有登录
            if(authentication instanceof AnonymousAuthenticationToken) {

            }
            else {

            }

            // 验证权限
            if("ROLE_OK".equals(configAttribute.getAttribute())) {
                // 允许访问
                return;
            }
        }

        // 抛出异常
        throw new AccessDeniedException("非法请求");
//        throw new UsernameNotFoundException("用户不存在");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }
}

5、自定义未登录返回信息的bean

package com.shenmazong.user.security;

import com.alibaba.fastjson2.JSON;
import com.shenmazong.user.utils.ResultResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author 军哥
 * @version 1.0
 * @description: 自定义未登录返回信息的bean
 * @date 2022/8/29 17:48
 */

@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        httpServletResponse.setStatus(401);

        PrintWriter writer = httpServletResponse.getWriter();

        ResultResponse failed = ResultResponse.FAILED(401, e.getMessage());
        String json = JSON.toJSONString(failed);

        writer.write(json);
        writer.flush();
        writer.close();
    }
}

6、SpringSecurity配置类

package com.shenmazong.user.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

/**
 * @author 军哥
 * @version 1.0
 * @description: MySecurityConfig
 * @date 2022/8/29 16:25
 */

@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;

    @Autowired
    MyAccessDecisionManager myAccessDecisionManager;

    @Autowired
    MyAuthenticationEntryPoint myAuthenticationEntryPoint;

    /**
     * @description 访问控制
     * @author 军哥
     * @date 2022/8/29 16:30
     * @version 1.0
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O o) {

                        // 设置访问过滤器
                        o.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);

                        // 设置权限过滤器
                        o.setAccessDecisionManager(myAccessDecisionManager);

                        return o;
                    }
                })
                .and().formLogin().permitAll()
                .and().csrf().disable();

        // 设置自定义未登录错误处理
        http.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint);
    }
}

7、测试接口类

package com.shenmazong.user.controller;

import com.shenmazong.user.service.TbUserService;
import com.shenmazong.user.utils.ResultResponse;
import com.shenmazong.user.vo.IdVo;
import com.shenmazong.user.vo.LoginInfoVo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author 军哥
 * @version 1.0
 * @description: IndexController
 * @date 2022/8/29 16:10
 */

@RestController
@Slf4j
@RequestMapping(value = "/user")
@Api(tags = "用户登录测试")
public class IndexController {

    @Autowired
    TbUserService tbUserService;

    @PostMapping(value = "/userLogin")
    public ResultResponse userLogin(@RequestBody LoginInfoVo loginInfoVo) {
        return tbUserService.userLogin(loginInfoVo);
    }

    @ApiOperation(value = "getUser")
    @PostMapping(value = "/getUser")
    public ResultResponse getUser(@RequestBody IdVo idVo) {
        return tbUserService.getUser(idVo);
    }

}

8、接口的实现

package com.shenmazong.user.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shenmazong.user.entity.TbUser;
import com.shenmazong.user.service.TbUserService;
import com.shenmazong.user.mapper.TbUserMapper;
import com.shenmazong.user.utils.ResultResponse;
import com.shenmazong.user.utils.TokenUtils;
import com.shenmazong.user.vo.IdVo;
import com.shenmazong.user.vo.LoginInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service
@Slf4j
public class TbUserServiceImpl extends ServiceImpl<TbUserMapper, TbUser>
    implements TbUserService{

    @Override
    public ResultResponse userLogin(LoginInfoVo loginInfoVo) {
        //--1 检查用户是否存在
        QueryWrapper<TbUser> wrapper = new QueryWrapper<>();
        wrapper.lambda().eq(TbUser::getUserName, loginInfoVo.getUserName()).last("limit 1");

        TbUser one = getOne(wrapper);
        if(one == null) {
            return ResultResponse.FAILED(404, "用户不存在");
        }

        //--2 验证密码
        boolean checkpw = BCrypt.checkpw(loginInfoVo.getUserPass(), one.getUserPass());
        if(!checkpw) {
            return ResultResponse.FAILED(401, "密码不正确");
        }

        //--3 生成token
        String token = TokenUtils.token()
                .setKey("123456")
                .setClaim("userId", "" + one.getUserId())
                .setClaim("userName", one.getUserName())
                .setClaim("userRole", "['admin','user']").makeToken();
        log.info("token="+token);
        one.setAccessToken(token);

        return ResultResponse.SUCCESS(one);
    }

    @Override
    public ResultResponse getUser(IdVo idVo) {

        TbUser tbUser = getById(idVo.getId());
        if(tbUser == null) {
            return ResultResponse.FAILED(404, "用户不存在");
        }

        return ResultResponse.SUCCESS(tbUser);
    }
}

参考文章