springSecurity-jwt

springSecurity-jwt

简介

springSecurity是spring家族中的一个安全管理框架,相比另一个安全框架shiro,他提供了更丰富的功能,社区资源也比shiro丰富。

一般来说大型的项目都是使用springSeurity来做安全框架。小项目用shiro比较多。相比而言,shiro上手更加简单。

核心功能

  • 认证:检验当前访问系统的是不是本系统的用户,并且要确定是哪个用户。
  • 授权:经过认证后判断当前用户是否有权限进行某个操作。

准备工作

  • 本demo中的常量类
public class Constant {
   
   /**
    * 验证码
    */
   public static final String CAPTCHA_KEY = "captcha";

   /**
    * token前缀
    */
   public static final String TOKEN_PREFIX = "Bearer ";
}
  • 本demo中用到的工具类

springBoot项目中常用的工具类

快速入门

  1. 导入依赖
      <!-- JSON工具类 -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- 阿里JSON解析器 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>2.0.23</version>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <dependency>
            <groupId>com.github.axet</groupId>
            <artifactId>kaptcha</artifactId>
            <version>0.0.9</version>
        </dependency>
        <!-- hutool工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>

        <!--mybatisPlus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3.4</version>
        </dependency>
        <!--mybatis-plus代码生成器-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--velocity模板  暂未使用-->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.3</version>
        </dependency>
        <!--freemarker模板-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--swagger2依赖-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
  1. 配置文件
server:
  port: 8089

spring:
  #mysql配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity_jwt?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
    username: root
    password: 123456
  # redis 配置
  redis:
    # 地址
    host: localhost
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 1
    # 密码
    password:
    # 连接超时时间
    timeout: 10s
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 8
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms

jwt:
  header: Authorization
  expire: 604800
  secret: f4402f4b1c984955a4a22c7c43614ba3


# 配置日志
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      # 配置逻辑删除
      logic-delete-value: 2
      logic-not-delete-value: 0
  mapper-locations: classpath*:/mapper/**/*.xml

认证

大致流程

  1. 认证流程

登录校验流程

  1. SpringSecurity完整流程

    springSecurity的原理其实就是一个过滤器链,内部包含了个各种功能的过滤器。

    下图只展示了核心过滤器,其他的非核心的并没有展示

    UsernamePasswordAuthenticatioFilter:负责处理我们在登录后的用户名和密码的登录请求。入门案例的认证工作主要由它负责。

    ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException.

    FilterSecurityInterceptor:负责权限校验的过滤器。

完整流程

  1. 通过Debug插看当前系统中springSecurity过滤器链中有哪些过滤器及他们的顺序

    debug

  2. 认证流程详解

    Authentication接口:他的实现类,表示当前访问系统的用户,封装了用户的相关信息。

    AuthenticationManager接口:定义了认证Authentication的方法。

    UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

    UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名信息要封装成UserDetails对象返回。然后这些信息封装到Authentication对象中。

    流程详解

思路分析

登入操作

  1. 自定义登录接口,调用ProvideerManager的方法进行认证。把用户信息存到redis中
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;
    
    @PostMapping("/user/login")
    public Result login(@RequestBody SysUser sysUser){
        //登录
        Result result = loginService.login(sysUser);
        return result;
    }
}

LoginServiceImpl

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public Result login(SysUser sysUser) {
        // AuthenticationManager  authenticate进行用户认证
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(sysUser.getUserName(), sysUser.getPassword());
        //该方法会去调用UserDetailsServiceImpl中的loadUserByUsername方法
        Authentication authenticate = authenticationManager.authenticate(token);
        if(ObjectUtils.isEmpty(authenticate)){
            //如果认证没通过,给出对应的提示
            throw new UsernameNotFoundException("用户名或密码错误");
        }

        LoginUser user = (LoginUser)authenticate.getPrincipal();
        //认证通过,使用userId生成jwt
        String userId = user.getSysUser().getUserId().toString();
        String generateToken = jwtUtil.generateToken(userId);
        //把完整的用户信息存入redis, userid作为key
        redisUtil.setCacheObject("login:"+user.getSysUser().getUserId(),user);
        return Result.success(200,"登入成功",generateToken);
    }
}
  1. 自定义LoginUser去实现UserDetails接口
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    //自定义系统用户
    private SysUser sysUser;
    //存储权限信息
    private List<String> permissions;

    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(SysUser sysUser, List<String> permissions) {
        this.sysUser = sysUser;
        this.permissions = permissions;
    }

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(!ObjectUtils.isEmpty(authorities)){
            return authorities;
        }
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    //不对此属性进行序列化
    @JsonIgnore
    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }
	
    @JsonIgnore
    @Override
    public String getUsername() {
        return sysUser.getUserName();
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // false-表示不可用
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  1. 自定义UserDetailsService实现类,在类中去查询数据库
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getUserName, username);
        SysUser user = sysUserMapper.selectOne(wrapper);
        if(ObjectUtils.isEmpty(user)){
            throw new UsernameNotFoundException("用户名或密码错误");
        }

        // 查询对应得权限信息
        return new LoginUser(user,getUserAuthority(user.getUserId()));
    }
}

校验

  1. 定义jwt校验过滤器,获取token,解析token获取其中的userId,从redis中获取用户信息,存到SecurityContextHolder中
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String authorization = request.getHeader(jwtUtil.getHeader());
        String token = null;
        if(StringUtils.isNotEmpty(authorization) && authorization.startsWith(Constant.TOKEN_PREFIX)){
             token = authorization.replace(Constant.TOKEN_PREFIX,"");
        }

        if(StringUtils.isEmpty(token)){
            //放行
            filterChain.doFilter(request,response);
            return;
        }
        Claims claims = jwtUtil.getClaimsByToken(token);
        if(ObjectUtils.isEmpty(claims)){
            throw new JwtException("token异常");
        }
        if(jwtUtil.jwtExpire(claims)){
           throw new JwtException("token过期");
       }
        //从Redis中获取用户信息
        Object object = redisUtil.getCacheObject("login:" + claims.getSubject());
        LoginUser loginUser = JSONObject.parseObject(JSONObject.toJSONString(object), LoginUser.class);
        if(ObjectUtils.isEmpty(loginUser)){
            throw new UsernameNotFoundException("用户未登录");
        }

        //存入SecurityContextHolder
        // 获取权限信息封装到Authentication
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

        //放行
        filterChain.doFilter(request,response);
    }
}

登出操作

  1. 定义一个登出接口,获取SecurityContextHolder中的认证信息,删除Redis中对应的数据
 @PostMapping("/user/logout")
    public Result logout(){
        //登录
        Result result = loginService.logout();
        return result;
    }
  1. LoginService
public interface LoginService {

    Result login(SysUser sysUser);

    Result logout();
}
  1. LoginServiceImpl
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private RedisUtil redisUtil;

 @Override
    public Result logout() {
        //获取SecurityContextHolder中的用户信息
        UsernamePasswordAuthenticationToken authentication =  (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

        // 获取用户信息(userId) 在JwtAuthenticationTokenFilter中存入的
        LoginUser user =  (LoginUser) authentication.getPrincipal();
		//删除redis中的信息
        redisUtil.deleteObject("login:"+user.getSysUser().getUserId());
        return Result.success(200,"退出成功",true);
    }

授权

  1. 授权基本流程

    在springSecurity中,会使用默认的FilterSecurityInterceptor来进行校验,在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

    所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication,然后设置我们的资源所需要的权限即可

  2. 实现

    springSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。

    但是要是用它我们需要先开启相关配置

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    }
    
  3. 查询对应的权限信息

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private SysMenuService sysMenuService;

    @Autowired
    private SysRoleService sysRoleService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getUserName, username);
        SysUser user = sysUserMapper.selectOne(wrapper);
        if(ObjectUtils.isEmpty(user)){
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        // 查询对应得权限信息
        return new LoginUser(user,getUserAuthority(user.getUserId()));
    }

    /**
     * 获取用户权限信息(角色、菜单权限)
     * @param userId
     * @return
     */
    public List<String> getUserAuthority(Long userId){
        //查询角色
        List<SysRole> sysRoles = sysRoleService.selectRoleByUserId(userId);
        //查询权限
        List<String> permsList = sysMenuService.selectPermsByUserId(userId);
        if(ObjectUtils.isNotEmpty(sysRoles)){
            String role;
            for (SysRole sysRole : sysRoles) {
                role = "ROLE_" + sysRole.getRoleKey();
                permsList.add(role);
            }
        }
        return permsList;
    }
}
  1. 根据userId查询权限的sql
<select id="selectPermsByUserId" resultType="java.lang.String">
        select distinct m.perms
        from
            sys_user_role ur
                left join sys_user u on u.user_id = ur.user_id
                LEFT JOIN sys_role r on ur.role_id = r.role_id
                left join sys_role_menu rm on rm.role_id = r.role_id
                left join sys_menu m on m.menu_id = rm.menu_id
        where u.user_id = #{userId}
        and u.status = 0
        and u.del_flag = 0
    </select>
  1. 根据userId查询用户角色的sql
<select id="selectRoleByUserId" resultMap="BaseResultMap">
        select distinct r.*
        from sys_role r
                 left join sys_user_role ur on r.role_id = ur.role_id
                 left join sys_user u on ur.user_id = u.user_id
        where u.user_id = #{userId}
        and u.status = 0
        and u.del_flag = 0
    </select>
  1. 在实现UserDetails接口的LoginUser中,把权限给到getAuthorities()方法并返回。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    //自定义系统用户
    private SysUser sysUser;
    //存储权限信息
    private List<String> permissions;

    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(SysUser sysUser, List<String> permissions) {
        this.sysUser = sysUser;
        this.permissions = permissions;
    }

    @JsonIgnore
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(!ObjectUtils.isEmpty(authorities)){
            return authorities;
        }
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

    //不对此属性进行序列化
    @JsonIgnore
    @Override
    public String getPassword() {
        return sysUser.getPassword();
    }

    @Override
    public String getUsername() {
        return sysUser.getUserName();
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        // false-表示不可用
        return true;
    }
}

从数据库查询权限信息

RBAC模型

  1. 概念:RBAC模型(Role-Based Access Control 基于角色的访问控制)基于角色的权限控制。通过角色关联用户,角色关联权限的方式间接赋予用户权限。
  2. 从图中我们可以看出主要由用户、角色、权限组成

rbac

  1. 在项目中我们可以更加细化,例如拆分成 sys_user、sys_user_role、sys_role、sys_role_menu、sys_menu 五张表
/*
 Navicat Premium Data Transfer

 Source Server         : localhost
 Source Server Type    : MySQL
 Source Server Version : 50731
 Source Host           : localhost:3306
 Source Schema         : springsecurity_jwt

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

 Date: 15/03/2023 18:37:31
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称',
  `parent_id` bigint(20) NULL DEFAULT 0 COMMENT '父菜单ID',
  `order_num` int(4) NULL DEFAULT 0 COMMENT '显示顺序',
  `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '路由地址',
  `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组件路径',
  `query` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '路由参数',
  `is_frame` int(1) NULL DEFAULT 1 COMMENT '是否为外链(0是 1否)',
  `is_cache` int(1) NULL DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)',
  `menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '#' COMMENT '菜单图标',
  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES (1, '查询用户', 0, 1, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:list', '#', '', '2023-03-14 16:28:40', '', NULL, '');
INSERT INTO `sys_menu` VALUES (2, '添加用户', 0, 2, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:add', '#', '', '2023-03-15 14:59:59', '', NULL, '');
INSERT INTO `sys_menu` VALUES (3, '更新用户', 0, 3, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:edit', '#', '', '2023-03-15 15:01:30', '', NULL, '');
INSERT INTO `sys_menu` VALUES (4, '删除用户', 0, 4, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:user:remove', '#', '', '2023-03-15 15:02:17', '', NULL, '');
INSERT INTO `sys_menu` VALUES (5, '测试', 0, 5, '', NULL, NULL, 1, 0, '', '0', '0', 'sys:perms:test', '#', '', '2023-03-15 16:46:06', '', NULL, '');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `role_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色名称',
  `role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色权限字符串',
  `role_sort` int(4) NOT NULL COMMENT '显示顺序',
  `data_scope` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
  `menu_check_strictly` tinyint(1) NULL DEFAULT 1 COMMENT '菜单树选择项是否关联显示',
  `dept_check_strictly` tinyint(1) NULL DEFAULT 1 COMMENT '部门树选择项是否关联显示',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '角色状态(0正常 1停用)',
  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '超级管理员', 'admin', 1, '1', 1, 1, '0', '0', 'admin', '2018-03-16 11:33:00', 'ry', '2018-03-16 11:33:00', '超级管理员');
INSERT INTO `sys_role` VALUES (2, '高级管理员', 'common', 2, '4', 1, 1, '0', '0', 'admin', '2018-03-16 11:33:00', 'admin', '2022-12-09 11:55:40', '普通角色');

-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu`  (
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  `menu_id` bigint(20) NOT NULL COMMENT '菜单ID',
  PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES (1, 1);
INSERT INTO `sys_role_menu` VALUES (1, 2);
INSERT INTO `sys_role_menu` VALUES (1, 3);
INSERT INTO `sys_role_menu` VALUES (2, 4);
INSERT INTO `sys_role_menu` VALUES (2, 5);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
  `dept_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
  `user_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户账号',
  `nick_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户昵称',
  `user_type` varchar(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '00' COMMENT '用户类型(00系统用户)',
  `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户邮箱',
  `phonenumber` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '手机号码',
  `sex` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
  `avatar` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '头像地址',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '密码',
  `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  `login_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '最后登录IP',
  `login_date` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间',
  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (6, NULL, 'admin', 'admin', '00', '123@qq.com', '13212344567', '0', 'vegjri', '$2a$2a$10$9jta9cb.61FuZ2kHMkKL7.8KWoDs7dC0vpFf1WD8Wzt1s3YHv4jzq', '0', '0', '', NULL, '', '2023-03-14 16:01:15', '', '2023-03-14 16:01:15', NULL);
INSERT INTO `sys_user` VALUES (7, NULL, 'user', 'user', '00', '123@qq.com', '13212344567', '0', 'vegjri', '$2a$10$Hh61ee6Vkfj1Mh9tVp7veu6YLAQwwe4N5pv282EbZMs/taYZczs3O', '0', '0', '', NULL, '', '2023-03-15 16:37:47', '', '2023-03-15 16:37:47', NULL);

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `role_id` bigint(20) NOT NULL COMMENT '角色ID',
  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (6, 1);
INSERT INTO `sys_user_role` VALUES (6, 2);
INSERT INTO `sys_user_role` VALUES (7, 2);

SET FOREIGN_KEY_CHECKS = 1;

自定义失败处理

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同的结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道springSecurity的异常处理机制。

在springSecurity中,如果我们在认证或者授权过程中出现了异常被ExceptionTranslationFiter捕获到,在ExceptionTranslationFiter回去判断是认证失败还是授权失败的异常。

如果是认证过程中出现的异常会被封装成AccessDeniedException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法进行处理。

如果我们需要自定义异常处理,只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可

  1. 自定义实现类
    • 认证失败处理
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Result result = new Result();
        result.setCode(HttpStatus.UNAUTHORIZED.value());
        result.setMsg("请先登入");
        result.setData(false);
        //处理异常
        WebUtil.readerString(response, JSON.toJSONString(result));
    }
}
  • 授权失败处理
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Result result = new Result();
        result.setCode(HttpStatus.FORBIDDEN.value());
        result.setMsg("授权失败");
        result.setData(false);
        //处理异常
        WebUtil.readerString(response, JSON.toJSONString(result));
    }
}
  1. 配置给springSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //认证失败处理
    @Autowired
    private AuthenticationEntryPoint authenticationEntryPoint;

    //授权失败处理
    @Autowired
    private AccessDeniedHandler accessDeniedHandler;


    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    /**
     * 将PasswordEncoder注入到容器中
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    /**
     * 用户认证
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
    private static final String[] URL_WHITELIST = {
            "/user/login",
            "/captcha",
            "/register",
            "/favicon.ico"
    };
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
             //允许跨域
            .cors()
            //关闭csrf
            .and().csrf().disable()
            //不通过session获取SecurityContent
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().authorizeRequests()
            //权限配置
            //.antMatchers("/user/test")
            //.hasAuthority("sys:user:add")
            //对于匿名接口,允许匿名访问
            .antMatchers(URL_WHITELIST).anonymous()
            //除上面的所有请求全部需要授权认证
            .anyRequest().authenticated()
            //配置异常处理器
            .and().exceptionHandling()
                //认证异常
                .authenticationEntryPoint(authenticationEntryPoint)
                //授权异常
                .accessDeniedHandler(accessDeniedHandler)
            //添加过滤器
            .and().addFilterBefore(jwtAuthenticationTokenFilter,UsernamePasswordAuthenticationFilter.class);
        
    }
}

跨域

浏览器出于安全考虑,使用XMLHttpRequest对象发起HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是禁止的。同源策略要求源相同才能进行正常通信,即协议、域名、端口号都完全一致。

前后端分离项目,前后端一般是不同源的,所以存在跨域问题。所以需要处理,让前端能进行跨域请求。

  1. 先对springBoot配置,允许跨域请求
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter()
    {
        // 添加映射路径,拦截一切请求
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        // 设置访问源地址
        config.addAllowedOriginPattern("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 有效期 1800秒
        config.setMaxAge(1800L);

        source.registerCorsConfiguration("/**", config);
        // 返回新的CorsFilter
        return new CorsFilter(source);
    }
}
  1. 开启springSecurity的跨域访问
 @Override
protected void configure(HttpSecurity http) throws Exception {
	//允许跨域
    http.cors();
}

权限校验方法

  1. springSecurity内置
  • springSecurity默认是禁用注解的,要开启注解,要在继承WebSecurityConfigurerAdapter的类加@EnableGlobalMethodSecurity注解,并在该类中将AuthenticationManager定义为Bean。
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(
  prePostEnabled = true, 
  securedEnabled = true, 
  jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

prePostEnabled = true 的作用是启用springSecurity的@PreAuthorize以及@PostAuthorize注解。

securedEnabled = true 的作用是启用springSecurity的@Secured注解。

jsr250Enabled = true 的作用是启用springSecurity的@RoleAllowed注解。

  • 此处以prePostEnabled = true为例,在接口中使用注解
@PreAuthorize("hasAuthority('sys:user:list')")
@GetMapping("/list")
public Result getUserList(){
    List<SysUser> list = sysUserService.list();
    return Result.success(200,"success",list);
}
  • @PreAuthorize("hasAuthority()") 有权限
  • @PreAuthorize("hasAnyAuthority()") 可以有多个权限
  • @PreAuthorize("hasRole()") 有角色 ,注意角色名称加上前缀ROLE_
  • @PreAuthorize("hasAnyRole()") 可以有多个角色,注意角色名称加上前缀ROLE_
  1. 自定义

当SpringSecurity提供的注解不满足我们的业务需求时,我们可以自定义。

@Component("ss")
public class ExpressionRoot {

    public boolean hasAuthority(String authority){
        //设置当前用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合是否存在authority
        return permissions.contains(authority);
    }
}

使用

@PreAuthorize("@ss.hasAuthority('sys:perms:test')")
    @GetMapping("/hello")
    public String test(){
        return "springSecurity";
    }

登入成功处理器

登出成功处理器

测试结果

  • 登入

login

  • 登出

logout

  • 添加用户

adduser

posted @ 2023-03-17 17:54  cxf0616  阅读(101)  评论(0编辑  收藏  举报