目录
-
-
- 0. 简介:
- 1.快速入门
-
- 1.1准备工作
- 1.2引入SpringSecurity
- 2.认证
-
- 2.1登录校验流程
- 2.2原理初探
- 2.2.1SpringSecurity完整流程
- 2.2.2认证流程详解
- 2.3 解决思路
- 2.3.1 思路分析
- 2.3.2 准备工作
- 2.3.3.2 密码加密存储
- 2.3.3.3 登录接口
- 2.3.3.4 认证过滤器
- 2.3.3.5 退出登录
- 3. 授权
-
- 3.0 权限系统的作用
- 3.1 授权基本流程
- 3.2 授权实现
-
- 3.2.1 限制访问资源所需权限
- 3.2.2封装权限信息
- 3.2.3 从数据库查询权限信息
-
- 3.2.3.1 RBAC权限模型
-
0. 简介:
Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。
一般来说中大型的项目都是使用SpringSecurity来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
1.快速入门
1.1准备工作
我们要搭建一个简单的SpringBoot工程
1.2引入SpringSecurity
在springboot项目中使用SpringSecurity我们只需要引入依赖即可实现入门案例。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.7.0</version>
</dependency>
引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。
必须登陆之后才能对接口进行访问。
2.认证
2.1登录校验流程
2.2原理初探
想要知道如何实现自己的登陆流程就必须要先知道入门案例中SpringSecurity的流程。
2.2.1SpringSecurity完整流程
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。(负责授权)
我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。
2.2.2认证流程详解
概念速查:
Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
2.3 解决思路
2.3.1 思路分析
登录
(1)自定义登录接口
调用ProviderManager的方法进行认证,如果认证通过的话就生成jwt
把用户信息存入redis
(2)自定义UserDetailsService
在这个实现列中去查询数据库
校验:
(1)定义jwt认证过滤器
获取token
解析token获取其中的userid
从redis中获取用户信息
存入SecurityContextHoder
2.3.2 准备工作
我们先创建一个表
CREATE TABLE `sys_user` (
`id` bigint(20) UNSIGNED ZEROFILL NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户名',
`nick_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '昵称',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '密码',
`status` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT '0' COMMENT '状态(0正常 1停用)',
`email` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '手机号',
`sex` char(1) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '性别(0男 1女 2未知)',
`avatar` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '头像',
`user_type` char(1) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户类型(0管理员 1普通用户)',
`create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建问的用户id',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`updata_by` bigint(20) NULL DEFAULT NULL COMMENT '更新人',
`updata_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) UNSIGNED ZEROFILL NULL DEFAULT 00000000000 COMMENT '删除日志(0代表未删除,1 代表已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (00000000000000000001, 'admin', '管理员', '{noop}123456', '0', NULL, NULL, NULL, NULL, '0', NULL, '2022-06-12 21:26:35', NULL, '2022-06-12 21:26:40', 00000000000);
SET FOREIGN_KEY_CHECKS = 1;
引入MybatisPlus和mysql驱动的依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
核心代码实现
创建一个实现类UserDetailsService接口,重写其中的方法。更加用户名从数据库中查询用户信息
package security_token.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.apache.catalina.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import security_token.domain.LoginUser;
import security_token.entity.SysUser;
import security_token.mapper.UserMapper;
import java.util.Objects;
@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
//TODO 查询用户信息
LambdaQueryWrapper<SysUser> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getUserName,username);
SysUser sysUser = userMapper.selectOne(queryWrapper);
//如果没有查询到用户信息就抛出异常
if(Objects.isNull(sysUser)){
throw new RuntimeException("用户名或者密码错误");
}
//TODO 查询对应的权限信息
//把数据封装成UserDetails返回
return new LoginUser(sysUser);
}
}
因为UserDetailsService方法的返回值是UserDetails类型,所以定义一个类,实现该接口,把用户信息封装到其中。
package security_token.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import security_token.entity.SysUser;
import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getUserName();
}
//判断用户是否过期 为true代表不过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//代表是否超时
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 代表是否可用
@Override
public boolean isEnabled() {
return true;
}
}
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户密码是明文存储,需要在密码前加{noop}.
2.3.3.2 密码加密存储
实际项目中不会把密码明文存在数据库中
默认使用的是PasswordEncoder要求数据库中的密码格式为{id}password.他会根据id进行判断密码的加密方式。但是我们一般不会采用这种方式。所以需要替换passwordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
@Configuration
public class SecurityConfig extends WebMvcConfigurer {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
测试类测试输入相同两个密码结果给出的密文不同。
@Test
public void TestBc(){
BCryptPasswordEncoder Encoder=new BCryptPasswordEncoder();
//输入密文的方法
String encode = Encoder.encode("123456");
String encode1 = Encoder.encode("123465");
System.out.println(encode);
System.out.println(encode1);
//校验的方法
String encode = Encoder.encode("123456");
System.out.println(Encoder.matches("123456", "$2a$10$BhrLLfznZFONDomNEVbsTuOrSi4K6hTMN6/nBaV.oe2Or0z/c4ZTq"));
}
//运行结果
$2a$10$BhrLLfznZFONDomNEVbsTuOrSi4K6hTMN6/nBaV.oe2Or0z/c4ZTq
$2a$10$P9JyxbNoaW6QARFuDo5kyOj8Q9Po2VRGnqEyC8WGj1ljJKhnmhlCC
2.3.3.3 登录接口
接下来我们需要自定义登录接口,然后让springSecurity对这个接口放行,让用户访问这个接口的时候不需要登录也可以访问。
在接口中我们通过AuthenticationManage的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话生成一个jwt,放入响应中返回。并且让用户下回请求时能通过jwt识别出具体是那个用户,我们需要把用户信息存入redis,可以把用户id作为key。
@Service
public class LoginService {
@Autowired
private RedisCache redisCache;
@Autowired
private AuthenticationManager authenticationManager;
public Results<Map<String, String>> login(SysUser sysUser){
//AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(sysUser.getUserName(),sysUser.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//认证没有通过,给出对应的提示
if (Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
//如果认证通过了,使用userid生成一个jwt,存入Results
LoginUser loginUser=(LoginUser) authenticate.getPrincipal();
String userId = loginUser.getSysUser().getId().toString();
String jwt = JwtUtil.createJWT(userId);
Map<String, String> map = new HashMap<>();
map.put("token",jwt);
//把完整的用户信息存入redis,userid作为id
redisCache.setCacheObject("login:"+userId,loginUser);
return Results.success(ResponseCode.SUCCESS,map);
}
}
2.3.3.4 认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String header = request.getHeader("token");
// StringUtils.hasText()作用
// 如果里面的值为null,""," ",那么返回值为false;否则为true
if (!StringUtils.hasText(header)){
//就是说header里面没有token,放行,并且结束执行后面的代码
filterChain.doFilter(request,response);
return;
}
String userId;
//解析token
try {
Claims claims = JwtUtil.parseJWT(header); userId = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey="login:"+userId;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
//存入securityContextHolder
//TODO 获取权限信息封装到Authenticate中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,
null,null);
SecurityContextHolder securityContextHolder = new SecurityContextHolder();
// 放行
filterChain.doFilter(request,response);
//FilterChain.doFilter(request,response)
}
}
2.3.3.5 退出登录
//注销接口
public Results logout() {
//获取securityHolder中的用户id
UsernamePasswordAuthenticationToken authenticationToken=(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();
Integer userId = loginUser.getSysUser().getId();
//删除redis中用户id
redisCache.deleteObject("login:"+userId);
return new Results(200,"注销成功");
}
3. 授权
3.0 权限系统的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
3.1 授权基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
3.2 授权实现
3.2.1 限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
@EnableGlobalMethodSecurity(prePostEnabled=true) //这里是添加到Securityconfig配置类当中
然后可以使用相应的注解。@PreAuthorize
@RestController
public class Hellotest {
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('test')")
public String hello(){
return "hello";
}
}
3.2.2封装权限信息
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
//存储权限信息
private List<String> permissions;
public LoginUser(SysUser sysUser,List<String> permissions) {
this.sysUser = sysUser;
this.permissions = permissions;
}
//存储SpringSecurity所需要的权限信息的集合
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
3.2.3 从数据库查询权限信息
3.2.3.1 RBAC权限模型
RBAC权限信息(Role—Based Access Control)即:基于角色的权限控制。这是目前对于开发者最易用,通用的开发模型。
-- 左连接where条件只影响右边的表,反之
SELECT DISTINCT sm.`perms` from sys_user_role sur
LEFT JOIN sys_role sr on sur.role_id=sr.id
LEFT JOIN sys_role_menu srm on sur.role_id=srm.role_id
LEFT JOIN sys_menu sm on sm.id=srm.menu_id
WHERE
user_id=2
AND
sr.`status`=0
AND
sm.`status`=0