写在最前面的话
如果想看Security的源码,那么先请看设计模式,这个框架简直就是《大神教你正确使用设计模式的N种姿势》,实际编程如果允许的情况下请用Shiro,而且文章不会对你的编程技巧有所提高,看完只会感觉需要定制自己的逻辑是需要多么的繁琐。
参考资料:
Spring Security Architecture
Spring Security源码分析一:Spring Security认证过程
Spring-Security源码分析-AuthenticationManagerBuilder
Security验证器
1.AuthenticationManagerBuilder继承图
在Security体系中由AuthenticationManager负责对传递进来的用户信息(Security称为AuthenticationToken)和从数据源读取的用户信息(Security称为UserDetails)进行对比验证,而AuthenticationManager是由AuthenticationManagerBuilder负责构建。
看到这里,这是一个典型的建造者模式
AuthenticationConfiguration作为指挥者负责配置和创建Global AuthenticationManagerBuilder,其中会调用AuthenticationManagerBuilder的build()函数构建出AuthenticationManager。
2.AuthenticationManagerBuilder的build()过程
这个图看上去很复杂,但是功能很简单,由AuthenticationManagerBuilder构建出AuthenticationManager,其中会将AuthenticationProviders绑定到AuthenticationManager,那么AuthenticationProviders的作用就是具体从数据源读取UserDetails并和AuthenticationToken的对比过程的代码实现。
注意一点:由AuthenticationConfiguration负责配置是全局AuthenticationManagerBuilder,全局的意思是这个AuthenticationManagerBuilder是放在ApplicationContext容器里。
3.AuthenticationManager验证核心逻辑
这个图看上去又很复杂,逻辑很简单,用户输入的token,交给ProviderManager,然后通过其中的Provider List逐一验证,验证的时候,从数据源加载注册的用户信息,对比成功就返回一个受认证的Token
ProviderManager.class(已简化)
// AuthenticationManager的实现类,具体的验证流程
public Authentication authenticate(Authentication authentication) {
// Token会被传入每一个Provider,由Provider对其验证
for (AuthenticationProvider provider : getProviders()) {
// Provider首先会对Token类型进行匹配,如果不匹配则直接调用下一个Provider
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
if (result == null && parent != null) {
// 调用父类的AuthenticationManager再次验证
try {
result = parent.authenticate(authentication);
}
}
}
稍微说明一下
Authentication:可以看做是用户输入的username和password的封装Token
Provider:提供Token和UserDetails对比的认证逻辑
UserDetailsService(下文会说到):从数据源读取用户信息并封装成UserDetails
4.短信验证码校验例子
例子很简单,假定短信码已经生成放入缓存并且发送给用户,这里只是演示,verifyCode为空字符串
1.获取用户输入手机号mobile和短信码smsCode
2.获取随机生成的短信码verifyCode
3.从数据源获取用户信息并封装成UserDetails
4.生成成功验证的SmsCodeAuthenticationToken
SmsCodeAuthenticationProvider.class
@Slf4j
public final class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Getter @Setter private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
// 获取用户输入的手机号和验证码
String mobile = (String) token.getPrincipal();
String smsCode = (String) token.getCredentials();
// 从缓存加载已发送的验证码
String verifyCode = "";
// 校验错误则抛出异常
if (!verifyCode.equals(smsCode)) {
throw new InternalAuthenticationServiceException("SMS Code Is Not Equal");
}
// 从数据源加载用户数据
UserDetails userDetails = getUserDetailsService().loadUserByUsername(mobile);
// 生成验证成功的token
SmsCodeAuthenticationToken authenticatedToken =
new SmsCodeAuthenticationToken(
userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
// authenticated token details
authenticatedToken.setDetails(authentication.getDetails());
return authenticatedToken;
}
/** provider只支持SmsCodeAuthenticationToken 类型的校验 */
@Override
public boolean supports(Class> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
SmsUserDetailsServiceImpl.class
@Slf4j
@Service(value = "smsUserDetailsServiceImpl")
public class SmsUserDetailsServiceImpl implements UserDetailsService {
@Autowired private UserService userServiceImpl;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// load user from cache if necessary
// load user from database
User user = userServiceImpl.findUserRolePermiByName(username);
if (user == null) {
throw new UsernameNotFoundException("User Name " + username + " Not Found");
}
// extract permissions
List authorities =
user.getRoles().stream()
.map(item -> new SimpleGrantedAuthority(item.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(
user.getName(), user.getPasswd(), authorities);
}
}
SmsCodeAuthenticationToken.class
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 6279233409712693968L;
/** 手机号 */
@Getter private final Object principal;
/** 短信码 */
@Getter private Object credentials;
SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
SmsCodeAuthenticationToken(
Object principal, Object credentials, Collection extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
credentials = null;
}
}
编程需要做些什么?
看上面认证流程红色部分,对于Security认证添加自定义认证器需要编写的部分
1.Authentication:自定义一个需要被认证Token的类型
2.Provider:自定义一个验证器实例
3.UserDetailsService:自定义用户信息加载服务
然后将UserDetailsService注入到Provider,Provider注入到ProviderManager
1.为什么要自定义UserDetailService?
自带JDBC数据加载,默认会创建一张权限相关的表,但是我们自己有用户数据的表,表结构不一定是我们想要的,而且加载数据是通过JdbcTemplate完成的,但是我们自己加载数据通常通过Mybatis来操作,所以有时候并不太合适。
InMemory适合用在初期测试时用。
LDAP的服务我至少还没有用过。
2.为什么要自定义Provider和Token
当你使用默认的servlet拦截时,比如formLogin的拦截器,拦截之后生成的是UsernamePasswordAuthenticationToken类型的,而使用Provider对Token做验证时是和Token类型匹配的,有时候我们想区分登录类型,或者区别Token类型来从不同的数据源加载用户数据。
当你自定义的UserDetailService作为Bean放到容器里的时候,AuthenticationConfiguration会把它注入到DaoAuthenticationProvider,而DaoAuthenticationProvider默认只支持UsernamePasswordAuthenticationToken类型
下节预告
目前先记住Security的认证流程的大概流程,还没有解释Authentication这个Token是哪里来的,下一篇解释Security对sevrlet的拦截再讲这个问题。