Spring Security认证与授权全解析
笔记哥 /
04-18 /
7点赞 /
0评论 /
437阅读
# 什么是Spring Security
> Spring Security是基于Spring框架,提供了一套Web应用安全性框架.专门为Java应用提供**用户认证(Authentication)和用户授权(Authorization),支持单体应用到微服务的全场景安全防护**
## 认证(Authentication)
- 验证某个用户是否为系统中的合法主体,即确认该用户是否可以访问此系统.认证一般需要用户提供用户名与密码,提供校验用户名与密码完成认证过程
- 简单来说→**认证是判断该用户是否能登录;**
- 关键要素:1.`Principal`:用户主体(如用户名)2.`Credentials`:验证凭证(如用户密码)3.`Authorities`:用户权限集合
## 授权(Authorization)
- 是指某个用户是否有权限执行某个操作.在同一系统中,不同用户所具有的权限是不同的.如对某一文件,有些用户只能读不能修改,而有些用户既可读也可以修改.某个角色都有一系列的权限
- 简单来说→**授权是判断该用户是否有权限去做特定的操作;**
- 关键要素:1.角色(`Role`):用户分组标识2.权限(`Permission`):具体操作权限
## 优势和缺点
- 优势
- 深度与Spring整合:无缝支持`Spring Boot、Spring MVC、Spring Data`等框架
- 企业级安全方案:支持O`Auth2,SAML,LDAP,JWT`等协议,满足复杂安全需求
- 旧版本无法脱离Web环境
- 新版本对框架进行分层提取,分为核心板块和Web板块,
- 缺点
- 性能开销:默认开启CSRF,Session管理等特性,对高性能场景需手动优化
- 配置复杂度高:默认配置覆盖大量安全规则,需要显式覆盖才能简化
## 与Shiro对比
| 对比维度 | Spring Security | **Shiro** |
| --- | --- | --- |
| 生态整合 | 深度集成 Spring 技术栈 | 需手动整合Spring,对非 Spring 项目更友好 |
| 微服务支持 | 天然支持 Spring Cloud Security | 需自行实现分布式会话和权限管理 |
| **典型场景** | 企业级应用,微服务架构,需要 OAuth2 的 SaaS 系统 | 中小型 Web 应用,移动端后台,快速开发项目 |
- 一般来说常见的安全管理技术栈组合是这样:
- SSM+Shiro
- Spring Boot/Spring Cloud+Spring Security
# Spring Security实现原理
>
>
> 对Web资源最好的保护是Filter,对方法调用的最好方法是AOP
>
- Spring Security进行认证和权限检验时就是通过一系列的Filter来进行拦截

- 如图所示:一个请求要访问到后端,就要从左到右经过这些过滤器.**其中绿色的过滤器是负责认证的过滤器,蓝色部分是负责异常处理的过滤器,橙色是负责权限校验的拦截器.**
- 对于我们而言,只需关注**`UserNamePasswordAuthenticationFilter**->`**负责登入认证和`FilterSecurityInterceptor`**->**负责授权\*\*
对于Spring Security,掌握了过滤器和组件就完全掌握了Spring Security.其使用方法就是对过滤器和组件进行扩展
## Spring Security入门
- 添加Spring Security相关依赖
```csharp
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt-api
0.12.6
io.jsonwebtoken
jjwt-impl
0.12.6
runtime
io.jsonwebtoken
jjwt-jackson
0.12.6
runtime
```
>
>
> 编写UserController进行测试
>

>
>
> 启动项目,访问localhost:8080进行测试,其会自动跳转到localhost:8080/login登入页面
>

- 默认的用户名:user
- 其密码会在项目启动时打印在控制台

- 输入正确的用户名和密码时,即可成功访问UserController中的get方法→说明Spring Security保护生效
- 当然在实际开发中,这种默认配置是不存在的,我们需要扩展这些组件

## 用户认证
用户认证的流程

核心接口:
- `Authentication`接口,表示当前访问系统的用户,封装了用户的信息,其实现类为`UsernamePasswordAuthenticationToken`
- `AuthenticationManager`接口,其定义了Authentication的方法
- `UserDetailsService`接口,加载用户特定数据的核心接口,其中定义了一个根据用户名查询用户信息的方法
- `UserDetails`接口,提供核心用户信息.将UserDetailsService中获取的信息封装为`UserDetails`对象返回.并将其封装至Authentication对象中
UserDetails中的基本方法:

用于认证的核心组件:
>
>
> 对于系统来说,同一时间会有多个用户正在使用,那么如何确认哪个用户正在请求登录接口是登录认证的核心目的.Spring Security提出了:**当前登录用户/当前认证用户**,Spring Security中使用`Authentication`来存储认证信息,表示当前用户
>
>
>
> 在Spring boot中使用安全上下文`SecurityContext`来获取`Authentication`,`SecurityContext`交有`SecurityContextHolder`来管理,使用以下方法即可获取`Authentication`
>
```csharp
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
```
- Spring Security三个认证核心组件为:
1. `Authentication`:存储认证信息的上下文,代表当前用户
2. `SecurityContext`:上下文对象,用来获取`Authentication`
3. `SecurityContextHolder`:上下文管理对象,用来获取SecurityContext
### 认证逻辑
- `AuthenticationManager`是Spring Security用于执行身份验证的组件,其`authenticate`方法可以完成认证.Spring Security默认的认证方法是在`UsernamePasswordAuthenticationFilter`这个过滤器中进行认证的
关键代码如下:
```csharp
// 创建用户认证令牌,使用用户名和密码作为凭证
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
// 通过认证管理器执行Spring Security认证流程,返回包含用户详情的认证对象
Authentication authenticate = authenticationManager.authenticate(token);
```
### 加密器PasswordEncoder
- `passwordEncoder`在Spring Security中用于处理密码加密存储和验证.
- 它可以负责将用户提交的明文密码转换为不可逆的加密字符串(如BCrypt算法),之后便将密码存储到数据库中
- 在用户登录时,验证用户输入的明文密码是否与存储的加密密码一致
>
>
> 若需要自定义加密方法,我们可以编写自定义加密器`CustomPasswordEncoder`
>
```csharp
public class CustomPasswordEncoder implements PasswordEncoder {
// 自定义密码加密方式,使用MD5加密算法
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())).equals(encodedPassword);
}
// 自定义密码加密方式,使用MD5加密算法
@Override
public String encode(CharSequence rawPassword) {
return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes()));
}
}
```
>
>
> 并在SecurityConfig中注册新的加密器
>
```csharp
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfiguration {
// 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new CustomPasswordEncoder();
}
}
```
>
>
> 直接使用`BCryptPasswordEncoder`加密器
>
```csharp
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfiguration {
// 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
```
## 自定义登录接口
- 首先要重写`SecuritySpring`中的方法,可以自己写使用@Bean注册,也可以重写`WebSecurityConfigurerAdapter`接口的方法
>
>
> 由于Spring Security会对每一个接口都会进行认证,有些接口需要放行,直接让用户访问,我们就得在`config()`中进行放行
>
- 接着需要把`AuthenticationManager`注入容器→因为要使用其的authenticate方法进行验证
```csharp
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 配置密码编码器
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new CustomPasswordEncoder();
}
/**
* 配置HTTP安全设置
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用CSRF保护(常用于API场景)
.csrf().disable()
// 配置会话管理为无状态(不创建和使用HTTP Session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 配置请求授权规则
.authorizeRequests()
// 允许匿名访问登录端点
.antMatchers("/user/login").permitAll()
// 所有其他请求需要认证
.anyRequest().authenticated();
}
/**
* 暴露AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
```
- IUserService
- 在其中编写`login()`方法来实现登录逻辑
```csharp
public interface ISysUserService extends IService {
/**
* @description: 登录
* @author: HYJ
* @date: 2025/4/15 0:01
* @param: [user]
* @return: edu.ptu.springsecurity.common.AjaxResult
**/
AjaxResult login(SysUser user);
}
```
- LoginUser
- `LoginUser`实现`UserDetails`接口,重写其中的方法.将业务数据衔接到Spring Security的认证体系中
```csharp
// 忽略未知的属性,避免序列化时出现异常
@JsonIgnoreProperties(ignoreUnknown = true)
public class LoginUser implements UserDetails {
// 用户信息
private SysUser user;
/**
* @description: 账号是否未过期
* @author: HYJ
* @date: 2025/4/15 0:02
* @param: []
* @return: boolean
**/
@Override
public boolean isEnabled() {
return true;
}
/**
* @description: 密码是否未过期
* @author: HYJ
* @date: 2025/4/15 0:02
* @param: []
* @return: boolean
**/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* @description: 账号是否未锁定
* @author: HYJ
* @date: 2025/4/15 0:02
* @param: []
* @return: boolean
**/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* @description: 账号是否未过期
* @author: HYJ
* @date: 2025/4/15 0:02
* @param: []
* @return: boolean
**/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* @description: 获取用户名
* @author: HYJ
* @date: 2025/4/15 0:02
* @param: []
* @return: java.lang.String
**/
@Override
public String getUsername() {
return user.getUsername();
}
/**
* @description: 获取密码
* @author: HYJ
* @date: 2025/4/15 0:02
* @param: []
* @return: java.lang.String
**/
@Override
public String getPassword() {
return user.getPassword();
}
/**
* @description: 获取权限
* @author: HYJ
* @date: 2025/4/15 0:02
* @param: []
* @return: java.util.Collection extends org.springframework.security.core.GrantedAuthority>
**/
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return null;
}
}
```
- 我们还可以使用另一种写法,更加贴合实际的开发环境,若觉得实现`UserDetails`接口比较繁琐,我们可以继承Spring Security提供的`org.springframework.security.core.userdetails.User`类.其内部已经帮我们实现了`UserDetails`接口,省去了大量重写工作
```csharp
@Getter
@Setter
public class LoginUser extends User {
private SysUser user;
/**
* 构造函数
*
* @param user 用户信息
* @param authorities 权限列表
*/
public LoginUser(SysUser user, Collection extends GrantedAuthority> authorities) {
super(user.getUsername(), user.getPassword(), authorities);
this.user = user;
}
}
```
- 推荐使用实现`UserDetails`接口的方法.继承User类会受父类牵制
- UserDetailsServiceImpl
- `UserDetailsServiceImpl`实现`UserDetails`了,扮演着 **用户数据与认证流程之间的桥梁**角色,其中重写`loadUserByUsernmae()`核心方法来实现从DB中获取用户信息,并将其封装为 Spring Security可识别的安全对象→`UserDetails`
```csharp
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserServiceImpl userService;
/**
* @description: 加载用户信息
* @author: HYJ
* @date: 2025/4/15 0:01
* @param: [username]
* @return: org.springframework.security.core.userdetails.UserDetails
**/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
// 构建查询条件,根据用户名查询用户信息
wrapper.eq(SysUser::getUsername, username);
SysUser user = userService.getOne(wrapper);
// 检查用户是否存在
if (Objects.isNull(user)) {
throw new RuntimeException("用户不存在");
}
// 返回包含用户详细信息的 LoginUser 对象
return new LoginUser(user);
}
}
```
- UserServiceImpl
- 实现login的底层操作
```csharp
@Service("userService")
public class UserServiceImpl extends ServiceImpl implements ISysUserService {
// 注入认证管理器,用于处理用户认证流程
@Resource
private AuthenticationManager authenticationManager;
// 注入Redis工具类,用于操作Redis缓存
@Resource
private RedisUtil redisUtil;
@Override
public AjaxResult login(SysUser user) {
// 创建用户认证令牌,使用用户名和密码作为凭证
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
// 通过认证管理器执行Spring Security认证流程,返回包含用户详情的认证对象
Authentication authenticate = authenticationManager.authenticate(token);
// 判断认证是否成功
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败,认证信息为空");
}
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
// 生成JWT令牌
String jwt = JWTUtil.createToken(loginUser.getUser());
// 认证成功,将用户信息存入Redis缓存
redisUtil.setCacheObject("user:" + user.getUserId(), user);
return AjaxResult.success("登录成功", jwt);
}
}
```
## 用户授权
- 用户授权是系统在确认用户身份后,根据其角色或者权限(Permissions)决定其能允许访问的资源或操作.

## LoginUser改造
- 在LoginUser中添加permissions字段→用于存储用户权限信息,authorities字段→存储springsecurity中所需的集合
```csharp
private SysUser user;
// 存储用户权限信息
private List permissions;
//防止出现序列化问题
@JsonIgnore
// 存储SpringSecurity所需要的权限信息的集合
private List authorities;
public LoginUser(List permissions, SysUser user) {
this.permissions = permissions;
this.user = user;
}
```
- 重写`getAuthorities()`方法用于将permissions中的权限封装为GrantedAuthority对象
```csharp
@Override
public Collection extends GrantedAuthority> getAuthorities() {
if (Objects.isNull(authorities)) {
authorities = new ArrayList<>();
}
// 将权限字符串封装成GrantedAuthority对象
permissions.forEach(permission ->
authorities.add(new SimpleGrantedAuthority(permission)));
return authorities;
}
```
## 构建查询权限的mapper
- 在userMapper下创建`findUserPermListByUserId()`方法用于获取用户权限
```csharp
public interface UserMapper extends BaseMapper {
/**
* 根据用户id查询权限列表
*
* @param userId 用户id
* @return permList 权限列表
*/
List findUserPermListByUserId(Long userId);
}
```
## 改造UserDetailsServiceImpl
- 调用`findUserPermListByUserId()`查询权限信息,放回到LoginUser中
```csharp
List userPermList = userMapper.findUserPermListByUserId(sysUser.getUserId());
// 返回用户信息和权限列表
return new LoginUser(userPermList, sysUser);
```
## 授权实现
### 在启动类进行配置
- 在启动类上配置注解启动,**来判断用户对某个控制层的方法是否具有访问权限**
```csharp
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityApplication.class, args);
}
}
```
## 在controller的方法进行配置
- 在方法上加上`@PreAuthorize`标签控制接口权限
### 自定义验证方法的实现方法
- 编写`AuthPermissonUtils`方法实现权限验证逻辑
```csharp
@Component("auth")
public class AuthPermissionUtils {
/**
* 判断是否有该权限
*
* @param permission 权限字符串
* @return true 有该权限 false 没有该权限
*/
public boolean hasPermission(String permission) {
// 获取当前用户的权限信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 判断用户是否有该权限
List permissions = loginUser.getPermissions();
return permissions.contains(permission);
}
}
```
```csharp
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService sysUserService;
@GetMapping("/living")
@PreAuthorize("@auth.hasPermission('living')")
public Result living() {
return Result.success("可以开房");
}
@GetMapping("/upgrade")
@PreAuthorize("@auth.hasPermission('upgrade')")
public Result upgrade() {
return Result.success("可以升级房型");
}
@GetMapping("/freeBreakfast")
@PreAuthorize("@auth.hasPermission('freeBreakfast')")
public Result freeBreakfast() {
return Result.success("有免费早餐");
}
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
return sysUserService.login(request);
}
}
```
# 异常处理方法
- 在遇到认证失败和授权失败时,我们希望可以放回与接口相同的json结构,这样可以让前端进行统一处理
- 如果**认证过程**中出现异常会被封装成AuthenticationException如何调用**`AuthenticationEntryPoint`**对象的方法去进行异常处理
```csharp
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(
JSON.toJSONString(Result.fail(401, "用户身份认证不通过"))
);
}
}
```
- 如果授权过程中出现的异常就会被封装AccessDeniedException然后调用**AuthenticationEntryPoint**对象的方法进行异常处理
```csharp
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(
JSON.toJSONString(Result.fail(401, "用户身份认证不通过"))
);
}
}
```
## 在SecurityConfig中进行配置
- 注入处理器
```csharp
@Resource
private AccessDeniedHandlerImpl accessDeniedHandler;
@Resource
private AuthenticationEntryPointImpl authenticationEntryPoint;
```
- 在使用http进行配置
```csharp
// 配置异常处理器
http
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
```
本文来自投稿,不代表本站立场,如若转载,请注明出处:http//www.knowhub.vip/share/2/2387
- 热门的技术博文分享
- 1 . ESP实现Web服务器
- 2 . 从零到一:打造高效的金仓社区 API 集成到 MCP 服务方案
- 3 . 使用C#构建一个同时问多个LLM并总结的小工具
- 4 . .NET 原生驾驭 AI 新基建实战系列Milvus ── 大规模 AI 应用的向量数据库首选
- 5 . 在Avalonia/C#中使用依赖注入过程记录
- 6 . [设计模式/Java] 设计模式之工厂方法模式
- 7 . 5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
- 8 . SQL 中的各种连接 JOIN 的区别总结!
- 9 . JavaScript 中防抖和节流的多种实现方式及应用场景
- 10 . SaltStack 远程命令执行中文乱码问题
- 11 . 推荐10个 DeepSeek 神级提示词,建议搜藏起来使用
- 12 . C#基础:枚举、数组、类型、函数等解析
- 13 . VMware平台的Ubuntu部署完全分布式Hadoop环境
- 14 . C# 多项目打包时如何将项目引用转为包依赖
- 15 . Chrome 135 版本开发者工具(DevTools)更新内容
- 16 . 从零创建npm依赖,只需执行一条命令
- 17 . 关于 Newtonsoft.Json 和 System.Text.Json 混用导致的的序列化不识别的问题
- 18 . 大模型微调实战之训练数据集准备的艺术与科学
- 19 . Windows快速安装MongoDB之Mongo实战
- 20 . 探索 C# 14 新功能:实用特性为编程带来便利