简介
在计算机科学中,二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。
JavaScript(通常缩写为 JS )是一种高级的、解释型的编程语言[5]。JavaScript 是一门基于原型、函数先行的语言[6],是一门多范式的语言,它支持面向对象编程,命令式编程,以及函数式编程。它提供语法来操控文本、数组、日期以及正则表达式等,不支持 I/O ,比如网络、存储和图形等,但这些都可以由它的宿主环境提供支持。它已经由 ECMA(欧洲计算机制造商协会)通过 ECMAScript 实现语言的标准化[5]。它被世界上的绝大多数网站所使用,也被世界主流浏览器( Chrome、IE、Firefox、Safari、Opera )支持。
虽然 JavaScript 与 Java 这门语言不管是在名字上,或是在语法上都有很多相似性,但这两门编程语言从设计之初就有很大的不同,JavaScript 的语言设计主要受到了 Self(一种基于原型的编程语言)和 Scheme(一门函数式编程语言)的影响[6]。在语法结构上它又与C语言有很多相似(例如 if 条件语句、while 循环、switch 语句、do-while 循环等)[7]。
在客户端,JavaScript 在传统意义上被实现为一种解释语言,但现在它已经可以被即时编译( JIT )执行。随着最新的 HTML5 和 CSS3 语言标准的推行它还可用于游戏、桌面和移动应用程序的开发和在服务器端网络环境运行,如 Node.js 。
在本系列文章(一)初识 Spring Security 中,我们不仅初步认识了 Spring Security,明晰了 Spring Security 的三个核心概念——认证、鉴权、防护,还准备研究其工作原理。
现在,就跟随本篇文章来探讨认证与授权是如何实现的吧。
在 Security 中,认证和授权功能是交由过滤器UsernamePasswordAuthenticationFilter来完成的。
那么,UsernamePasswordAuthenticationFilter又是如何运作的呢?
我们直接去阅读源码,最终将得到简化的认证授权流程信息:
UsernamePasswordAuthenticationFilter执行attemptAuthentication方法将提交的表单信息封装为一个UsernamePasswordAuthenticationToken对象,传递给AuthenticationManager执行authenticate认证方法AuthenticationManager接口通过多态选择默认的实现类ProviderManager去执行authenticate方法ProviderManager在执行该方法时会遍历AuthenticationProvider列表,通过多态选择实现类AbstractUserDetailsAuthenticationProvider和DaoAuthenticationProvider去执行认证方法DaoAuthenticationProvider在retrieveUser方法中会从UserDetailsService的实现类的loadUserByUsername得到UserDetails对象UserDetails对象的合理性之后,创建一个认证成功的存放了权限信息的Authentication对象,用作后续的鉴权现在你肯定看的一头雾水,因为你并不知道上面流程中的各个接口和类的作用是什么,不过没关系,下面我们将一步一步地分析它们。
在认证流程的第 ① 步中会将表单信息封装UsernamePasswordAuthenticationToken该对象,这发生在UsernamePasswordAuthenticationFilter的attemptAuthentication方法中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 1.获取表单中的用户名和密码
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
// 2.封装为一个对象
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
`
这个对象是干啥的呢?我们看下源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
...
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
...
}
public abstract class AbstractAuthenticationToken implements Authentication,
CredentialsContainer {
// 权限集合
private final Collection<GrantedAuthority> authorities;
private Object details;
private boolean authenticated = false;
...
}
从源码中得知:
principal:等同于usernamecredentials:等同于passwordauthorities:继承的父类权限集合暂时为空 构造完UsernamePasswordAuthenticationToken对象后应当做些什么?
那肯定是查看数据库是否存在与UsernamePasswordAuthenticationToken对象匹配的用户名,没有肯定就认证失败了嘛!
认证具体是交由AuthenticationManager接口的实现类ProviderManager去完成的,我们来看下源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// 认证提供者列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public List<AuthenticationProvider> getProviders() {
return providers;
}
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
...
try {
// 根据不同的认证提供者进行认证
result = provider.authenticate(authentication);
// 只要有一个认证提供者认证成功,就算认证成功,跳出循环
if (result != null) {
copyDetails(authentication, result);
break;
}
...
} catch {
...
}
}
if (result != null) {
...
return result;
}
}
ProviderManager维护了一个 List,该 List 中存放了AuthenticationProvider接口不同的实现类,认证时需遍历所有的实现类进行认证,只要有一个AuthenticationProvider认证成功,就算认证成功。
一般情况下,是由AuthenticationProvider接口的实现类DaoAuthenticationProvider根据用户名从数据源中获取用户信息对象的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17protected 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;
} catch (){
...
}
}
获取到UserDetails对象后,就是密码的比对了,由于注册时密码应当会以特定算法加密密码用密文进行保存,所以解密时也需要先通过对应的加密算法加密为密文,与获取的密文进行匹配,匹配成功,则认证成功。
以上过程由additionalAuthenticationChecks方法完成:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
// 表单未输密码抛异常
throw new BadCredentialsException(...);
}
String presentedPassword = authentication.getCredentials().toString();
// 将表单密码加密后与数据源获取的密文进行匹配
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
// 不匹配抛异常
throw new BadCredentialsException(...);
}
}
在前面的分析过程中,我们发现ProviderManager类中维护了一个AuthenticationProvider列表,为什么需要这个列表呢?
定义AuthenticationProvider接口的目的在于认证方式可能不同,需要子类实现,默认情况下的认证方式实现类为DaoAuthenticationProvider。
emmm,认证方式,什么意思?
我们知道现在表单使用的是用户名/密码的信息,所以我们数据源查询应当是根据用户名查询出用户对象,但如果产品提了需求说再加一种认证方法,使用电话和验证码的方式进行认证,那现在的认证方式不就不符合业务需要了嘛?
此时,我们可以自定义一个AuthenticationProvider的实现类,重写业务逻辑以匹配业务场景。
因此,总结来讲就是会存在五花八门的认证方式:
因此,若默认的用户名/密码认证方式不符合业务需要,可以随业务进行扩展。
不得不提一嘴,若没有任何配置,Spring Security 根据用户名查询用户对象的数据源是从默认的内存中读取的,这自然不符合正常场景,我们的用户信息都是保存到数据库的,那么如何进行修改以从数据库中查询用户对象呢?
从前面知道,根据用户名获取用户信息的过程是由前面的DaoAuthenticationProvider中调用的retrieveUser方法的以下方法实现:1
2// 从数据源中根据用户名获取 UserDetails 对象
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
我们从源码来看看这个UserDetailsService到底是什么:1
2
3public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
很明显,它是一个接口,默认根据用户名读取用户信息的实现类InMemoryUserDetailsManager重写了它的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public class InMemoryUserDetailsManager implements UserDetailsManager,
UserDetailsPasswordService {
......
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());
}
......
}
那么,我们现在想走数据库去查询用户信息,肯定就需要实现该接口,重写其方法,从数据库读取用户信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserMapper userMapper;
/**
* Spring Security 认证授权均通过 UserDetailsService 接口提供的 loadUserByUsername 方法,
* 该方法返回 UserDetails 对象,该对象包含了用户角色权限信息,之后 url 权限校验需要使用
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> userWrapper = new QueryWrapper<>();
userWrapper.eq("username", username);
User user = userMapper.selectOne(userWrapper);
if (user == null) {
throw new UsernameNotFoundException(username);
}
Set<String> permissionSet = userMapper.selectPermissionListByUsername(username);
return new SecurityUser(user.getUsername(), user.getPassword(), permissionSet,
true, true, true, true);
}
}
定义完成以后,还需要将默认的实现类进行替换,直接注入即可:
1 | @EnableWebSecurity |
不知道你有没有注意到一点,loadUserByUsername方法的返回值为UserDetails接口,其源码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public interface UserDetails extends Serializable {
// 权限集合
Collection<? extends GrantedAuthority> getAuthorities();
// 获取密码
String getPassword();
// 获取用户名
String getUsername();
// 帐号未过期
boolean isAccountNonExpired();
// 帐号未锁定
boolean isAccountNonLocked();
// 凭证未过期
boolean isCredentialsNonExpired();
// 帐号可用
boolean isEnabled();
}
咦,这方法不是应该从数据库查询出用户对象嘛?为什么返回这个接口呢?什么意思?
emmm,这方法准确而言,应当从数据库查询出一个包含角色和权限的用户对象,因为后续鉴权需要使用到,并且由于 Spring Security 后续鉴权都是通过UserDetails接口多态进行判断的,所以我们最好创建一个对象实现该接口,然后根据数据库信息填充该对象,不然是不符合 Security 的要求滴!
前面说过,数据库的密码是加密后的密文形式,所以比对时需要将表单的密码进行加密,之后才能判断它们是否相等,表单密码的加密就需要用到PasswordEncoder了:1
2
3
4
5
6
7if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}