文章目录
- 核心概念介绍
-
- [1. 认证](#1. 认证)
- [2. 授权](#2. 授权)
-
- 2.1基于角色的访问控制
- [2.2 基于资源的访问控制【推荐】](#2.2 基于资源的访问控制【推荐】)
- 实现原理
- 整合Springboot实现权限控制
-
- [1. 引入依赖](#1. 引入依赖)
- [2. 定义权限配置类](#2. 定义权限配置类)
- [3. 配置处理器](#3. 配置处理器)
- [4. 用户信息提供者](#4. 用户信息提供者)
- [5. 在接口上设置权限](#5. 在接口上设置权限)
核心概念介绍
Spring Security 实现权限控制主要基于两大核心机制:认证(Authentication) 和授权(Authorization)。
1. 认证
即输入账号密码登录,系统判断用户身份是否合法的过程。
2. 授权
授权是在认证后发生的,根据用户权限来控制访问不同资源。
2.1基于角色的访问控制
Role-Based Access Control,即按角色进行授权。

在业务代码中可以这样写:
java
//伪代码
if(主体.hasRole("总经理角色id")){
查询工资;
}
如果扩展,让部门经理也可以查工资,则需要改动代码:
java
//伪代码
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
查询工资;
}
这样看,当授权角色发生变化时,需要修改代码,可扩展性差。
2.2 基于资源的访问控制【推荐】
Resource-Based Access Control,即按资源进行授权

如果这个人有查询工资的权限,就可以查询工资。
伪代码可以是:
java
if(主体.hasPermission("查询工资权限标识")){
查询工资
}
实现原理
spring security的底层是由servlet过滤器实现的。

当一个 HTTP 请求到达时,会依次经过多个Filter,每个过滤器负责特定安全任务。(所有请求都会经过servlet,而在经过servlet之前会先通过过滤链)
整合Springboot实现权限控制
1. 引入依赖
xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<groupId>com.whl.security</groupId>
<artifactId>security-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>security-demo</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!--spring web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.4.1</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger测试-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.1.0</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2. 定义权限配置类
java
@Configuration //配置类
@EnableMethodSecurity
public class WebSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//开启授权保护
http.authorizeHttpRequests(
(authorize) -> authorize
// .requestMatchers("/user/list").hasAuthority("USER_LIST")
// .requestMatchers("/user/add").hasAuthority("USER_ADD")
.requestMatchers("/user/**").hasRole("ADMIN")
//对所有请求开启授权保护
.anyRequest()
//已认证的请求会被自动授权
.authenticated()
);
//自动使用表单授权方式【注释后就没有html的登录页面了,就只有浏览器默认自带的】
http.formLogin(form -> {
form.loginPage("/login").permitAll()
.usernameParameter("myusername")
.passwordParameter("mypassword")
.failureUrl("/login?error")//校验失败时跳转
.successHandler(new MyAuthenticationSuccessHandler())//认证成功时的处理
.failureHandler(new MyAuthenticationFailureHandler())//认证失败时的处理
;
});
http.logout(logout -> {
logout.logoutSuccessHandler(new MyLogoutSuccessHandler()); //注销成功处理
});
http.exceptionHandling(exception -> {
exception.authenticationEntryPoint(new MyAuthenticationEntryPoint()); //请求未认证处理
exception.accessDeniedHandler(new MyAccessDeniedHandler());
});
//会话并发处理
http.sessionManagement(session -> {
session.maximumSessions(1).expiredSessionStrategy(new MySessionInformationExpiredStrategy());
});
//跨域
http.cors(withDefaults());
//关闭CSRF攻击防御
http.csrf(csrf -> csrf.disable());
return http.build();
}
}
3. 配置处理器
- 认证成功时
java
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//获取用户身份信息
Object principal = authentication.getPrincipal();
// //获取用户凭证信息
// Object credentials = authentication.getCredentials();
// //获取用户权限信息
// Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
HashMap result = new HashMap();
result.put("code", 0);
result.put("msg", "登录成功");
result.put("data", principal);
//将对象转为json字符串
String json = JSON.toJSONString(result);
//认证成功后返回json数据
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
- 认证失败时
java
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String localizedMessage = exception.getLocalizedMessage();
HashMap result = new HashMap();
result.put("code", -1);
result.put("msg", localizedMessage);
//将对象转为json字符串
String json = JSON.toJSONString(result);
//认证成功后返回json数据
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
- 注销成功处理
java
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
HashMap result = new HashMap();
result.put("code", 0);//成功
result.put("msg", "注销成功");
//将对象转为json字符串
String json = JSON.toJSONString(result);
//认证成功后返回json数据
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
- 请求未认证处理
java
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
String localizedMessage = "需要登录"; //authException.getLocalizedMessage();
HashMap result = new HashMap();
result.put("code", -1); //失败
result.put("msg", localizedMessage);
//将对象转为json字符串
String json = JSON.toJSONString(result);
//认证成功后返回json数据
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
- 请求未授权时
java
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
HashMap result = new HashMap();
result.put("code", -1); //失败
result.put("msg", "没有权限");
//将对象转为json字符串
String json = JSON.toJSONString(result);
//认证成功后返回json数据
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
- 会话失效时处理
java
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
/**
* 当会话失效时的处理.
*
* @param event
* @throws IOException
* @throws ServletException
*/
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException,
ServletException {
HashMap result = new HashMap();
result.put("code", -1); //失败
result.put("msg", "该账号已从其他设备登录");
//将对象转为json字符串
String json = JSON.toJSONString(result);
//返回响应
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
4. 用户信息提供者
java
@Component
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
//引入持久层
@Resource
private UserMapper userMapper;
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
/**
* 向数据库中插入新的用户信息.
*
* @param userDetails 用户信息
*/
@Override
public void createUser(UserDetails userDetails) {
//实现数据插入
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setEnabled(true);
userMapper.insert(user);
}
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return false;
}
/**
* 通过用户名从数据库中获取用户信息.
*
* @param username 用户名
* @return UserDetails对象
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username);
User user = userMapper.selectOne(queryWrapper);
if (user == null) {
throw new UsernameNotFoundException(username);
} else {
/** Collection<GrantedAuthority> authorities = new ArrayList<>();
// authorities.add(() -> "USER_LIST");
authorities.add(() -> "USER_ADD");
//组装security中的user对象
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
true, //用户账号是否过期
true, //用户凭证是否过期
true, //用户是否未被锁定
authorities //权限列表【暂时先创建空的】
);*/
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.disabled(!user.getEnabled())
.credentialsExpired(false)
.accountLocked(false)
.roles("ADMIN")
.authorities("USER_ADD")
.build();
}
}
}
这里loadUserByUsername方法中,authorities()中的内容是写死的,实际使用中可以通过查询数据库方式实现:
java
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 根据用户名从数据库查询用户信息
AuthUsers user = authUsersService.lambdaQuery()
.eq(AuthUsers::getUsername, username)
.one();
// 2. 校验用户是否存在
if (user == null) {
log.warn("认证失败:用户 '{}' 不存在。", username);
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 3. 查用户账户是否被禁用
if (user.getIsDelete()) {
log.warn("认证失败:用户 '{}' 已被禁用。", username);
// 在Spring Security中,更标准的做法是抛出 `DisabledException`,但为了保持与原逻辑一致,这里仍使用UsernameNotFoundException
throw new UsernameNotFoundException("用户已禁用: " + username);
}
// 4. 获取用户的权限列表
List<GrantedAuthority> authorities = getUserAuthorities(user.getId());
// 5. 构建Spring Security的UserDetails对象
// 这个对象封装了用户名、密码、权限以及账户状态。
return User.withUsername(user.getUsername())
.password(user.getPasswordHash()) // 数据库中存储的已加密密码
.authorities(authorities) // 用户的权限集合
.accountExpired(false) // 账户是否过期
.accountLocked(false) // 账户是否被锁定
.credentialsExpired(false) // 凭证(密码)是否过期
.disabled(false) // 账户是否被禁用
.build();
}
/**
* 根据用户ID获取其对应的所有权限(角色)。
*
* @param userId 用户ID
* @return 用户的权限列表
*/
private List<GrantedAuthority> getUserAuthorities(Long userId) {
// 1. 根据用户ID查询其拥有的所有角色ID
List<Long> roleIds = authUserRolesService.lambdaQuery()
.eq(AuthUserRoles::getUserId, userId)
.select(AuthUserRoles::getRoleId)
.list()
.stream()
.map(AuthUserRoles::getRoleId)
.collect(Collectors.toList());
// 2. 如果用户未分配任何角色,记录日志并返回一个空的权限列表
if (CollectionUtils.isEmpty(roleIds)) {
log.info("用户ID '{}' 未分配任何角色。", userId);
return List.of(); // 返回一个不可变的空列表
}
// 3. 根据角色ID列表,查询所有对应的角色信息
List<AuthRoles> roles = authRolesService.lambdaQuery()
.in(AuthRoles::getId, roleIds)
.list();
// 4. 将角色名称转换为Spring Security的GrantedAuthority对象
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
5. 在接口上设置权限
使用注解@PreAuthorize("hasRole('')")设置访问接口所需要的角色
java
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
public UserService userService;
/**
* 获取用户列表.
*
* @return
*/
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/list")
public List<User> getList() {
return userService.list();
}
/**
* 添加用户.
*
* @param user 用户信息
* @return
*/
@PreAuthorize("hasRole('USER')")
@PostMapping("/add")
public void add(@RequestBody User user) {
userService.saveUserDetails(user);
}
}
也可以使用注解@PreAuthorize("hasAuthority('')")设置访问接口需要的权限
java
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
public UserService userService;
/**
* 获取用户列表.
*
* @return
*/
@PreAuthorize("hasAuthority('USER_LIST')")
@GetMapping("/list")
public List<User> getList() {
return userService.list();
}
/**
* 添加用户.
*
* @param user 用户信息
* @return
*/
@PreAuthorize("hasAuthority('USER_ADD')")
@PostMapping("/add")
public void add(@RequestBody User user) {
userService.saveUserDetails(user);
}
}
更多详情可查看:https://github.com/TimberWill/security-demo
以上为个人学习分享,如有问题,欢迎指出:)