使用的jar和版本:
springboot:2.4.2
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
<version>2.2.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.socialgroupId>
<artifactId>spring-social-securityartifactId>
<version>1.1.6.RELEASEversion>
dependency>
一些解释和说明
(1)Authentication(身份验证)对象:是Spring Security用来描述当前用户的相关信息的一个对象。而对于不同的登录方式,会实现一种该组件,实际上就是个pojo。
查看它的源代6个方法,后面的流程有用到。
public interface Authentication extends Principal, Serializable {
'
//授予委托人的权限集合。例如登录人是员工则是员工的权限,登录者是用户则是用户的权限。
Collection<? extends GrantedAuthority> getAuthorities();
//证明主体正确的凭据,通常是密码
Object getCredentials();
//存储有关身份验证请求的其他详细信息。例如:IP地址,证书序列号等。
Object getDetails();
//被认证主体的身份
Object getPrincipal();
//令牌是否通过身份验证
boolean isAuthenticated();
//设置身份验证令牌是否受信任
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
(2)idea按ctrl+n可以搜索所有的源代码
只要其中一个登录过滤器链通过,其它的相同类型的过滤器直接通过,不再检验。这也好理解,因为我们登录账号的时候只需要使用一种登录方式进行检验就行。
例如:
BasicAuthenticationFilter过滤器:登录方式默认弹出一个输入弹窗
UsernamePasswordAuthenticationFilter过滤器:为最常常见的账号密码登录方式
除此之外还有邮箱登录、短信登录等等。
本文主要讲解账号密码登录的方式
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//这个类设置许多的默认值,后期都可以修改为我们需要的
//在这里默认账号为username,密码为password,默认请求路径和方式分别为/login,POST
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
//默认的登录访问地址
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
//仅仅接受post请求
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
//接受request请求 和 response响应
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//调用了两个属性进行判断是否为post请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
//获取用户名和密码
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
//构造UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
//为details属性赋值
setDetails(request, authRequest);
// 调用authenticate方法进行校验,AuthenticationManager接口的ProviderManager实现类进行校验
return this.getAuthenticationManager().authenticate(authRequest);
}
构造UsernamePasswordAuthenticationToken对象
传入获取到的用户名和密码,而用户名对应UPAT对象中的principal属性,而密码对应credentials属性。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
//UsernamePasswordAuthenticationToken对象构造器
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
//默认指定不应信任身份验证令牌
setAuthenticated(false);
}
//代码省略
//。。。。
//。。。。
通常,一个 AuthenticationManager(或更常见的一个 AuthenticationProvider)将在成功进行身份验证之后返回一个不变的身份验证令牌,在这种情况下,令牌可以安全地返回 true此方法。返回true将提高性能,因为AuthenticationManager不再需要为每个请求调用。
//AuthenticationManager接口
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
//AuthenticationProvider接口
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
i
boolean supports(Class<?> var1);
}
ProviderManager实现类
通过查找后进入,然后使用ctrl+H组合键查看它的继承关系,找到ProviderManager实现类,它实现了AuthenticationManager接口,它有个重要的authenticate方法。主要是根据传入的token遍历容器中的所有的provider,找到对应的适配器,并调用适配器去处理当前token。
如果有多个AuthenticationProvider支持传递的 Authentication对象,则第一个能够成功验证该Authentication对象的对象将确定 result,从而覆盖AuthenticationException 早期支持AuthenticationProviders抛出的任何可能(提高性能)。验证成功后,AuthenticationProvider将不会尝试后续的。如果通过任何支持均未通过身份验证, AuthenticationProvider则将抛出最后一个抛出的错误 AuthenticationException。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//代码省略
//。。。。
//。。。。。
//整个过程主要就是确定用户当前是登录哪种登录方式而使用对于的登录过滤器进行验证
//迭代多个AuthenticationProvider对象(登录验证过滤器)
for (AuthenticationProvider provider : getProviders()) {
//注意这个supports方法
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//注意这个authenticate方法
//确定Authentcation对象
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;
//代码省略
//。。。。
//。。。。
}
/*
ProviderManager中有一个List用来存储定义的AuthenticationProvider认证实现类
也可以认为是一个认证处理器链来支持同一个应用中的多个不同身份认证机制
ProviderManager将会根据顺序来进行验证
*/
public List<AuthenticationProvider> getProviders() {
return this.providers;
}
AbstractUserDetailsAuthenticationProvider抽象类
工作流程:先看缓存中是否有用户,如果没有,调用自实现去找,找到之后,做了后置检测,检测4个标志是否为true,并且判断容器中是否有passwordencoder,如果有,使用加密匹配规则,如果没有,直接用字符串匹配前端以及数据库中的密码。
authenticate认证方法
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
//如果从缓存中没有获取到UserDetails
//表示没有登录成功或记录
//那么它调用retrieveUser方法来获取用户信息UserDetails,同时捕获异常
if (user == null) {
cacheWasUsed = false;
try {
//这里的retrieveUser是抽象方法,主要是关注它的子类实现。
//等下会讲,先观察UserDetails
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
//代码省略(下面会将)。。。
//........。。
//...........
return createSuccessAuthentication(principalToReturn, authentication, user);
}
supports是否支持某种类型token的方法
public boolean supports(Class<?> aClass) {
//返回UsernamePasswordAuthenticationToken
//说明它是支持UsernamePasswordAuthenticationToken类型的AuthenticationProvider
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
}
UserDetails接口
发现该封装类一共就7个方法,都是用来表示用户的认证信息的,从上往下,依次为,用户权限列表,密码,用户名,账号本身是否过期,是否锁定,本次登录凭证是否过期,账号本身是否可用。基本上封装了大多数系统所需要的认证信息。如果系统中没有需要验证账号本身是否过期的业务,那么也可以将这些字段永远设置为真,如果为假,那么则认证失败。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();//在集合中获取所有权限
String getPassword();//获取密码
String getUsername();//获取用户名
boolean isAccountNonExpired();//是否账号过期
boolean isAccountNonLocked();//是否账号被锁定
boolean isCredentialsNonExpired();//凭证(密码)是否过期
boolean isEnabled();//是否可用
}
继续观察刚才AbstractUserDetailsAuthenticationProvider抽象类的authenticate认证方法中的省略代码,有三个重要的检验用户登录方法和一个检验成功的回调方法:
//。。。。。。。。。。。接上
try {
//preAuthenticationChecks.check()方法:检验3个boolean方法
this.preAuthenticationChecks.check(user);
//additionalAuthenticationChecks():用于检测账号和密码(可以点进去查看)
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException ex) {
if (!cacheWasUsed) {
throw ex;
}
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
//postAuthenticationChecks():检验1个Boolean
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
//如果上面的检查都通过并且没有异常,表示认证通过,会调用下面的方法:
return createSuccessAuthentication(principalToReturn, authentication, user);
}
preAuthenticationChecks预检查,在最下面的内部类DefaultPreAuthenticationChecks中可以看到,它会检查上面提到的三个boolean方法,即检查账户未锁定、账户可用、账户未过期,如果上面的方法只要有一个返回false,就会抛出异常,那么认证就会失败。下面还有个postAuthenticationChecks.check(user)后检查,在最下面的DefaultPostAuthenticationChecks内部类中可以看到,它会检查密码未过期,如果为false就会抛出异常
@Override
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"));
}
}
@Override
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger
.debug("Failed to authenticate since user account credentials have expired");
throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired",
"User credentials have expired"));
}
}
additionalAuthenticationChecks() 是附加检查,允许子类对UserDetails给定的身份验证请求执行返回(或缓存)返回的任何其他检查,它主要是检查用户密码的正确性,如果密码为空或者错误都会抛出异常。通常,子类至少会Authentication.getCredentials()与 进行比较UserDetails.getPassword()。如果需要自定义逻辑来比较UserDetails和/或的 其他属性UsernamePasswordAuthenticationToken,则这些属性也应出现在此方法中。
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
createSuccessAuthentication() 如果全部检验通过,创建一个成功的Authentication对象,查看该方法的方法体,发现是通过构造方法实例化对象UsernamePasswordAuthenticationToken时,调用的是三个参数的构造方法,返回一个成功的认证令牌。
三个参数说明:
UsernamePasswordAuthenticationToken继承AbstractAuthenticationToken实现Authentication
所以当在页面中输入用户名和密码之后首先会进入到UsernamePasswordAuthenticationToken验证(Authentication),然后生成的Authentication会被交由AuthenticationManager来进行管理而AuthenticationManager管理一系列的AuthenticationProvider,而每一个Provider都会通–UserDetailsService和UserDetail来返回一个以UsernamePasswordAuthenticationToken实现的带用户名和密码以及权限的Authentication。
在下面的代码中通过new出一个新UsernamePasswordAuthenticationToken 的对象并使用setDetails(authentication.getDetails()):向新的对象设置authentication对象的有关身份验证请求的其他详细信息,这些可能是IP地址,证书序列号等。到这里就表示认证通过了。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal,
authentication.getCredentials(),
this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
this.logger.debug("Authenticated user");
return result;
}
下面是获取用户信息UserDetails的retrieveUser方法
它是调用了getUserDetailsService先获取到UserDetailsService对象,通过调用UserDetailsService对象的loadUserByUsername方法根据对应的username用户名来获取用户信息UserDetails
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//获取用户信息UserDetails
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
//代码省略
//。。。。
//。。。。。
找到UserDetailsService,发现它是一个接口,查看继承关系,有很多实现,都是spring-security提供的实现类,并不满足我们的需要,我们想自己制定获取用户信息的逻辑,所以我们可以实现这个接口。比如从我们的数据库中查找用户信息
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
覆盖安全框架的默认登录, 只要继承 UserDetailsService接口并被容器监控到,就可以覆盖掉默认方法。下面为自定义登录的实现类。
@Service
@Slf4j
public class UserDeatailServiceImpl implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("登入的用户:{}",s);
Users users=new Users();
users.setUsername(s);
Users loginUser = usersMapper.selectOne(users);
if (loginUser==null){
throw new PassPortException("用户名或者密码不匹配");
}
//默认全部为true保证通过
return new User(loginUser.getUsername(), loginUser.getPassword(),
true, true, true, true,
//身份权限名可以自定义,按照这里写比较规范
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_ADMIN"));
}
}
如果成功,那么使用认证成功处理器处理,并且将登陆成功的依据交给rememberMeService。
如果失败(登录流程一次)调用登录失败处理器,并且清空securityContext,再调用rememberMeService的loginfail
接下来看它的源代码
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
//第三种情况:返回Null,表示身份验证过程未完成。
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//UsernamePasswordAuthenticationFilter类的attemptAuthentication方法继承了AbstractAuthenticationProcessingFilter类
//返回一个经过身份验证的用户令牌;如果身份验证不完整,则返回null。
//或是 抛出异常AuthenticationException:表示身份验证失败。
Authentication authenticationResult = attemptAuthentication(request, response);
//第三种情况:返回Null,表示身份验证过程未完成。
if (authenticationResult == null) {
return;
}
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//第一种情况:一个认证返回的对象,调用登录成功的逻辑方法
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
//第二种情况:验证中出现一次,调用登录失败的逻辑的方法
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
//第二种情况:验证中出现一次,调用登录失败的逻辑的方法
unsuccessfulAuthentication(request, response, ex);
}
}
//登录成功的逻辑方法
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
//从当前执行的线程中获取对象信息并更改当前已认证的主体
SecurityContextHolder.getContext().setAuthentication(authResult);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
}
//将登陆成功的依据交给rememberMeService
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
//成功处理器,可以自定义
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
//登录失败的逻辑方法
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
//清空securityContext
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
//记住我的功能 表示保存失败
this.rememberMeServices.loginFail(request, response);
//失败处理器,可以自定义
this.failureHandler.onAuthenticationFailure(request, response, failed);
}