本篇中加入了以下三个功能:
- 修改用户名密码的参数名称
- 通过自定义一个AuthenticationProvider在系统中加入一个后门
- 将验证身份信息展示到前端
1.principal:身份 credentials:凭证 AuthenticationManager:身份验证管理类(它是验证管理类的总接口)ProviderManager类:(负责具体的验证管理)
Spring Security主要包括两大功能:验证和鉴权。验证就是确认用户的身份,一般采用用户名和密码的形式;鉴权就是确认用户拥有的身份(角色、权限)能否访问受保护的资源。
2.原理图
3.参与验证的要素
Spring Security不仅仅支持网页登录这一种验证模式,它还支持多种其他验证模式。但是其参与验证的要素基本上还是用户、密码、角色、受保护的资源这四种。
用户名称一般在前端由访问者填入,而系统用户在后端一般存储在内存中或数据库中。
密码一般在前端由访问者填入,用于验证用户身份,在Security 5.0后密码必须使用PasswordEncoder加密,一般来说使用BCryptPasswordEncoder即可。
角色,又可以被看做权限,在Spring Security中,有时用Role表示,有时用Authority表示。前端一般看不到系统的角色。后端角色由管理者赋予给用户(可以事先赋予,也可以动态赋予),角色信息一般存储在内存或数据库中。
受保护的资源,一般来说就是指网址,有时也可以将某些函数方法定义为资源,但本文不涉及这类情况.
参与验证的要素(用户名、密码)在前端由表单提交,由网络传入后端后,会形成一个Authentication类的实例。该实例在进行验证前,携带了用户名、密码等信息;在验证成功后,则携带了身份信息、角色等信息。Authentication接口代码节选如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
其中getCredentials()返回一个Object credentials,它代表验证凭据,即密码;getPrincipal()返回一个Object principal,它代表身份信息,即用户名等;getAuthorities()返回一个Collection<? extends GrantedAuthority>,它代表一组已经分发的权限,即本次验证的角色(本文中权限和角色可以通用)集合。
有了Authentication实例,则验证流程主要围绕这个实例来完成。它会依次穿过整个验证链,并存储在SecurityContextHolder中。也可以像本文中的代码一样,在验证途中伪造一个Authentication实例,骗过验证流程,获得所有权限。
4.验证的规则
验证规则定义了以下几个东西:
- 受保护的资源即网址,它们一般按访问所需权限分为几类
- 哪一类资源可以由哪些角色访问
- 规则定义在WebSecurityConfigurerAdapter的子类中
具体规则的定义方法可以在代码中观察
5.验证流程
前文已经介绍了Authentication类,它代表了验证信息。
再介绍一个类AuthenticationManager,它是验证管理类的总接口;而具体的验证管理需要ProviderManager类,它具有一个List<AuthenticationProvider> providers属性,这实际上是一个AuthenticationProvider实例构成的验证链。链上都是各种AuthenticationProvider实例,这些实例进行具体的验证工作,它们之间的关系如下图(图来自互联网)所示:
验证成功后,验证实例Authentication会被存入SecurityContextHolder中,而它则利用线程本地存储TLS功能。在验证成功且验证未过期的时间段内,验证会一直有效。而且,可以在需要的地方,从SecurityContextHolder中取出验证信息,并进行操作。例如将验证信息展示在前端。
具体的验证流程如下:
- 后端从前端的表单得到用户密码,包装成一个Authentication类的对象;
- 将Authentication对象传给“验证管理器”ProviderManager进行验证;
- ProviderManager在一条链上依次调用AuthenticationProvider进行验证;
- 验证成功则返回一个封装了权限信息的Authentication对象(即对象的Collection<? extends GrantedAuthority>属性被赋值);
- 将此对象放入安全上下文SecurityContext中;
- 需要时,可以将Authentication对象从SecurityContextHolder上下文中取出。
注意,在ProviderManager管理的验证链上,任何一个AuthenticationProvider通过了验证,则验证成功。所以,要在系统中留一个后门,只需要在代码中添加一个AuthenticationProvider的子类BackdoorAuthenticationProvider,并在输入特定的用户名(alex)时,直接伪造一个验证成功的Authentication,即可通过验证,代码如下:
@Component
public class BackdoorAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String name = authentication.getName();
String password = authentication.getCredentials().toString();
//利用alex用户名登录,不管密码是什么都可以,伪装成admin用户
if (name.equals("alex")) {
Collection<GrantedAuthority> authorityCollection = new ArrayList<>();
authorityCollection.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
authorityCollection.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(
"admin", password, authorityCollection);
} else {
return null;
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(
UsernamePasswordAuthenticationToken.class);
}
}
然后在SecurityConfiguration类中,将BackdoorAuthenticationProvider的实例加入到验证链中即可,代码如下:
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
BackdoorAuthenticationProvider backdoorAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
...省略
//将自定义验证类注册进去
auth.authenticationProvider(backdoorAuthenticationProvider);
}
...省略
}
6.将验证身份信息展示到前端
前面已经提到,验证成功后,验证信息存入SecurityContextHolder中。因此可以在需要的地方,将其提取出来,然后在前端展示出来。
后端代码:
@Controller
public class UserController {
@RequestMapping("/user")
public String user(@AuthenticationPrincipal Principal principal, Model model) {
model.addAttribute("username", principal.getName());
//从SecurityContextHolder中得到Authentication对象,进而获取权限列表,传到前端
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Collection<GrantedAuthority> authorityCollection = (Collection<GrantedAuthority>) auth.getAuthorities();
model.addAttribute("authorities", authorityCollection.toString());
return "user/user";
}
@RequestMapping("/admin")
public String admin(@AuthenticationPrincipal Principal principal, Model model) {
model.addAttribute("username", principal.getName());
//从SecurityContextHolder中得到Authentication对象,进而获取权限列表,传到前端
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Collection<GrantedAuthority> authorityCollection = (Collection<GrantedAuthority>) auth.getAuthorities();
model.addAttribute("authorities", authorityCollection.toString());
return "admin/admin";
}
}