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 + Shiro 实现前后端分离权限控制
基于Spring Boot架构的前后端完全分离项目API路径问题
建议收藏毕设/私活/大佬必备,一个挣钱的标准开源前后端分离springboot+vue+redis+Spring Security脚手架--若依框架
建议收藏毕设/私活/大佬必备,一个挣钱的标准开源前后端分离springboot+vue+redis+Spring Security脚手架--若依框架