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

Posted 黑马程序员官方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Security在前端后端分离项目中的使用相关的知识,希望对你有一定的参考价值。

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

1 文章导读

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手摸手,带你搭建前后端分离商城系统03 整合Spring Security token 实现方案,完成主业务登录

在 spring-boot 中如何分离前端和后端机器?

Spring Boot + Shiro 实现前后端分离权限控制

基于Spring Boot架构的前后端完全分离项目API路径问题

建议收藏毕设/私活/大佬必备,一个挣钱的标准开源前后端分离springboot+vue+redis+Spring Security脚手架--若依框架

建议收藏毕设/私活/大佬必备,一个挣钱的标准开源前后端分离springboot+vue+redis+Spring Security脚手架--若依框架