本文参考:
Spring Security 架构与源码分析
spring security——基本介绍(一)
Spring Security requires a Java 8 or higher Runtime Environment.
当用户登录的时候,一般来说要校验用户名密码的正确性。还要查询此用户的权限信息。这些信息被保存在Authentication中。
以UsernamePasswordAuthenticationFilter的校验过程为例,如下图所示
用户名密码被放在authRequest 中,作为入参递给AuthenticationManager的方法authenticate(authRequest)。
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
Authentication有两个作用
1.AuthenticationManager的输入,提供用户提供的用于身份验证的凭据。在此场景中使用时,isAuthenticated()返回false
2.表示当前已验证的用户。当前的身份验证可以从SecurityContext中获得。
包含信息:
isAuthenticated() @return true if the token has been authenticated and the
AbstractSecurityInterceptor
does not need to present the token to theAuthenticationManager
again for re-authentication.
if (authentication.isAuthenticated() && !alwaysReauthenticate) {
return authentication;
}
// 否则
authentication = authenticationManager.authenticate(authentication);
我们从SecurityContextHolder中设置并获取身份认证信息Authentication。
Example 53. 手动设置身份验证信息
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
Example 54. 获取当前用户
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
org.springframework.security.core.context.SecurityContextHolder 是 SecurityContext的存放容器,默认使用ThreadLocal (MODE_THREADLOCAL)存储,意味SecurityContext在相同线程中的方法都可用。
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL;
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
……
}
成员变量 SecurityContextHolderStrategy strategy;
默认 ThreadLocalSecurityContextHolderStrategy
SecurityContextImpl实现接口SecurityContext 的两个方法,存放身份验证信息Authentication
一般在拦截器中会调用AuthenticationManager的authenticate方法。
AuthenticationManager is the API that defines how Spring Security’s Filters perform authentication. The Authentication that is returned is then set on the SecurityContextHolder by the controller (i.e. Spring Security’s Filterss) that invoked the AuthenticationManager.
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
authenticate()方法主要做三件事:
AuthenticationException
AuthenticationManager的默认实现是ProviderManager,它委托一组AuthenticationProvider实例来实现认证 this.getProviders().iterator();
ProviderManager包含一组AuthenticationProvider,执行authenticate时,遍历Providers,然后调用supports,如果支持,则执行遍历当前provider的authenticate方法,如果一个provider认证成功,则break,返回Authentication。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
if (result == null && parent != null) {
result = parentResult = parent.authenticate(authentication);
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
((CredentialsContainer) result).eraseCredentials();
}
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
}
从上面的代码可以看出, ProviderManager有一个可选parent,如果parent不为空,则调用parent.authenticate(authentication)
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
Multiple AuthenticationProviders can be injected into ProviderManager.
Each AuthenticationProvider performs a specific type of authentication. For example, DaoAuthenticationProvider supports username/password based authentication while JwtAuthenticationProvider supports authenticating a JWT token.
AuthenticationProvider有多种实现,大家最关注的通常是DaoAuthenticationProvider,继承于abstract class AbstractUserDetailsAuthenticationProvider,核心是通过UserDetails来实现认证,DaoAuthenticationProvider默认会自动加载,不用手动配。
public abstract class AbstractUserDetailsAuthenticationProvider implements
AuthenticationProvider, InitializingBean, MessageSourceAware
核心的authenticate 伪代码:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
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 = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
throw notFound;
}
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
抽象方法
protected Authentication createSuccessAuthentication(Object principal,
Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
}
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
}
}
所我们是从UserDetailsService.loadUserByUsername(username)来获取用户,
然后比较密码是否相同
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
默认四种实现,获取当前用户名的用户信息
可参考: Spring Security5 四种添加用户的方式
本文都是通过userDetailsService这种方式来添加用户
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(users()).passwordEncoder(passwordEncoder());
}
@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("$2a$10$6Ed5ZXTH9DNwKzeFU07Ks.jGr3beIjU3o6mi12Jgh4Rh2t0goLbqO")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("$2a$10$6Ed5ZXTH9DNwKzeFU07Ks.jGr3beIjU3o6mi12Jgh4Rh2t0goLbqO")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
用户信息被存放在final Map
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserDetails user = users.get(username.toLowerCase());
if (user == null) {
throw new UsernameNotFoundException(username);
}
return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), user.getAuthorities());
}
然后用loadUserByUsername获取
JdbcUserDetailsManager继承JdbcDaoImpl
@Bean
UserDetailsManager users() {
UserDetails user = User.builder()
.username("user")
.password("$2a$10$6Ed5ZXTH9DNwKzeFU07Ks.jGr3beIjU3o6mi12Jgh4Rh2t0goLbqO")
.roles("USER")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(datasource);
users.createUser(user);
return users;
}
users.createUser(user)会在数据库中创建这个用户和他的权限
public void createUser(final UserDetails user) {
validateUserDetails(user);
// 创建用户
getJdbcTemplate().update(createUserSql, ps -> {
ps.setString(1, user.getUsername());
ps.setString(2, user.getPassword());
ps.setBoolean(3, user.isEnabled());
int paramCount = ps.getParameterMetaData().getParameterCount();
if (paramCount > 3) {
//NOTE: acc_locked, acc_expired and creds_expired are also to be inserted
ps.setBoolean(4, !user.isAccountNonLocked());
ps.setBoolean(5, !user.isAccountNonExpired());
ps.setBoolean(6, !user.isCredentialsNonExpired());
}
});
if (getEnableAuthorities()) {
// 创建权限
insertUserAuthorities(user);
}
}
创建CustomUserDetailsService继承UserDetailsService
略
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder());
}
@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}
略
从名字可以看出AuthenticationManagerBuilder就是用来创建AuthenticationManager的
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(customUserDetailsService()).passwordEncoder(passwordEncoder());
}
public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(T userDetailsService) throws Exception {
this.defaultUserDetailsService = userDetailsService;
return (DaoAuthenticationConfigurer)this.apply(new DaoAuthenticationConfigurer(userDetailsService));
}
使用userDetailsService()添加用户时(假设CustomUserDetailsService)会返回
DaoAuthenticationConfigurer
public interface ProviderManagerBuilder<B extends ProviderManagerBuilder<B>> extends SecurityBuilder<AuthenticationManager> {
B authenticationProvider(AuthenticationProvider var1);
}
AuthenticationManagerBuilder的实现
public AuthenticationManagerBuilder authenticationProvider(AuthenticationProvider authenticationProvider) {
this.authenticationProviders.add(authenticationProvider);
return this;
}