SpringSecurity是一个功能强大且高度可定制的身份验证和访问控制框架。提供了完善的认证机制和方法级的授权功能。
二、登录认证流程
springsecurity的默认登录流程图:
我们平常的登录的整体流程是:用户点击登录从前端发起请求,后端接受前端发送过来的用户名和密码,然后去数据库查询是否存在此用户,如果存在,就放行,让用户进入系统,如果不存在用户或者密码错误,则提示用户信息错误。在springsecurity中,也大致遵循这个思路。
下面我们来一一解读一下:
user:前端传过来的参数封装到了user对象里面。
UsernamePasswordAuthenticationToken:(位于org.springframework.security.authentication包下)通过类名可以看出,它是使用用户名密码方式进行认证。它需要传两个参数用户名和密码。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
// 序列化id
private static final long serialVersionUID = 530L;
// 一般指的是用户名
private final Object principal;
// 一般指的是密码
private Object credentials;
// 构造器,
// principal 一般指的是用户名
// credentials 一般指的是密码
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
// 初始化父类构造 权限集合为空
super((Collection)null);
this.principal = principal; //principal传过来的用户名
this.credentials = credentials; //credentials穿过来的密码
// 设置是否已认证 默认为false
this.setAuthenticated(false); //false表示未认证
AuthenticationManager:认证管理接口类,它有好几个实现类。在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录(还有这样的操作?没想到吧),所以说AuthenticationManager一般不直接认证,AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。
流程:
我们获得UsernamePasswordAuthenticationToken 对象后,会去执行authenticationManager的authenticae方法。当我们点authenticationManager会看到,它其实是一个接口,里面就只有一个authenticae空方法。
那我们找到这个接口的实现类ProviderManager,里面有authenticae的实现方法。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//通过for循环,查看是否支持该登录方式
for (AuthenticationProvider provider : getProviders()) {
//获取class,判断当前provider是否支持authentication
if (!provider.supports(toTest)) {
continue;
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
try {
//如果支持,则调用provider的authentication方法开始校验
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
catch (AccountStatusException | InternalAuthenticationServiceException ex) {
prepareException(ex, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw ex;
catch (AuthenticationException ex) {
lastException = ex;
//如果不支持的话调用父级提供Provider,重新执行该authenticate方法,看是否支持该登录方式
if (result == null && this.parent != null) {
// Allow the parent to try.
try {
parentResult = this.parent.authenticate(authentication);
result = parentResult;
catch (ProviderNotFoundException ex) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
catch (AuthenticationException ex) {
parentException = ex;
lastException = ex;
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
// If the parent AuthenticationManager was attempted and successful then it
// will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent
// AuthenticationManager already published it
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
return result;
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
// If the parent AuthenticationManager was attempted and failed then it will
// publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the
// parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
throw lastException;
result = provider.authenticate(authentication);:进入的是AbstractUserDetailsAuthenticationProvider#authenticate 方法中:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
//从Authentication提取出登录用户名
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
//从数据库中获取到一个实现了UserDetails的user对象
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw ex;
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
try {
this.preAuthenticationChecks.check(user);//preAuthenticationChecks.check()方法区检验user中的各个账户状态是否正常,例如账户是否被禁用、账户是否被锁定、账户是否过期等等。
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);//additionalAuthenticationChecks方法则是做密码比对的
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
this.postAuthenticationChecks.check(user);//postAuthenticationChecks.check方法中检查密码是否过期
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
//通过createSuccessAuthentication方法构建一个新的UsernamePasswordAutheToken
return createSuccessAuthentication(principalToReturn, authentication, user);
依次展开来说:
1、首先是retrieveUser方法,这个方法的实现在DaoAuthenticationProvider类里面,因为这个类继承了AbstractUserDetailsAuthenticationProvider类,故retrieveUser函数会去数据库里面查找有没有用户名为username的用户,DaoAuthenticationProvider里面的retrieveUser实现代码如下:
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//this.getUserDetailsService()读取了框架中的UserDetailsService(这是个接口),进而去UserDetailsService里面执行loadUserByUsername方法,然而我们通常会写xxDetailsService来实现UserDetailsService接口,并注入到框架中,所以会去执行我们自己写的xxDetailsService里面的loadUserByUsername方法
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
return loadedUser;
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
catch (InternalAuthenticationServiceException ex) {
throw ex;
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
自己写的xxDetailsService里面的loadUserByUsername方法。下面是我的接口实现类
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName,s);
User user = userMapper.selectOne(wrapper);
//在这里进行判断一下,数据库里面是否有此用户信息。如果没有查询到用户信息就抛出异常
if(Objects.isNull(user)){
throw new RuntimeException("用户名或者密码错误");
//TODO 查询对应的权限信息
// List<String> permission = Arrays.asList("test","admin");
List<String> list = menuMapper.selectPermsByUserId(user.getId());
//把数据封装成userDetails进行返回
LoginUser loginUser = new LoginUser(user,list);
return loginUser;
2、preAuthenticationChecks.check()函数,这个函数会去检查我们账户的状态信息,例如账户是否被禁用、账户是否被锁定、账户是否过期,具体代码如下:
public void check(UserDetails user) {
//检查账户是否锁定
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
//检查账户是否启用
if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
//检查账户是否过期
if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account has expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
默认情况下,在我的UserDetails接口实现类LoginUser里面,重写了这些属性值,默认返回true就好。
public class LoginUser implements UserDetails {
private User user;
private List<String> permissions;
public LoginUser(User user, List<String> permission) {
this.user = user;
this.permissions = permission;
@JSONField(serialize = false)//不会序列化到流中
private List<SimpleGrantedAuthority> authorities;
//返回权限信息的方法
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
//把permissions中的string类型的权限信息封装成SimpleGrantedAuthority对象
// List<SimpleGrantedAuthority> list =null;
// for(String permission : permissions){
// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
// list.add(simpleGrantedAuthority);
// }
if(authorities == null){
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
//返回密码
@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 true;
3、additionalAuthenticationChecks()函数,这个函数会去匹配密码,当然是拿登录用户传进来的明文密码去匹配数据库里面的加密后的密码,具体代码如下
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
//获得登录用户的明文密码
String presentedPassword = authentication.getCredentials().toString();
//将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
首先获得登录用户的明文密码 ,然后将明文密码与之前从数据库里面获取的userDetails对象的密文密码做匹配。匹配函数实现不做讲述。
最终通过createSuccessAuthentication(principalToReturn, authentication, user)方法,返回一个authentication,在这个方法里会去重新创建一个UsernamePasswordAuthenticationToken,将已认证状态标志注明。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// Ensure we return the original credentials the user supplied,
// so subsequent attempts are successful even with encoded passwords.
// Also ensure we return the original getDetails(), so that future
// authentication events after cache expiry contain the details
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal,
authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
为什么想和大家捋一捋 Spring Security 登录流程呢?这是因为之前小伙伴们的一个提问:如何在 Spring Security 中动态修改用户信息?
如果你搞清楚了 Spring Security 登录流程,这其实不是问题。
先来大致描述一下问题场景:
你在服务端的安全管理使用了 Spring Security,用户登录成功之后,Spring Security 帮你把用户信息保存在 Session 里,但是具体保存在哪里,要是不深究你可能就不知道, 这带来了一个问题,如果用户在前端操作修改了当前
spring security 主要有两大功能,即认证和授权
1、security是如何认证账号的:
验证逻辑实现是在类AbstractUserDetailsAuthenticationProvider,此类实现了接口AuthenticationProvider的接口方法
Authentication authenticate(Authentication authentication) thr...
Spring Security的登录认证流程如下:
1. 用户输入用户名和密码,生成一个AuthenticationToken对象。
2. 这个Token对象被传递给一个实现了AuthenticationManager接口的对象进行验证。
3. AuthenticationManager对Token对象进行验证,验证成功后返回一个Authentication对象。
4. 在验证成功后,可以调用AuthenticationSuccessHandler成功处理器进行跳转。
5. 在createSuccessAuthentication方法中,会重新创建一个UsernamePasswordAuthenticationToken对象,并将已认证状态标志注明。
6. 在认证流程中,我们会执行authenticationManager的authenticate方法,该方法实际上是一个接口,里面只有一个空方法。
总结一下,Spring Security的登录认证流程包括用户输入凭证信息,验证凭证信息,并返回认证结果的过程。123
#### 引用[.reference_title]
- *1* [spring-security-用户登陆验证流程](https://blog.csdn.net/a1396537376/article/details/90706729)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item]
- *2* *3* [SpringSecurity登录认证流程](https://blog.csdn.net/weixin_52353216/article/details/127359420)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}} ] [.reference_item]
[ .reference_list ]