问题:Ironrhino用户登录中的密码校验流程、OAuth认证、token校验实现过程
用户登录认证过程总结:
- 用户使用用户名和密码进行登录;
- Spring Security 将获取到的用户名和密码封装成一个实现了
Authentication
接口的UsernamePasswordAuthenticationToken
; - 将上述产生的
token
对象传递给AuthenticationManager
进行登录认证; -
AuthenticationManager
认证成功后将会返回一个封装了用户权限等信息的Authentication
对象; - 通过调用
SecurityContextHolder.getContext().setAuthentication(...)
将AuthenticationManager
返回的Authentication
对象赋予给当前的SecurityContext
。
Spring Security已经实现了用户的认证和授权功能,铁犀牛在此基础上,扩展了动态验证码登录的方式。
代码1显示了在用户输入用户名和密码点击登录后的过程,UsernamePasswordAuthenticationToken
会对用户名和密码进行封装,然后传递到AuthenticationManager
的方法authenticate
中。AuthenticationManager
是一个用来处理认证(Authentication)请求的接口。在其中只定义了一个方法 authenticate()
,该方法只接收一个代表认证请求的 Authentication
对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication
对象进行返回。
代码1: LoginAction.java
public class LoginAction extends BaseAction {
...
public String execute() throws Exception {
...
...
UsernamePasswordAuthenticationToken attempt = new UsernamePasswordAuthenticationToken(username, password);
WebAuthenticationDetailsSource wads = ReflectionUtils.getFieldValue(usernamePasswordAuthenticationFilter,
"authenticationDetailsSource");
attempt.setDetails(wads.buildDetails(request));
authResult = authenticationManager.authenticate(attempt);
...
}
}
在 Spring Security 中,AuthenticationManager
的默认实现是 ProviderManager
,代码2展示的是ProviderManager
部分代码。
它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider
列表,然后会依次使用每一个 AuthenticationProvider
进行认证,如果有一个 AuthenticationProvider
认证后的结果不为 null
,则表示该 AuthenticationProvider
已经认证成功,之后的 AuthenticationProvider
将不再继续认证。然后直接以该 AuthenticationProvider
的认证结果作为ProviderManager
的认证结果。如果所有的 AuthenticationProvider
的认证结果都为 null
,则表示认证失败,将抛出一个 ProviderNotFoundException
。
代码2: ProviderManager.java
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
...
private List<AuthenticationProvider> providers;
...
...
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
...
Iterator e = this.getProviders().iterator();
while (e.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider) e.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (AccountStatusException arg10) {
this.prepareException(arg10, authentication);
throw arg10;
} catch (InternalAuthenticationServiceException arg11) {
this.prepareException(arg11, authentication);
throw arg11;
} catch (AuthenticationException arg12) {
lastException = arg12;
}
}
}
...
if (result != null) {
...
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(
this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()},
"No AuthenticationProvider found for {0}"));
}
this.prepareException((AuthenticationException) lastException, authentication);
throw lastException;
}
}
...
}
AbstractUserDetailsAuthenticationProvider
实现了AuthenticationProvider
接口,在authenticate
方法中首先会根据username
去加载对应的UserDetails
实例(UserDetails
接口包含了用户的信息,包括用户名、密码、权限、是否启用、是否被锁定、是否过期等);然后运行到this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication)
,将UserDetails
实例中的密码与UsernamePasswordAuthenticationToken
用户输入认证密码进行对比,可继续查看AbstractUserDetailsAuthenticationProvider
子类DaoAuthenticationProvider
。
代码3: AbstractUserDetailsAuthenticationProvider.java
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
...
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
} catch (UsernameNotFoundException arg5) {
this.logger.debug("User \'" + username + "\' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw arg5;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
...
}
...
}
DaoAuthenticationProvider
是ProviderManager
中AuthenticationProvider
接口的默认实现类,而DaoAuthenticationProvider
也是不直接操作数据库的,它把任务委托给了UserDetailService
;前面在父类AbstractUserDetailsAuthenticationProvider
中,authenticate()
方法是先加载UserDetails
实例,这部分在retrieveUser()
方法中实现,可以从代码4中看到UserDetails
是通过 UserDetailsService
的 loadUserByUsername()
方法进行加载的,Spring Security 提供了UserDetailsService
的默认实现类JdbcDaoImpl
来支持从数据库中加载用户信息。
在我们自己的项目中只需要实现UserDetailsService
接口,并重写loadUserByUsername()
方法返回一个UserDetails
对象。
代码4: DaoAuthenticationProvider.java
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
...
private PasswordEncoder passwordEncoder;
private UserDetailsService userDetailsService;
...
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
...
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails ex = this.getUserDetailsService().loadUserByUsername(username);
if (ex == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
} else {
return ex;
}
} catch (UsernameNotFoundException arg3) {
this.mitigateAgainstTimingAttack(authentication);
throw arg3;
} catch (InternalAuthenticationServiceException arg4) {
throw arg4;
} catch (Exception arg5) {
throw new InternalAuthenticationServiceException(arg5.getMessage(), arg5);
}
}
...
}
继续查看additionalAuthenticationChecks()
方法中的代码,最终将两个密码的对比进入了PasswordEncoder
的matches()
方法中。
铁犀牛框架将PasswordEncoder
默认实现修改为org.ironrhino.core.spring.security.password.MixedPasswordEncoder
。
<bean id="passwordEncoder" class="org.ironrhino.core.spring.security.password.MixedPasswordEncoder" />
MixedPasswordEncoder
提供了多种加密方式,根据密码的长度进行判断。
matches(...)
方法的返回结果就是认证结果,在这里完成了最终的密码校验。
代码5: MixedPasswordEncoder.java
public class MixedPasswordEncoder implements PasswordEncoder {
...
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null || encodedPassword == null)
return false;
switch (encodedPassword.length()) {
case 32:
return matches(Hex.decode(encodedPassword), legacyDigest(rawPassword.toString()));
case 48:
byte[] digested = Hex.decode(encodedPassword);
byte[] salt = EncodingUtils.subArray(digested, 0, saltGenerator.getKeyLength());
return matches(digested, digest(rawPassword, salt, true));
case 80:
digested = Hex.decode(encodedPassword);
salt = EncodingUtils.subArray(digested, 0, saltGenerator.getKeyLength());
return matches(digested, digest(rawPassword, salt, false));
default:
return false;
}
}
...
}
验证码登录
在铁犀牛框架中,扩展了动态验证码验证的功能。新建类VerificationAuthenticationProvider
继承了DaoAuthenticationProvider
,并且重写了additionalAuthenticationChecks()
方法。
在重写的additionalAuthenticationChecks()
方法中先判断UserDetails
对象是否实现了VerificationAware
接口,若符合则进入验证码的对比逻辑,否则使用Spring Security默认实现(代码4)。
在我们的项目中,若要使用验证码登录,则需要定义的用户类实现VerificationAware
接口。例如:
- 在appserver中
Customer
类同时实现了UserDetails
和VerificationAware
,因此在additionalAuthenticationChecks
中会进入验证码的流程; - 而marketserver中
Employee
只实现了UserDetails
接口,因此在additionalAuthenticationChecks
中会默认使用密码验证;
代码6: VerificationAuthenticationProvider.java
public class VerificationAuthenticationProvider extends DaoAuthenticationProvider {
private VerificationManager verificationManager;
private AuthenticationManager authenticationManager;
...
@PostConstruct
public void init() {
List<AuthenticationProvider> providers = ((ProviderManager) authenticationManager).getProviders();
Iterator<AuthenticationProvider> it = providers.iterator();
while (it.hasNext()) {
if (it.next() instanceof DaoAuthenticationProvider) {
it.remove();
break;
}
}
providers.add(0, this);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (userDetails instanceof VerificationAware) {
VerificationAware user = (VerificationAware) userDetails;
if (user.isVerificationRequired()) {
verificationManager.verify(user);
if (verificationCodeQualified && !user.isPasswordRequired())
return; // skip check password
}
}
super.additionalAuthenticationChecks(userDetails, authentication);
}
}
从代码6可以看到,当使用验证码的方式时,会将VerificationAware
对象传递给VerificationManager
的verify()
方法,在铁犀牛框架中,DefaultVerificationManager
是VerificationManager
的默认实现类,
从代码7可以看到,DefaultVerificationManager
的send()
方法主要是对UserDetails
对象做了一些条件判断,若满足则调用VerificationService
接口的send()
方法;verify()
方法则是将用户输入的验证码传给VerificationService
接口的verify()
方法,具体对验证码的管理便是在VerificationService
中实现的。
代码7: DefaultVerificationManager.java
public class DefaultVerificationManager implements VerificationManager {
...
@Override
public void send(String username) throws ReceiverNotFoundException {
UserDetails ud = userDetailsService.loadUserByUsername(username);
String receiver = null;
if (ud instanceof VerificationAware)
receiver = ((VerificationAware) ud).getReceiver();
if (StringUtils.isBlank(receiver))
throw new ReceiverNotFoundException("No receiver found for: " + ud.getUsername());
verificationService.send(receiver);
}
@Override
public void verify(VerificationAware user) throws WrongVerificationCodeException {
String username = user.getUsername();
String receiver = user.getReceiver();
if (StringUtils.isBlank(receiver))
throw new ReceiverNotFoundException("No receiver found for: " + username);
String verificationCode = RequestContext.getRequest().getParameter("verificationCode");
if (verificationCode == null)
verificationCode = RequestContext.getRequest().getParameter("password");
boolean verified = verificationService.verify(receiver, verificationCode);
if (!verified)
throw new WrongVerificationCodeException("Wrong verification code: " + verificationCode);
}
}
VerificationService
只有send()
和verify()
两个方法,分别是产生/缓存获取和对比验证码,而发送验证码的任务又委托给VerificationCodeNotifier
接口来实现,因此在我们的项目中只需要实现VerificationCodeNotifier
接口,重写send()
方法。
代码8是VerificationService
接口的默认实现类DefaultVerificationService
的代码。
通常在输入手机号和验证码进行认证之前,要先获取验证码,因此会先触发send()
方法;在send()
方法中,先是尝试从缓存中获取,这里有对重复发送的控制;若缓存中没有则产生验证码,并且存入缓存;最后将验证码通过VerificationCodeNotifier
接口的send()
方法发送出去。
verify
方法则直接将用户输入的验证码和缓存中取出相应的验证码进行对比,返回验证结果;同时有对验证次数的控制,当验证次数达到阈值后,会从缓存中删除验证码。
代码8: DefaultVerificationService.java
public class DefaultVerificationService implements VerificationService {
...
private CacheManager cacheManager;
private VerificationCodeGenerator verficationCodeGenerator;
private List<VerificationCodeNotifier> verificationCodeNotifiers;
...
public void send(String receiver, final String verficationCode) throws ReceiverNotFoundException {
String codeToSend;
if (verficationCode == null) {
codeToSend = (String) cacheManager.get(receiver, CACHE_NAMESPACE);
if (codeToSend != null && cacheManager.exists(receiver + SUFFIX_RESEND, CACHE_NAMESPACE)) {
log.warn("{} is trying resend within cooldown time", receiver);
return;
}
} else {
codeToSend = verficationCode;
cacheManager.put(receiver, codeToSend, expiry, TimeUnit.SECONDS, CACHE_NAMESPACE);
}
if (codeToSend == null || !reuse) {
codeToSend = verficationCodeGenerator.generator(receiver, length);
cacheManager.put(receiver, codeToSend, expiry, TimeUnit.SECONDS, CACHE_NAMESPACE);
}
for (VerificationCodeNotifier notifier : verificationCodeNotifiers)
notifier.send(receiver, codeToSend);
cacheManager.put(receiver + SUFFIX_RESEND, "", resendInterval, TimeUnit.SECONDS, CACHE_NAMESPACE);
}
public boolean verify(String receiver, String verificationCode) {
boolean verified = verificationCode != null
&& verificationCode.equals(cacheManager.get(receiver, CACHE_NAMESPACE));
if (!verified) {
throttleService.delay("verification:" + receiver, verifyInterval, TimeUnit.SECONDS, verifyInterval / 2);
Integer times = (Integer) cacheManager.get(receiver + SUFFIX_THRESHOLD, CACHE_NAMESPACE);
if (times == null)
times = 1;
else
times = times + 1;
if (times >= maxAttempts) {
cacheManager.delete(receiver, CACHE_NAMESPACE);
cacheManager.delete(receiver + SUFFIX_THRESHOLD, CACHE_NAMESPACE);
} else {
cacheManager.put(receiver + SUFFIX_THRESHOLD, times, expiry, TimeUnit.SECONDS, CACHE_NAMESPACE);
}
} else {
cacheManager.delete(receiver, CACHE_NAMESPACE);
cacheManager.delete(receiver + SUFFIX_THRESHOLD, CACHE_NAMESPACE);
}
return verified;
}
}
参考资料
token校验过程
- 所有的请求都会先经过
OAuthHandler
的handle()
方法,从url参数或请求头中获取到token
,然后将token
传递给OauthAuthorizationService
的get()
方法返回一个OAuthAuthorization
对象;
1.1. 在OauthAuthorizationService
的实现类OAuthAuthorizationServiceImpl
中,首先会委托OAuthAuthorizationProvider
去获取token信息(铁犀牛中未实现OAuthAuthorizationProvider
接口),然后再尝试通过OauthManager
获取;
1.2. 铁犀牛中默认使用DefaultOAuthManager
调用AuthorizationManager
从数据库中获取token信息; - 判断上一步中返回的
OAuthAuthorization
对象的状态,若符合则认证成功,再使用userDetailsService.loadUserByUsername(...)
根据用户名获取到对应的UserDetails
对象; - 判断用户的状态,满足要求则将
UserDetails
对象及权限信息封装到Authentication
对象中; - 通过调用
SecurityContextHolder.getContext().setAuthentication(...)
将Authentication
对象赋予给当前的SecurityContext
。
网友评论