Spring Security在前端后端分离项目中的使用

news2024/12/24 3:19:21

Spring Security 是 Spring 家族中的一个安全管理框架,可以和Spring Boot项目很方便的集成。Spring Security框架的两大核心功能:认证授权

认证: 验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户。简单的理解就是登陆操作,如果可以登录成功就说明您是本系统的用户,如不能登录就说明不是本系

统的用户!而且登录成功以后需要记录当前登录用户的信息!

授权:经过认证后判断当前用户是否有权限进行某个操作!

如上图所示就是展示了当前登录用户可以操作的权限:用户管理、角色管理、菜单管理等,并且针对角色管理可以进行新增、修改、删除、导出等权限。

而现在前后端分离开发成为了主流的开发方式,那么在前后端分离开发方式下如何使用Spring Security就是本文章需要重点研究的内容。

2、Spring Security认证功能

2.1 前端分离项目的认证流程

要想了解如果使用Spring Security进行认证,那么就需要先了解一下前后端分离项目中的认证流程,如下所示:

2.2 Spring Security原理初探

要想使用Spring Security框架来实现上述的认证操作,就必须先要了解一个Spring Security框架的工作流程。

2.2.1 过滤器链

Spring Security的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter: 负责处理我们在登陆页面填写了用户名密码后的登陆请求。

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

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

2.2.2 认证流程

Spring Security的认证流程大致如下所示:

概念速查:

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

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

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

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

2.3 认证实现

在前后端分离项目中,前端请求的是我们自己定义的认证接口。因为在认证成功以后就需要针对当前用户生成token,Spring Security中提供的原始认证就无法实现了。在我们自定

义的认证接口中,需要调用Spring Security的API借助于Spring Security实现认证。

2.3.1 思路分析

认证:

1、自定义认证接口

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

② 把用户信息存入redis中

2、自定义UserDetailsService

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

校验

1、定义Jwt认证过滤器

① 获取token

② 解析token获取其中的userid

③ 从redis中获取用户信息

④ 存入SecurityContextHolder

2.3.2 集成Redis

添加依赖

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

添加redis配置

在application.yml文件中添加Redis的相关配置

spring:
  redis:
    host: 127.0.0.1
    port: 6379

2.3.3 集成Mybatis Plus

添加依赖

<!-- 引入mybatis plus的依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.3</version>
</dependency>

<!-- 数据库驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- lombok依赖包 -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

创建数据库表

CREATE TABLE `sys_user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `nick_name` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
  `password` VARCHAR(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `status` CHAR(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
  `email` VARCHAR(64) DEFAULT NULL COMMENT '邮箱',
  `phone_number` VARCHAR(32) DEFAULT NULL COMMENT '手机号',
  `sex` CHAR(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  `avatar` VARCHAR(128) DEFAULT NULL COMMENT '头像',
  `user_type` CHAR(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
  `create_by` BIGINT(20) DEFAULT NULL COMMENT '创建人的用户id',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `update_by` BIGINT(20) DEFAULT NULL COMMENT '更新人',
  `update_time` DATETIME DEFAULT NULL COMMENT '更新时间',
  `del_flag` INT(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'

-- 插入数据
insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578309, 'zhangsan', '张三', '1234', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);

数据库相关配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/security?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 1234
    driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis plus的配置
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: assign_id 

User实体类

@Data
@TableName(value = "sys_user")
public class User {

    @TableId
    private Long id ;                         // 唯一标识
    private String userName ;                // 用户名
    private String nickName ;                // 昵称
    private String password ;                // 密码
    private String status ;                  // 状态 账号状态(0正常 1停用)
    private String email ;                   // 邮箱
    private String phoneNumber ;            // 电话号码
    private String sex ;                     // 性别  用户性别(0男,1女,2未知)
    private String avatar ;                  // 用户头像
    private String userType ;                // 用户类型 (0管理员,1普通用户)
    private Long createBy ;                  // 创建人
    private Date createTime ;                // 创建时间
    private Long updateBy ;                  // 更新人
    private Date updateTime ;                // 更新时间
    private Integer delFlag ;                // 是否删除  (0代表未删除,1代表已删除)
    
}

UserMapper接口

public interface UserMapper extends BaseMapper<User> { }

启动类

@SpringBootApplication
@MapperScan(basePackages = "com.itheima.security.mapper")
public class SecurityApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityApplication.class , args) ;
    }

}

2.3.4 集成Junit

添加依赖

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

编写测试类

@SpringBootTest(classes = SecurityApplication.class)
public class SecurityApplicationTest {

    @Autowired
    private UserMapper userMapper ;

    @Test
    public void findAll() {
        List<User> selectList = userMapper.selectList(new LambdaQueryWrapper<User>());
        selectList.forEach( s -> System.out.println(s) );
    }

}

2.3.5 UserDetailsService

在Spring Security的整个认证流程中会调用会调用UserDetailsService中的loadUserByUsername方法根据用户名称查询用户数据。默认情况下调用的是

InMemoryUserDetailsManager中的方法,该UserDetailsService是从内存中获取用户的数据。现在我们需要从数据库中获取用户的数据,那么此时就需要自定义一个

UserDetailsService来覆盖默认的配置。

UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper ;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根据用户名查询用户数据
        LambdaQueryWrapper<User> lambdaQueryWrapper = Wrappers.<User>lambdaQuery().eq(User::getUserName ,username) ;
        User user = userMapper.selectOne(lambdaQueryWrapper);

        // 如果查询不到数据,说明用户名或者密码错误,直接抛出异常
        if(user == null) {
            throw new RuntimeException("用户名或者密码错误") ;
        }

        // 将查询到的对象转换成Spring Security所需要的UserDetails对象
        return new LoginUser(user);

    }

}

LoginUser

package com.itheima.security.domain;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

// 用来封装数据库查询出来的用户数据
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {

    private User user ;
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

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

    @Override
    public boolean isAccountNonExpired() {          // 账号是否没有过期
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {           // 账号是否没有被锁定
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {      // 账号的凭证是否没有过期
        return true;
    }

    @Override
    public boolean isEnabled() {                    // 账号是否可用
        return true;
    }
}

测试认证

先通过Spring Security提供的默认登录接口进行认证的测试,需要启动Redis。此时控制台会输出如下错误:

报错的原因:默认情况下Spring Security在获取到UserDetailsService返回的用户信息以后,会调用PasswordEncoder中的matches方法进行校验,但是此时在Spring容器中并不

存在任何的PasswordEncoder的对象,因此无法完成校验操作。

解决方案:

① 使用明文认证

要使用明文进行认证,就需要在密码字段值的前面添加{noop}字样!

② 配置加密算法

2.3.6 配置加密算法

一般情况下关于密码在数据库中都是密文存储的,在进行认证的时候都是基于密文进行校验。具体的实现步骤:

1、使用指定的加密算法【BCrypt】对密码进行加密处理,将加密以后的密文存储到数据库中

2、在Spring容器中注入一个PasswordEncoder对象,一般情况下注入的就是:BCryptPasswordEncoder

我们可以定义一个Spring Security的配置类,Spring Security要求这个配置类要继承WebSecurityConfigurerAdapter

@Configuration
public class SpringSecurityConfigurer extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder() ;
    }

}

测试:将数据库的用户密码更改为使用BCryptPasswordEncoder加密以后的密文

@SpringBootTest(classes = SecurityApplication.class)
public class SecurityApplicationTest {

    @Autowired
    private PasswordEncoder passwordEncoder ;

    @Test
    public void testBcrypt() {
        // 加密测试
        String encode = passwordEncoder.encode("1234");
        System.out.println(encode);

        // 校验测试
        boolean matches = passwordEncoder.matches("1234", "$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm");
        System.out.println(matches);
    }
}

2.3.7 登录接口

整体实现思路:

① 接下我们需要自定义登陆接口,然后让Spring Security对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

② 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在Security Config中配置把AuthenticationManager注入容器。

③ 认证成功的话要生成一个jwt,将jwt令牌进行返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,在返回之前,我们需要把用户信息存入redis,可以把用户id

作为key。

拦截规则配置

在SpringSecurityConfigurer中重写configure(HttpSecurity http)方法:

// 配置Spring Security的拦截规则
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
            .csrf().disable()                                                               // 关闭csrf
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)     // 指定session的创建策略,不使用session
            .and()                                                                          // 再次获取到HttpSecurity对象
            .authorizeRequests()                                                            // 进行认证请求的配置
            .antMatchers("/user/login").anonymous()                         				// 对于登录接口,允许匿名访问
            .anyRequest().authenticated();                                                  // 除了上面的请求以外所有的请求全部需要认证
}

Spring容器注册AuthenticationManager

在SpringSecurityConfigurer中重写authenticationManagerBean方法:

登录接口定义

UserController

@RestController
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserService userService ;

    @PostMapping(value = "/login")
    public ResponseResult<Map> login(@RequestBody User user) {
        return userService.login(user) ;
    }

}

ResponseResult

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult<T> {

    private Integer code ;
    private String msg ;
    private T data ;

}

UserService

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private AuthenticationManager authenticationManager ;

    @Autowired
    private RedisTemplate<String , String> redisTemplate ;

    @Override
    public ResponseResult<Map> login(User user) {

        // 创建Authentication对象
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName() , user.getPassword()) ;

        // 调用AuthenticationManager的authenticate方法进行认证
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        if(authentication == null) {
            throw new RuntimeException("用户名或密码错误");
        }

        // 将用户的数据存储到Redis中
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        redisTemplate.boundValueOps("login_user:" + userId).set(JSON.toJSONString(loginUser));

        // 生成JWT令牌并进行返回
        Map<String , String> params = new HashMap<>() ;
        params.put("userId" , userId) ;
        String token = JwtUtils.getToken(params);

        // 构建返回数据
        Map<String , String> result = new HashMap<>();
        result.put("token" , token) ;
        return new ResponseResult<Map>(200 , "操作成功" , result);

    }

}

2.3.8 认证过滤器

当用户在访问我们受保护的资源的时候,就需要校验用户是否已经登录。我们需要自定义一个过滤器进行实现。

过滤器内部的逻辑:

1、获取请求头中的token,对token进行解析

2、取出其中的userid

3、使用userid去redis中获取对应的LoginUser对象。

4、然后封装Authentication对象存入SecurityContextHolder

5、放行

注意:这个过滤器需要将其加入到Spring Security的过滤器链中

认证过滤器:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisTemplate<String , String> redisTemplate ;

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

        // 1、从请求头中获取token,如果请求头中不存在token,直接放行即可!由Spring Security的过滤器进行校验!
        String token = request.getHeader("token");
        if(token == null || "".equals(token)) {
            filterChain.doFilter(request , response);
            return ;
        }

        // 2、对token进行解析,取出其中的userId
        String userId = null ;
        try {
            Claims claims = JwtUtils.getClaims(token);
            userId= claims.get("userId").toString();
        }catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法") ;
        }

        // 3、使用userId从redis中查询对应的LoginUser对象
        String loginUserJson = redisTemplate.boundValueOps("login_user:" + userId).get();
        LoginUser loginUser = JSON.parseObject(loginUserJson, LoginUser.class);
        if(loginUser != null) {
            // 4、然后将查询到的LoginUser对象的相关信息封装到UsernamePasswordAuthenticationToken对象中,然后将该对象存储到Security的上下文对象中
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null , null) ;
            SecurityContextHolder.getContext().setAuthentication(authenticationToken); 
        }
        
        // 5、放行
        filterChain.doFilter(request , response);
    }

}

配置过滤器:

2.3.9 退出登录

我们只需要定义一个退出接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。

UserService添加退出登录接口:

@Override
public ResponseResult logout() {

    // 获取登录的用户信息
    LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    Long userId = loginUser.getUser().getId();

    // 删除Redis中的用户数据
    redisTemplate.delete("login_user:" + userId) ;

    // 返回
    return new ResponseResult(200 , "退出成功" , null) ;

}

3、Spring Security授权功能

3.1 权限系统的作用

权限系统作用:保证系统的安全性

举例:例如一个学校图书馆的管理系统,如果是普通学生登录以后使用借书和还书的功能,不可能让他具有添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账

号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。总结起来就是不同的用户可以使用不同的功能,这就是权限系统要去实现的效果。

权限功能的实现我们不能只依赖前端去根据用户的权限来选择显示哪些菜单、哪些按钮。因为如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功

能操作。所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。

3.2 授权基本流程

在Spring Security中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后

获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限

即可。

3.3 入门案例

3.3.1 资源添加所需权限

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

相关配置。

开启权限配置功能

在启动类上添加@EnableGlobalMethodSecurity(prePostEnabled = true)

方法添加所需权限

不给用户添加任何权限信息进行测试,返回信息为:

{
    "timestamp": "2022-07-04T06:31:47.821+00:00",
    "status": 403,
    "error": "Forbidden",
    "path": "/hello"
}

3.3.2 用户添加所拥有的权限

UserDetailsServiceImpl

在UserDetailsServiceImpl中构建测试的权限数据,并将其设置给LoginUser对象:

LoginUser

LoginUser接收权限数据,并且对getAuthorities方法进行改造,返回Spring Security所需要的权限对象:

JwtAuthenticationTokenFilter

在JWT过滤器中需要从Redis中获取LoginUser对象,在构建UsernamePasswordAuthenticationToken对象的时候,为其设置权限数据:

3.4 从数据库查询权限信息

3.4.1 RBAC权限模型

RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

3.4.2 环境准备

数据库环境准备

权限表(菜单表):

CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
  `path` varchar(200) DEFAULT NULL COMMENT '路由地址',
  `component` varchar(255) DEFAULT NULL COMMENT '组件路径',
  `visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
  `status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
  `perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
  `create_by` bigint(20) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(20) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

# 插入基础数据
insert into security.sys_menu (id, menu_name, path, component, visible, status, perms, icon, create_by, create_time, update_by, update_time, del_flag, remark) values (1543917775762886657, '添加用户', '/user/addUser', 'addUser', '0', '0', 'system:user:add', 'icon-add', 1, '2022-07-04 11:20:57', 1, '2022-07-04 11:20:57', 0, '添加用户按钮');
insert into security.sys_menu (id, menu_name, path, component, visible, status, perms, icon, create_by, create_time, update_by, update_time, del_flag, remark) values (1543918065589379073, '查看用户列表', '/user/userList', 'userList', '0', '0', 'system:user:list', 'icon-list', 1, '2022-07-04 11:22:06', 1, '2022-07-04 11:22:06', 0, '查看用户列表用户按钮');
角色表:
CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(128) DEFAULT NULL,
  `role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
  `status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
  `del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
  `create_by` bigint(200) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_by` bigint(200) DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

# 插入测试数据
insert into security.sys_role (id, name, role_key, status, del_flag, create_by, create_time, update_by, update_time, remark) values (1, '系统管理员', 'admin', '0', 0, 1, '2022-07-04 19:25:06', 1, '2022-07-04 19:25:19', '系统管理员');
insert into security.sys_role (id, name, role_key, status, del_flag, create_by, create_time, update_by, update_time, remark) values (2, '普通用户', 'user', '0', 0, 1, '2022-07-04 19:25:48', 1, '2022-07-04 19:25:52', '普通用户角色');
角色菜单中间表:
CREATE TABLE `sys_role_menu` (
  `role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
  PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

# 插入基础测试数据
insert into security.sys_role_menu (role_id, menu_id) values (1, 1543917775762886657);
insert into security.sys_role_menu (role_id, menu_id) values (1, 1543918065589379073);
insert into security.sys_role_menu (role_id, menu_id) values (2, 1543918065589379073);

用户表:

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
  `nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
  `password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
  `status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
  `email` varchar(64) DEFAULT NULL COMMENT '邮箱',
  `phone_number` varchar(32) DEFAULT NULL COMMENT '手机号',
  `sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
  `avatar` varchar(128) DEFAULT NULL COMMENT '头像',
  `user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
  `create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

# 插入测试数据
insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578309, 'zhangsan', '张三', '$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);
insert into security.sys_user (id, user_name, nick_name, password, status, email, phone_number, sex, avatar, user_type, create_by, create_time, update_by, update_time, del_flag) values (1501123580308578310, 'admin', '系统管理员', '$2a$10$ZqVB18PPA3P/MR9So/i8N.1UvVb.PblNl2sbj6pQJNDCgqiZqNQUm', '0', 'hly@itcast.cn', '1312103105', '0', 'http://www.itcast.cn', '1', 1, '2022-03-08 09:12:06', 1, '2022-03-08 09:12:06', 0);

用户角色中间表:

CREATE TABLE `sys_user_role` (
  `user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
  `role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
  PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

# 插入基础数据
insert into security.sys_user_role (user_id, role_id) values (1501123580308578309, 2);
insert into security.sys_user_role (user_id, role_id) values (1501123580308578310, 1);

SQL测试查询某一个用户所具有的权限:

SELECT distinct m.perms FROM sys_user u
    left join sys_user_role ur on ur.user_id = u.id
    left join sys_role_menu rm on rm.role_id = ur.role_id
    left join sys_menu m on m.id = rm.menu_id
WHERE u.id = 1501123580308578310 ;

Menu实体类

// 菜单表(Menu)实体类
@TableName(value="sys_menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Menu {

    @TableId
    private Long id;
    private String menuName;        // 菜单名
    private String path;            // 路由地址
    private String component;       // 组件路径
    private String visible;         // 菜单状态(0显示 1隐藏)
    private String status;          // 菜单状态(0正常 1停用)
    private String perms;           // 权限标识
    private String icon;            // 菜单图标
    private Long createBy;          // 创建人
    private Date createTime;        // 创建时间
    private Long updateBy;          // 更新人
    private Date updateTime;        // 更新时间
    private Integer delFlag;        // 是否删除(0未删除 1已删除)
    private String remark;          // 备注
    
}

MenuMapper接口

// 操作菜单表的Mapper接口
public interface MenuMapper extends BaseMapper<Menu> {

    // 查询某一个用户的权限信息
    public abstract List<String> findUserMenuById(Long userId) ;

}

application.yml修改

MenuMapper.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.itheima.security.mapper.MenuMapper">

    <select id="findUserMenuById" resultType="java.lang.String">
        SELECT distinct m.perms FROM sys_user u
             left join sys_user_role ur on ur.user_id = u.id
             left join sys_role_menu rm on rm.role_id = ur.role_id
             left join sys_menu m on m.id = rm.menu_id
        WHERE u.id = #{userId} ;
    </select>

</mapper>

3.4.3 UserDetailsService修改

从数据库中查询该用户的真实权限信息:

4、自定义失败处理

4.1 实现思路

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

SpringSecurity的异常处理机制。

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

现的异常。

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

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

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

4.2 代码实现

4.2.1 认证失败处理器

@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录", null);
        String json = JSON.toJSONString(result) ;
        WebUtils.renderString(response,json);
    }

}

4.2.2 授权失败处理器

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足" , null);
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response,json);

    }

}

4.2.3 Spring Security配置处理器

实现步骤:

1、先注入对应的处理器

2、使用HttpSecurity对象的方法去配置

5、跨域处理

5.1 跨域说明

浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。

同源策略要求源相同才能正常进行通信,所谓的源相同指定是:协议、域名、端口号都完全一致。

前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。

所以我们就要处理一下,让前端能进行跨域请求。

5.2 解决方案

5.2.1 Spring Boot项目添加跨域请求配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
      // 设置允许跨域的路径
        registry.addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETE", "PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

5.2.2 Spring Security开启跨域访问支持

由于我们的资源都会收到Spring Security的保护,所以想要跨域访问还要让Spring Security运行跨域访问。

//SpringSecurityConfigurer#configure 允许跨域
http.cors();

6、其他问题说明

6.1 其他权限校验方式

我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。Spring Security还为我们提供了其它方法例如:hasAnyAuthority,hasRole,

hasAnyRole等。

6.1.2 hasAnyAuthority

hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

6.1.3 hasRole

hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

6.1.4 hasAnyRole

hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

6.2 基于配置的权限控制

我们也可以在配置类中使用使用配置的方式对资源进行权限控制。

注意: 如果此时在方法上使用了@PreAuthorize(value = "hasAuthority('system:user:add')")指定了权限信息,那么就需要用于同时拥有两个权限才可以进行访问。

6.3 CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。https://blog.csdn.net/freeking101/article/details/86537087

Spring Security去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携

带或者是伪造的就不允许访问。

我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储在cookie中,并且需要前端代码去把

token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

7、总结

本文章给大家介绍了一下在前后端分离项目中如何使用Spring Security完成认证和授权的相关操作,并且介绍一下如何自定义认证和授权失败的处理器,以及如何解决跨域的相关

问题。大家可以参考本文章实际操作一下,相信大家很快就可以掌握Spring Security在前后端分离项目中的使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/194832.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全网最详细的mybatis plus 条件构造器queryWrapper学习,比如and(),eq(),or(),like(),between(),orderByAsc()等方法以及分页操作

文章目录1. 引言2. 结构关系3. 环境配置3.1 引入jar包3.2 创建数据源3.2 创建User实体类3.4 创建UserMapper类3.5 创建UserService类4. 操作演示5. 注意事项1. 引言 mybatis大家都有使用过&#xff0c;既面向对象又灵活可配。不友好的地方是&#xff0c;会随着使用出现大量xml…

C++之缺省参数以及C++的输入输出

文章目录前言一、缺省参数的定义二、缺省参数的分类1.全缺省参数2.半缺省参数三、缺省参数的注意事项1. 半缺省参数必须从右往左依次来给出&#xff0c;不能间接给缺省值2. 缺省参数不能在函数声明和定义中同时出现3. 缺省值必须是常量或者全局变量4. C语言不支持四、C的输入&a…

考虑实时市场联动的电力零售商鲁棒定价策略(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

JavaEE初阶第三课:网络初识

欢迎来到javaee初阶的第三课&#xff0c;这节课我会带大家来初识网络 荔枝目录1.认识IP和端口1.1 IP1.2端口2.认识协议2.1协议分层&#xff08;TCP/IP协议&#xff09;2.2封装与分用2.3两台主机之间的网络通信流程&#xff08;非同一网段&#xff09;3.其他常见名词 解释3.1客户…

电脑老是蓝屏是什么原因?怎么修复蓝屏

电脑老是蓝屏是什么原因&#xff1f;其实电脑蓝屏的原因是有很多种的&#xff0c;每个地方出问题都会导致电脑蓝屏&#xff0c;所以我们要想知道蓝屏的原因&#xff0c;那么就需要一步步的去检测&#xff0c;去排除掉一些可能&#xff0c;才能得出正确的蓝屏原因的答案。 一.什…

MATLAB 数组计算

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人主页&#xff1a;小嗷犬的个人主页 &#x1f34a;个人网站&#xff1a;小嗷犬的技术小站 &#x1f96d;个人信条&#xff1a;为天地立心&…

Java字典注解的简单实现(AOP)

需求来源 在开发过程中&#xff0c;必不可少会用到字典&#xff0c;例如&#xff0c;数据库字段性别字段可能是这样的&#xff1a;1&#xff1a;男&#xff1b;2&#xff1a;女&#xff0c;在数据存储的时候用1和2&#xff0c;但是在前端展示的时候需要使用男和女&#xff0c;…

有没有人像我一样每次面对sql都很痛苦

今天又操作了一遍sql&#xff0c;这世界上有没有人像我一样&#xff0c;每一次面对mysql都是一次痛苦的经历。 不知道别人怎么想&#xff0c;反正我是这样的。使用mysql从来没有让我快乐过。在数据库里面&#xff0c;最喜欢mongo&#xff0c;当然我也只会mongo。但是每一次使用…

NL-meals、BM3D

常用的高斯滤波或者均值滤波相对都比较简单&#xff0c;即每个窗口的滤波核都是一样的。稍微复杂一些保边滤波如&#xff0c;双边滤波和导向图滤波等。这里介绍几种ffmpeg里面包含的相对比较复杂的滤波算法。计算量不可谓不小。可以通过ffmpeg查看源码实现过程&#xff0c;这里…

RHCE(防火墙)

文章目录一、什么是防火墙二、iptables三、firewalld四、作业一、什么是防火墙 防火墙&#xff1a;防火墙是位于内部网络和外部网络之间的屏障&#xff0c;它按照系统管理员预先定义的规则来控制数据包的进出 防火墙可以分为硬件防火墙和软件防火墙。硬件防火墙是由厂商设计好的…

LongAdder/LongAccumulator类分析

一、LongAdder简介 1.下图是JDK-API文档的的截图信息 我们可以得知这两个类都是1.8开始提供的&#xff0c;并且都具有顶级的并发性。这两类的区别点主要在于LongAdder初始值为0&#xff0c;只能做累加操作&#xff0c;而LongAccumulator可以完成一些复杂的计算&#xff0c;本…

软件设计师考试整理-0-前言

1. 整理初衷 2022年下半年的软考成绩出来了&#xff0c;查了一下&#xff0c;上午58分&#xff0c;下午61分&#xff0c;虽然不高&#xff0c;但是也过了&#xff0c;还是值得开心的&#xff0c;毕竟在最初报考的时候&#xff0c;会的知识点寥寥无几。 先解释下为什么我会报考…

富勒烯C60,131159-39-2,水溶性富勒烯,CARBON C60

产品描述&#xff1a;富勒烯C60与金刚石、石墨是碳的三种同素异形体&#xff0c;富勒烯分子是一种由60个碳原子结合形成的稳定分子&#xff0c;它具有60个顶点和32个面&#xff0c;其中12个为正五边形&#xff0c;20个为正六边形&#xff0c;它形似足球&#xff0c;所以又称为富…

OpenMMLab AI实战营Day2 图像分类

目录 一、图像分类 二、卷积神经网络 三、超越ResNet的图像分类模型 1、神经结构搜索 2、Transformer 3、ConvNext 四、轻量化神经网络 五、Vision Transformer 六、模型学习 七、学习率与优化器调整策略 八、数据增强 一、图像分类 图像分类&#xff1a;识别图像中…

网络骗局丨典型案例分析,大家一起来避雷!

作者&#xff1a;黑蛋因为疫情的困扰&#xff0c;总体经济都不是很好&#xff0c;春节前后&#xff0c;网络诈骗高发期&#xff0c;以下是几种典型案例&#xff0c;一起来看看。比较常见的有以下几种&#xff1a;1、网络购物骗局小红在某平台网购一件产品&#xff0c;几天后&am…

Nacos+Springcloud+mybatis-plus+oracle的整合

NacosSpringcloudmybatis-plusoracle的整合 1、项目结构 2、父类的依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&q…

shell脚本基本使用

目录 1.是什么&#xff1f; 2.写法 3.shell脚本语法 3.1第一个shell脚本 3.2交互式shell脚本 3.3shell脚本的数值计算 3.4test命令 3.5中括号【】判断符 3.6默认变量 4.shell脚本条件判断 4.1 if fi 4.2 if then else 4.3 if elif else 4.4 case 5.shell脚本函数…

Linux 学习笔记 正则、管道、进程与任务定时

一、筛选与搜索 1.1 grep 命令 筛选数据 grep 是 Globally search a regular expression and print 的缩写。意思是全局搜索一个正则表达式&#xff0c;并且打印。 考虑这样的一个名为 a.txt 的文件&#xff1a; 执行 grep apple ./a.txt 即可匹配所有含有 apple 的字符 默认…

IDEA搭建Finchley.SR2版本的SpringCloud父子基础项目-------zuul和SpringConfig

1.Zuul是什么 Zuul包含了对请求的路由和过滤两个最主要的功能&#xff1a; 其中路由功能负责将外部请求转发到具体的微服务实例上&#xff0c;是实现外部访问统一入口的基础而过滤器功能则负责对请求的处理过程进行干预&#xff0c;是实现请求校验、服务聚合等功能的基础. Zu…

4款实用的办公软件,每一款都让你效率翻倍,相见不易

称心的电脑使用体验&#xff0c;总离不开实用的工具&#xff0c;一款高效的工具&#xff0c;能让你的办公体验翻倍。 1、Mem Reduct 这是一款强大到离谱的电脑清理工具&#xff0c;比某安全卫士实用多&#xff0c;没有任何弹屏广告&#xff0c;完全免费使用&#xff0c;内存清理…