MHBLOG当中SpringBootSecurity+JWT运用结合
SpringbootSecurity的工作流程
当用户在 Spring Boot Security 系统中提交登录的账号和密码时,系统会经历一系列流程来验证用户的身份并建立安全的会话。下面是 Spring Boot Security 工作流程的详细讲解,分解了从用户提交登录信息到完成身份认证的整个过程。
Spring Boot Security 工作流程(登录流程):
-
用户提交登录请求
- 用户通过前端(通常是登录页面)提交用户名和密码,这些信息通过 POST 请求发送到服务端(例如,
/login
路径)。
- 用户通过前端(通常是登录页面)提交用户名和密码,这些信息通过 POST 请求发送到服务端(例如,
-
过滤器链处理请求
- Spring Security 的过滤器链(
FilterChain
)会拦截所有请求,其中的核心过滤器之一是UsernamePasswordAuthenticationFilter
,它专门处理用户提交的认证请求(如用户名和密码)。
- Spring Security 的过滤器链(
-
UsernamePasswordAuthenticationFilter
获取登录信息UsernamePasswordAuthenticationFilter
负责从请求中提取用户名和密码,通常从HttpServletRequest
的参数中提取这些信息。它会将用户输入的凭证封装为UsernamePasswordAuthenticationToken
对象。
-
委托给 AuthenticationManager 进行认证
UsernamePasswordAuthenticationFilter
会将封装好的UsernamePasswordAuthenticationToken
对象提交给AuthenticationManager
,由它负责进一步的认证处理。- Spring Security 默认使用
ProviderManager
作为AuthenticationManager
的实现,它会将认证委托给多个AuthenticationProvider
。
-
AuthenticationProvider
验证凭证- 在典型的应用程序中,Spring Security 使用
DaoAuthenticationProvider
作为默认的AuthenticationProvider
。 DaoAuthenticationProvider
使用UserDetailsService
来加载用户信息,它会根据传入的用户名从数据库或其他存储中加载用户详细信息(通常包括加密的密码)。
- 在典型的应用程序中,Spring Security 使用
-
UserDetailsService
加载用户信息UserDetailsService
的实现类(例如自定义实现或 Spring Security 默认的InMemoryUserDetailsManager
)会查询用户的详细信息,并返回一个UserDetails
对象。- 该对象通常包含:用户名、加密后的密码、账户是否启用、是否未过期等信息。
-
密码验证
- 一旦
UserDetails
被成功加载,DaoAuthenticationProvider
会通过PasswordEncoder
比较用户输入的明文密码和数据库中存储的加密密码是否匹配。 - Spring Security 默认使用
BCryptPasswordEncoder
,它会对输入的明文密码进行加密处理,并与数据库中的哈希密码进行比对。
- 一旦
-
认证成功或失败
- 如果密码匹配且账户未锁定、未过期,
DaoAuthenticationProvider
会将认证成功的用户信息封装为Authenticated UsernamePasswordAuthenticationToken
,并返回给AuthenticationManager
。 - 如果密码不匹配或其他认证信息无效,则抛出异常,如
BadCredentialsException
,认证失败。
- 如果密码匹配且账户未锁定、未过期,
-
安全上下文保存用户信息
- 如果认证成功,
AuthenticationManager
返回的Authentication
对象会被存储到SecurityContext
中。 SecurityContextHolder
是一个持有用户安全上下文的容器,保存了经过认证的用户信息(Authentication
对象),该信息在当前会话中是全局可用的。- 默认情况下,Spring Security 会通过
HttpSessionSecurityContextRepository
将安全上下文保存到用户的HttpSession
中。
- 如果认证成功,
-
创建会话并返回响应
- 如果认证成功,Spring Security 会根据配置创建用户的会话(
Session
)。系统会通过RememberMe
机制,决定是否保持用户的登录状态。 - 如果认证成功,系统会重定向或返回一个成功的响应(例如返回主页或 JWT Token,取决于系统的设计)。
- 如果认证成功,Spring Security 会根据配置创建用户的会话(
-
认证失败处理
- 如果认证失败,Spring Security 会调用失败处理器(例如
SimpleUrlAuthenticationFailureHandler
),返回认证失败的错误信息或重定向到登录页面。
- 如果认证失败,Spring Security 会调用失败处理器(例如
总结流程图:
用户提交用户名和密码
↓
`UsernamePasswordAuthenticationFilter` 拦截请求并提取凭证
↓
凭证交给 `AuthenticationManager` 进行认证
↓
`AuthenticationManager` 调用 `AuthenticationProvider` 进行认证
↓
`AuthenticationProvider` 使用 `UserDetailsService` 加载用户信息
↓
`AuthenticationProvider` 比较输入密码和数据库密码(通过 `PasswordEncoder`)
↓
[认证成功] [认证失败]
↓ ↓
将认证后的信息存入 `SecurityContext` 调用失败处理器
↓ ↓
创建会话并返回响应 返回错误信息
重要组件的作用:
UsernamePasswordAuthenticationFilter
:拦截登录请求,并提取用户名和密码。AuthenticationManager
:负责管理整个认证流程,通常委托给AuthenticationProvider
。AuthenticationProvider
:实际执行认证的核心组件,调用UserDetailsService
加载用户数据并进行密码验证。UserDetailsService
:加载用户信息的接口,通常通过实现这个接口来查询数据库中的用户。PasswordEncoder
:负责密码的加密和比对,通常使用BCryptPasswordEncoder
。SecurityContextHolder
:保存当前认证成功的用户信息,系统通过它判断用户是否已登录。HttpSession
:保存用户的安全上下文信息,默认在会话中存储。
用户登录后的授权流程:
- 一旦用户登录成功并在
SecurityContextHolder
中保存了认证信息,Spring Security 会在每次请求时通过SecurityContext
验证用户是否已登录,以及用户是否有权访问特定资源。
在MHBlog开发的过程当中,运用了SpringbootSecurity+JWT的组合来验证登录
那么什么是JWT呢?
这里不过多描述,JWT不是很难,所以直接放视频不懂直接看JWT使用
使用详解
由上面我们可以知道,在Security的验证是封装成UsernamePasswordAuthenticationToken对象来传输给其他Filter进行验证的,所以我们的SecurityImpl里要创建一个对象来将Username和Password来构建一个UsernamePasswordAuthenticationToken对象
@Override
public ResponseResult login(User user) {
//创建验证对象Token
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
//判断是否认证通过
if (Objects.isNull(authenticate)) {
throw new RuntimeException("用户名或密码错误");
}
//获取UserId生成Token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String id = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(id);
//把用户信息存入redis,然后把Token和UserInfo封装 返回
redisCache.setCacheObject("bloglogin:" + id, loginUser);
UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
BlogUserLoginVo vo = new BlogUserLoginVo(jwt, userInfoVo);
return ResponseResult.okResult();
}
之后将UsernamePasswordAuthenticationToken类提交给AuthenticationManager来进行认证,因为Springboot里默认是没有AuthenticationManager这个Bean的,所以我们得在SecurityConfig类里自己定义一个Bean来添加进IOC容器里
@Configuration
public class SecurityConfig {
//此配置方法适用于5.7版本之后
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
之后在SecurityImpl类的方法里注入AuthenticationManager类的Bean
将UsernamePasswordAuthenticationToken传给AuthenticationManager的authenticate()方法
ProviderManager会调用DaoAuthenticationProvider使用UserDetailsService接口的loadUserByUsername方法来根据Username获取内存中的用户,这时后我们就要创建一个类来实现UserDetailsService接口的loadUserByUsername方法
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户查询用户信息
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(queryWrapper);
// 判断是否查到用户
// 如果没查到抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户不存在");
}
// TODO 查询权限封装
// 查到用户作为方法返回值返回
return new LoginUser(user);
}
}
让我们来查看这个接口的源码
从源码我们可以知道,这个方法要返回的是一个UserDetails类,但是我们ORM数据库查询出来的实体Entity并不是这个类,这个UserDetails我们点进去查看一下
可以发现这是一个接口类
所以我们创建一个LoginUser类来实现这个UserDetails接口
在这个自定义的LoginUser类当中封装一个User类的属性,这样子Details就能获取User的UserName和Password了
让我们回到BlogLoginServiceImpl
getPrincipal()
这又是什么方法?
getPrincipal()
是 Authentication
接口中的一个方法,它用于获取当前经过认证的用户的主要身份信息(通常是用户名或用户对象)。在 Spring Security 中,当用户成功通过身份验证后,Authentication
对象会被存储在 SecurityContext
中,而 getPrincipal()
方法可以返回该用户的身份信息。
详细解释:
-
Authentication
接口:Authentication
是 Spring Security 的核心接口之一,表示经过身份验证或正在进行身份验证的主体(通常是用户)。- 它包含关于身份验证过程中的各种信息,比如用户身份、权限、凭证等。
-
getPrincipal()
方法:getPrincipal()
返回与身份验证相关的主身份信息,即用户的主要标识。这个标识通常是经过认证的用户的用户名或UserDetails
对象。- 如果使用的是基于用户名和密码的身份验证(如
UsernamePasswordAuthenticationToken
),getPrincipal()
通常返回的是实现了UserDetails
接口的对象,或直接是用户名。
示例:getPrincipal()
返回值的类型
-
基于用户名和密码的认证:
- 在基于用户名和密码的认证(如
UsernamePasswordAuthenticationToken
)中,getPrincipal()
返回的是UserDetails
对象,通常是用户的详细信息。
代码示例:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Object principal = authentication.getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails) principal).getUsername(); System.out.println("Authenticated user: " + username); } else { String username = principal.toString(); System.out.println("Authenticated user: " + username); }
- 如果用户通过身份验证,
principal
通常是一个实现了UserDetails
接口的对象,比如自定义的CustomUserDetails
或 Spring Security 提供的org.springframework.security.core.userdetails.User
对象。 - 如果
principal
是字符串类型,通常它是用户的用户名。
- 在基于用户名和密码的认证(如
-
匿名用户或未认证的用户:
- 如果用户尚未认证或是匿名用户,
getPrincipal()
可能返回一个字符串"anonymousUser"
,或者在某些情况下返回null
。
- 如果用户尚未认证或是匿名用户,
常见场景:
-
获取用户详细信息:
在经过身份验证后,通常我们会通过getPrincipal()
获取用户的详细信息(如用户名、权限等),以便在业务逻辑中使用。示例:
@GetMapping("/user-info") public ResponseEntity<?> getUserInfo() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); UserDetails userDetails = (UserDetails) authentication.getPrincipal(); return ResponseEntity.ok(userDetails.getUsername()); }
-
自定义
UserDetails
:
如果你实现了一个自定义的UserDetails
类,比如CustomUserDetails
,那么getPrincipal()
方法会返回你的自定义用户对象,你可以通过它获取与用户相关的自定义信息。
总结:
getPrincipal()
方法用于返回经过身份验证的用户的主要标识信息,通常是一个UserDetails
对象或用户名。- 在大多数情况下,它返回的是一个实现了
UserDetails
接口的对象(如自定义的用户类),可以通过它获取用户名、密码、权限等用户信息。 - 对于匿名用户或未认证的用户,
getPrincipal()
可能返回"anonymousUser"
或null
。
通过 getPrincipal()
,你可以轻松获取当前认证用户的详细信息并用于后续的业务逻辑。
也就是说这里返回的是一个LoginUser类,这个类当中有我们主要的用户实体User
从下面的代码我们可以看得出来
之后就是将user的ID作为Subject传入JWT里再存入Redis缓存(k,v)当中
最后再将用户信息VO类和JwtToken封装进前端API所规定的Vo类当中返回
注:
在DaoAuthenticationProvider当中会选择PasswordEncoder,所以我们在Security里配置一下PasswordEncoder
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
这个编码是单向不可逆的,对比是否一致就是对比哈希值,密码:
工作流程例子:
详细的工作流程:
-
前端用户输入的账号密码封装成一个对象
- 前端用户提交登录表单,包含用户名和密码。该信息通过 POST 请求发送到后端,后端会将其封装为一个对象(例如
LoginRequest
),然后传递给UsernamePasswordAuthenticationToken
进行处理。
例如:
public class LoginRequest { private String username; private String password; // getters and setters }
- 前端用户提交登录表单,包含用户名和密码。该信息通过 POST 请求发送到后端,后端会将其封装为一个对象(例如
-
封装成
UsernamePasswordAuthenticationToken
对象- 后端接收到
LoginRequest
对象后,将用户名和密码封装成UsernamePasswordAuthenticationToken
,这是 Spring Security 内置的一个认证对象,用于存放用户凭证(用户名和密码)。
代码示例:
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
- 后端接收到
-
调用
authenticationManager
进行认证- 接下来,将这个
UsernamePasswordAuthenticationToken
对象传递给authenticationManager
的authenticate()
方法。 authenticationManager
是 Spring Security 中用于处理认证请求的核心组件。默认情况下,Spring Boot 不会自动配置一个authenticationManager
,所以你需要在配置类中手动定义它。
代码示例:
Authentication authentication = authenticationManager.authenticate(authenticationToken);
- 接下来,将这个
-
默认没有
authenticationManager
,需要手动配置- Spring Boot 默认没有自动配置
AuthenticationManager
,因此你需要在你的配置类(通常是SecurityConfig
)中手动配置一个AuthenticationManager
Bean。
配置
AuthenticationManager
Bean:@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
- Spring Boot 默认没有自动配置
-
AuthenticationManager
调用DaoAuthenticationProvider
进行认证- 当调用
authenticationManager.authenticate()
时,实际执行认证的是DaoAuthenticationProvider
。 DaoAuthenticationProvider
会调用UserDetailsService
来加载用户的详细信息,进行认证(包括密码验证)。
- 当调用
-
UserDetailsService
从数据库中根据用户名查询用户信息UserDetailsService
是 Spring Security 中的一个接口,专门用于从数据源(例如数据库)中加载用户信息。- 你需要自己实现
UserDetailsService
接口,并重写loadUserByUsername(String username)
方法。该方法通过传入的username
查询数据库中对应的用户信息。
实现
UserDetailsService
:@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 根据用户名查询用户 User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found"); } // 将用户信息封装到 UserDetails 并返回 return new CustomUserDetails(user); } }
-
将查询到的用户封装成
UserDetails
对象返回- 如果用户存在,
loadUserByUsername
方法会将用户信息封装到实现了UserDetails
接口的类中(如CustomUserDetails
),然后返回该对象。 CustomUserDetails
类是一个自定义的类,通常用来封装用户的关键信息,例如用户名、密码、权限等。
示例:
public class CustomUserDetails implements UserDetails { private User user; public CustomUserDetails(User user) { this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { // 返回用户的权限 return Arrays.asList(new SimpleGrantedAuthority(user.getRole())); } @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 user.isEnabled(); } }
- 如果用户存在,
-
验证密码并返回
Authentication
对象DaoAuthenticationProvider
使用PasswordEncoder
来验证用户输入的密码是否与数据库中存储的加密密码一致。通常使用BCryptPasswordEncoder
来进行密码的加密和匹配。- 如果验证成功,
DaoAuthenticationProvider
会返回一个认证成功的Authentication
对象,并将其存储在 Spring Security 的上下文中。
-
认证成功后存入
SecurityContext
并放行请求- 认证成功后,Spring Security 会将
Authentication
对象存入SecurityContextHolder
中,用户的信息就可以在整个会话中使用。 - 之后,用户可以访问受保护的资源,Spring Security 会通过
SecurityContext
验证用户的身份和权限。
- 认证成功后,Spring Security 会将
总结:
- 用户登录时输入用户名和密码。
- 封装到
UsernamePasswordAuthenticationToken
中。 - 调用
authenticationManager.authenticate()
进行认证。 AuthenticationManager
调用DaoAuthenticationProvider
。DaoAuthenticationProvider
使用UserDetailsService
加载用户信息。UserDetailsService
从数据库中查询用户信息。- 将用户信息封装到
UserDetails
对象中返回。 DaoAuthenticationProvider
验证密码。- 认证成功后,保存到
SecurityContext
中。
你需要自定义 UserDetailsService
来查询数据库中的用户信息,并配置 AuthenticationManager
以使用 DaoAuthenticationProvider
进行认证。