【Spring Security】| 从0到1编写一个权限认证 | 学会了吗?
目录
- 一. 🦁 认证前的工作
- 1. 添加依赖
- 2. 创建数据库表(数据自行添加)
- 3. 编写用户实体类
- 4. 编写Dao接口
- 5. 在启动类中添加 @MapperScan 注解
- 6. 继续添加各种包
- 二. 🦁 自定义逻辑认证原理—UserDetailsService
- 三. 🦁 数据库认证
- 四. 🦁 密文加密操作
- 五. 🦁自定义表单登录
- 1. 编写自定义页面
- 2. 在Spring Security配置类自定义登录页面
- 3. 配置登录成功跳转处理器
- 4. 配置登录失败跳转处理器
- 5. 编写退出登录跳转处理器
- 6. 编写退出登录跳转配置
- 六. 🦁 关闭csrf防护
- ——————over————————
- 七. 🦁 授权_RBAC
- 八. 🦁 权限表设计
- 九. 🦁 修改认证逻辑,认证成功后给用户授权
- 十. 🦁 设置访问控制的三种方式
- 1. 准备工作
- 2. 配置类设置访问控制
- 3. 自定义访问控制逻辑
- 4. 注解设置访问控制
一. 🦁 认证前的工作
本次操作是基于SpringBoot项目的,使用Mybatis-Plus作为ORM框架,具体创建流程不再一一阐述。
1. 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2. 创建数据库表(数据自行添加)
CREATE TABLE `users` (
`uid` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
`phone` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
PRIMARY KEY (`uid`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
3. 编写用户实体类
@Data
public class Users {
private Integer id;
private String username;
private String password;
private String phone;
}
4. 编写Dao接口
public interface UsersMapper extends BaseMapper<Users> {
}
5. 在启动类中添加 @MapperScan 注解
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.lion.mysecuritydemo.mapper")
public class MysecuritydemoApplication {
public static void main(String[] args) {
SpringApplication.run(MysecuritydemoApplication.class, args);
}
}
6. 继续添加各种包
二. 🦁 自定义逻辑认证原理—UserDetailsService
在项目中,认证逻辑一般是通过自定义实现的,将实现了UserDetailsService
接口的实现类放入Spring容器中,即可实现自定义逻辑认证。
实现
UserDetailsService
接口必须重写loadUserByUsername
方法,该方法定义了具体的认证逻辑,参数 username 是前端传来的用户名,我们需要根据传来的用户名查询到该用户(一般是从数据库查询),并将查询到的用户封装成一个UserDetails对象,该对象是Spring Security提供的用户对象,包含用户名、密码、权限。Spring Security会根据UserDetails对象中的密码和客户端提供密码进行比较。相同则认证通过,不相同则认证失败,详细流程如下图:
三. 🦁 数据库认证
数据库认证是最常用的,我们现在来看看数据库认证应该怎么写?
其实就是按我们上面说的自定义一个MyUserDetailsService类,并且实现UserDetailsService接口,将其放入Spring容器中,如下:
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UsersMapper usersDao;
/**
* 自定义认证逻辑(现在是数据库认证)
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户
QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
Users users = usersDao.selectOne(wrapper);
if (users == null){
return null;
}
// 2. 封装成UserDetails对象
UserDetails userDetails = User.withUsername(users.getUsername())
.password(users.getPassword())
.authorities("admin") //授权操作
.build();
return userDetails;
}
}
四. 🦁 密文加密操作
在实际开发中,为了数据安全性,在数据库中存放密码时不会存放原密码,而是会存放加密后的密码。而用户传入的参数是明文密
码。此时必须使用密码解析器才能将加密密码与明文密码做比对。Spring Security中的密码解析器是 PasswordEncoder 。
Spring Security要求容器中必须有 PasswordEncoder 实例,Spring Security官方推荐的密码解析器是 BCryptPasswordEncoder 。
我们在security配置类中加入如下一个方法即可:
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
五. 🦁自定义表单登录
虽然Spring Security给我们提供了登录页面,但在实际项目中,更多的是使用自己的登录页面。Spring Security也支持用户自定义登
录页面。用法如下:
1. 编写自定义页面
<!doctype html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录</title>
<link href="/css/styles.css" rel="stylesheet" >
</head>
<body>
<div class="htmleaf-container">
<div class="wrapper">
<div class="container">
<h1>Welcome</h1>
<form class="form" action="/login" method="post">
<input type="text" placeholder="用户名" name="username">
<input type="password" placeholder="密码" name="password">
<button type="submit" id="login-button">登录</button>
</form>
</div>
<ul class="bg-bubbles">
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</div>
</body>
</html>
2. 在Spring Security配置类自定义登录页面
在Spring Security配置类里继承WebSecurityConfigurerAdapter类,重写protected void configure(HttpSecurity http) 方法,如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定义表单登录
http.formLogin()
.loginPage("/login.html") // 自定义登录页面
.usernameParameter("username") // 表单中的用户名项
.passwordParameter("password") // 表单中的密码项
.loginProcessingUrl("/login") // 登录路径,表单向该路径提交,提交后自动执行UserDetailsService的方法
.successHandler(new MyLoginSuccessHandler()) // 登录成功后跳转的路径
.failureHandler(new MyLoginFailureHandler()); // 登录失败后跳转的路径
}
这里使用的认证成功和失败跳转的处理方式是编写自定义成功和失败处理器(个人认为这个方法比较常用),因为登录成功后,如果除了跳转页面还需要执行一些自定义代码时,如:统计访问量,推送消息等操作时,可以自定义登录成功处理器。
3. 配置登录成功跳转处理器
public class MyLoginSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 1. 拿到登录用户信息
UserDetails principal = (UserDetails) authentication.getPrincipal();
// 做一些需要的事情
// 2. 重定向回到主页
response.sendRedirect("/main");
}
}
4. 配置登录失败跳转处理器
public class MyLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("记录失败日 志...");
response.sendRedirect("/fail");
}
}
5. 编写退出登录跳转处理器
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("清除一些数据...");
response.sendRedirect("/login.html");
}
}
6. 编写退出登录跳转配置
在系统中一般都有退出登录的操作。退出登录后,Spring Security进行了以下操作:
清除认证状态
销毁HttpSession对象
跳转到登录页面
在Spring Security中,退出登录的写法如下:在security配置类里编写:
// 退出登录配置
http.logout()
.logoutUrl("/logout") // 退出登录路径
// .logoutSuccessUrl("/login.html") // 退出登录后跳转的路径
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.clearAuthentication(true) //清除认证状态,默认为true
.invalidateHttpSession(true); // 销毁HttpSession对象,默认为true
六. 🦁 关闭csrf防护
CSRF:
跨站请求伪造,通过伪造用户请求访问受信任的站点从而进行非法请求访问,是一种攻击手段。 Spring Security
为了防止CSRF攻击,默认开启了CSRF防护,这限制了除了GET请求以外的大多数方法。我们要想正常使用Spring Security需要突破CSRF防护。
我们这里直接关闭csrf防护即可,在security配置类添加如下代码:
http.csrf().disable();
到这里认证工作就全部完成啦,现在来完成授权工作的编写!!!
——————over————————
七. 🦁 授权_RBAC
授权即认证通过后,系统给用户赋予一定的权限,用户只能根据权限访问系统中的某些资源。
Resource-Based Access Control
基于资源的访问控制,即按资源(或权限)进行授权。比如在企业管理系统中,用户必须 具有查询报表权限才可以查询企业运营报
表。逻辑为:
if(主体.hasPermission("查询报表权限")){
查询运营报表
}
这样在系统设计时就已经定义好查询报表的权限标识,即使查询报表所需要的角色变化为总经理和股东也不需要修改授权代码,系统
可扩展性强。该授权方式更加常用。
八. 🦁 权限表设计
用户角色,角色权限都是多对多关系,即一个用户拥有多个角色,一个角色属于多个用户;一个角色拥有多个权限,一个权限属于多
个角色。这种方式需要指定用户有哪些角色,而角色又有哪些权限。
如:
张三拥有总经理的角色,而总经理拥有查询工资、查询报表的权限,这样张三就拥有了查询工资、查询报表的权限。这样管理用户时只需管理少量角色,而管理角色时也只需要管理少量权限即可。
我们在原有的Users表上,再添加角色表和权限表:
// 角色
@Data
public class Role {
private String rid;
private String roleName;
private String roleDesc;
}
// 权限
@Data
public class Permission {
private String pid;
private String permissionName;
private String url;
}
并且在UsersDao接口添加findPermissionByUsername方法。
// 根据用户名查询权限
List<Permission> findPermissionByUsername(String username);
这个方法设计五表查询,需要自定义编写sql语句:
SELECT DISTINCT
permission.pid,permission.permissionName,permission.url
FROM
users
LEFT JOIN users_role ON users_role.uid = users.uid
LEFT JOIN role ON role.rid = users_role.rid
LEFT JOIN role_permission ON role_permission.rid = role.rid
LEFT JOIN permission ON role_permission.pid = permission.pid
WHERE
username = #{username}
九. 🦁 修改认证逻辑,认证成功后给用户授权
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UsersMapper usersDao;
/**
* 自定义认证逻辑(现在是数据库认证)
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 查询用户
QueryWrapper<Users> wrapper = new QueryWrapper<Users>().eq("username", username);
Users users = usersDao.selectOne(wrapper);
if (users == null){
return null;
}
// 2. 查询用户权限
List<Permission> permissions = usersDao.findPermissionByUsername(username);
// 将自定义权限集合转为Security的权限类型集合
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (Permission permission : permissions) {
grantedAuthorities.add(new SimpleGrantedAuthority(permission.getUrl()));
}
// 3. 封装成UserDetails对象
UserDetails userDetails = User.withUsername(users.getUsername())
.password(users.getPassword())
.authorities(grantedAuthorities) //授权操作
.build();
return userDetails;
}
}
十. 🦁 设置访问控制的三种方式
在给用户授权后,我们就可以给系统中的资源设置访问控制,即拥有什么权限才能访问什么资源。
1. 准备工作
编写控制器类,添加控制器方法资源
@RestController
public class MyController {
@GetMapping("/reportform/find")
public String findReportForm() {
return "查询报表";
}
@GetMapping("/salary/find")
public String findSalary() {
return "查询工资";
}
@GetMapping("/staff/find")
public String findStaff() {
return "查询员工";
}
}
2. 配置类设置访问控制
修改Security配置类:
// 权限拦截配置
http.authorizeRequests()
.antMatchers("/login.html").permitAll() //表示任何权限都可以访问
.antMatchers("/reportform/find").hasAnyAuthority("/reportform/find") // 给资源配置需要的权限
.antMatchers("/salary/find").hasAnyAuthority("/salary/find")
.antMatchers("/staff/find").hasAnyAuthority("/staff/find")
.anyRequest().authenticated(); //表示任何请求都需要认证后才能访问
3. 自定义访问控制逻辑
如果资源数量很多,一条条配置需要的权限效率较低。我们可以自定义访问控制逻辑,即访问资源时判断用户是否具有名为该资源
URL的权限。
@Service
public class MyAuthorizationService {
// 自定义访问控制逻辑,返回值为是否可以访问资源
public boolean hasPermission(HttpServletRequest request, Authentication authentication){
// 获取认证的用户
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// 获取登录用户的权限
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 获取请求的URL路径
String uri = request.getRequestURI();
// 将路径封装为权限对象
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(uri);
return authorities.contains(grantedAuthority);
}
}
4. 注解设置访问控制
除了配置类,在SpringSecurity中提供了一些访问控制的注解。这些注解默认都是不可用的,需要开启后使用。
有两个,一个**@Secured**,因为使用麻烦,这里不细说。
我们来说说**@PreAuthorize**。该注解可以在方法执行前判断用户是否具有权限
① 在启动类开启注解使用
在启动类上方添加:
@EnableGlobalMethodSecurity(prePostEnabled = true)
② 在控制器方法上添加注解
@PreAuthorize("hasAnyAuthority('/reportform/find')")
@GetMapping("/reportform/find")
public String findReportForm() {
return "查询报表";
}
到这里,一个权限表就完成啦!!!