springsecurity 定制用户认证
如果定制spring security的用户认证,方法有很多,在T31项目中,我们定义了一个UserDetailServiceImpl即完成了定制逻辑,为什么这个起到了效果。通过深入了解spring security的认证逻辑来解密。
一、spring security 认证类图
1.1 认证管理器AuthenticationManager
认证过程由认证管理器AuthenticationManager来总负责,所以我们可以看到在spring security的安全配置类中,专门针对它有配置方法:
// 配置用户认证器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userDetailService)
.passwordEncoder(passwordEncoder());
}
从类图中看出,ProviderManager实现了认证管理器。从组合关系看出这个管理器就是收集了一堆认证器,一旦来了认证请求,看那个认证器能搞,就让哪个认证器去搞。从ProviderManager源码来看一下Authenticate中核心部分:
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
1.2 认证器AuthenticationProvider
认证器一般是成对出现的,一个证件类型Authentication对应能够处理的认证器AuthenticationProvider。
有很多认证器,我们只关注一种,就是通过用户名密码来认证的这一分支。
首先这个AbstractUserDetailsAuthenticationProvider定义了认证过程,我们看一下这类的Authenticate方法(省掉cache部分):
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
postAuthenticationChecks.check(user);
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}
核心代码就四步:
- 获取用户信息 retrieveUser(交给子类实现)
- 前置检查
- 核心校验逻辑(子类中实现)
- 后置检查
1.3 真正干活的DaoAuthenticationProvider
DaoAuthenticationProvider认证器负责去拿用户信息,然后校验用户名密码。
1. 获取用户信息
获取用户信息,就是借助于UserDetailsService来获取:
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
return loadedUser;
}
2. 验证用户名密码
通过代码,看出是通过passwordEncoder来校验用户名和密码的。
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");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
二、定制实战
根据分析结果,其实我们只需要定制一下获取用户信息的逻辑即可,在具体实现中,我们通过OpenFeigh调用admin-server的服务,来获取用户信息。
用户信息包括用户的基本信息(用户名角色)和授权信息(角色、权限)。
2.1 配置认证管理器
配置认证管理器,注入新的获取用户信息的实现类。
// 配置用户认证器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.userDetailService)
.passwordEncoder(passwordEncoder());
}
2.2 定义实现类
public class UserDetailServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserDetailServiceImpl.class);
@Autowired
UserClient userClient;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDTO user = userClient.getByUsername(username);
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
if (user != null) {
logger.debug("current user = " + user);
//获取⽤户的授权
List<String> roles = userClient.listRolesByUserId(user.getId());
//声明授权⽂件
for (String role : roles) {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_" + role);
//spring Security中权限名称 必须满⾜ROLE_XXX
grantedAuthorities.add(grantedAuthority);
}
}
logger.debug("granted authorities = " + grantedAuthorities);
return new User(user.getUsername(), user.getPassword(), grantedAuthorities);
}
}
至此,完成理论和实战的过程。