Spring Security的基本介绍
在讲解Spring Security 之前,我们先来了解一下认证和授权的这两个基本概念。所谓认证(Authentication), 通俗地来说,就是搞清楚你是谁(who are you?); 而授权(Authorization), 就是你能干什么(what are you allowed to do?) 。 所有的安全框架,基本都是为了解决这两个问题的,Spring Security也不例外。
1. Spring Security的简介
Spring Security是Spring家族推出一款用于认证和授权的安全框架。该框架采用责任链的设计模式,由多个过滤器组成过滤器链来完成认证和授权的功能。框架的整体结构如图1所示。
图1中SecurityFilterChain中的N个过滤器就是Spring Security的核心部分。对于每个客户端的请求,在到达Controller之前,除了应用本身定义的过滤器之外,还需要通过Spring Security过滤器链的处理。认证和授权的实现,都隐藏在这条过滤器链中。下面是过滤器链中的部分过滤器以及它们处理请求的顺序。
Filter | Duty |
---|---|
... | |
LogoutFilter | 用于处理退出登录请求 |
UsernamePasswordAuthenticationFilter | 使用用户名和密码来进行登录认证 |
DefaultLoginPageGeneratingFilter | |
DefaultLogoutPageGeneratingFilter | |
... | |
RememberMeAuthenticationFilter | 记住我认证处理过滤器 |
AnonymousAuthenticationFilter | 匿名认证处理过滤器 |
SessionManagementFilter | Session管理过滤器,用户管理session信息 |
ExceptionTranslationFilter | 异常处理过滤器,用于处理认证过程中抛出的异常 |
FilterSecurityInterceptor | 最后一个filter, 用于权限的判断 |
上述过滤器中,最重要的是UsernamePasswordAuthenticationFilter
过滤器。该过滤器会根据Post请求的用户名和密码产生一个token信息,然后调用AuthenticationManager.authenticate(token)
来完成认证。
2. Spring Security的基本概念
Spring Security的核心部分都在一条过滤器链中,而在这条链中,想要完整认证和授权,还需要借助Spring Security提供的一些组件,下面让我们来认识一下它们。
Authentication: 在Spring Security中,
Authentication
用于表示一个被认证的用户。Authentication里面包含了用户信息(Principal)、密码(Credentials)以及所拥有的权限(Authorities)。(Principal通常是一个UserDetails)SecurityContext: Spring Security中的上下文对象,用于保存当前被认证的用户信息
Authentication
。SecurityContextHolder: 从名字上也可以看出这个是用于保存
SecurityContext
的。默认的情况下,使用ThreadLocal
来保存SecurityContext
对象,因此在认证过后,可以在任意方法中通过SecurityContext.getContext()
方法来获取当前SecurityContext
。
Authentication
、SecurityContext
以及SecurityContextHolder
这三者之间的关系如图2所示。
在认证的时候,通常需要产生一个Authentication
,然后按照Authentication -> SecurityContext -> SecurityContextHolder
的顺序将认证的信息存放到安全上下文中。相反地,在认证后,想要获取当前用户信息,可以按照SecurityContextHolder -> SecurityContext -> Authentication
的顺序获得,即Authentication authentication = SecurityContextHolder.getContext.getAuthentication()
。
AuthenticationManager: Spring Security默认的一个认证接口,定义了Spring Security认证的方法。默认的情况下,会在该方法中验证用户提交的信息,验证成功后将返回的
Authentication
对象保存到上下文中。在UsernamePasswordAuthenticationFilter
会调用AuthenticationManager
的authenticate
方法进行认证。ProviderManager:
AuthenticationManager
最常用的实现。用于管理一个由AuthenticationProvider
组成的链表。根据需要调用AuthenticationProvider
提供的认证方式,如果全部都认证失败,则会抛出认证异常信息。AuthenticationProvider: 每个
AuthenticationProvider
代表了一种特定的认证方式。比如常用的DAOAuthenticationProvider
会根据开发的需要加载username
和password
来进行认证。DAOAuthenticationProvider: 调用
UserDetailsService.loadUserByUsername()
方法得到用户信息,以供后续认证需要。UserDetailsService: 要完成用户的认证,必不可少的一部分就是验证用户的
username
和password
是否正确。Spring Security提供In-Memory和配置文件保存用户名和密码,然而在真实的系统中,用户的信息通常保存在数据库中。加载用户信息的逻辑,通常需要实现Spring Security的UserDetailsService
接口,并在对应的方法中返回一个UserDetails
以供后续的认证和授权。UserDetails:
UserDetailsService.loadUserByUsername()
放回的类型,内部包含用户名、密码、权限等用户信息。Authentication
中Principal
通常也是一个UserDetails
。PassowrdEncoder: 用户密码不可能明文保存,Spring Security提供了一些
PasswordEncoder
用于encode password。官方推荐使用的是BCryptPasswordEncoder
。
3. 认证和授权的实现
- 引入spring security依赖
org.springframework.boot
spring-boot-starter-security
- 配置
WebSecurityConfigurerAdapter
Spring Security为我们提供了一个默认的登录页面和登录用户,默认的用户名为user,密码打印在控制台中。在实际的应用场景中,默认的登录页面和用户信息肯定是不能满足我们的需求的。因此需要进行一定的配置。WebSecurityConfigurerAdapter
是Spring Security中最重要的一个配置类,在这个配置类中我们可以配置登录页面、需要认证的URL等信息、异常处理等。想要开启Spring Security的配置,还需要在配置类中加入@EnableWebSecurity
注解。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/hello", "/login").permitAll() // 1: /hello 和 /login 不要认证就可以访问
.anyRequest().authenticated(); // 2: 其他所有的URL都需要认证之后才可以访问
http.formLogin()
.loginPage("/login") // 3: 自定义登录页面
.loginProcessingUrl("/login") // 4: 自定义登录请求
.failureHandler(new LoginFailureHandler()); // 5: 自定义登录失败处理器
http.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler()); // 6: 自定义权限不足处理器
}
}
WebSecurityConfigurerAdapter
有多个重载的configure
方法,在public void configure(HttpSecurity http)
中可以配置哪些URL是不需要认证即可访问,哪些必须要认证才能访问。如上面的代码中,请求/hello
和/login
的时候不需要登录认证即可访问,其余请求都需要认证之后才能访问。http.formLogin().loginPage()
和loginProcessingUrl()
则配置了自定义的登录页面和登录处理的URL。该方法中还可以给登录失败和权限不足添加自定义的一个处理器,这个前后端分离的情况下应用广泛。自定义的处理器如下所示。
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
String value = objectMapper.writeValueAsString(CommonResult.fail("login failed"));
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(value);
writer.flush();
writer.close();
}
}
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ObjectMapper objectMapper = new ObjectMapper();
String value = objectMapper.writeValueAsString(CommonResult.fail("access denied"));
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(value);
writer.flush();
writer.close();
}
}
- 实现
UserDetailsService
前面提到过,Spring Security会调用AuthenticationManager.authenticate()
方法来进行用户认证。那么认证所需要的真实用户信息来自哪里呢?答案就是UserDetailsService
接口。这个接口只有一个方法UserDetails loadUserByUsername(String username)
,该方法用于加载用户的信息,然后和前端提交的用户名密码做对比。在实际应用中,通常需要实现该接口,然后在loadUserByUsername(String username)
加载数据库中的用户信息。例如:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional userPoOptional = userRepository.findByUsername(username);
if (!userPoOptional.isPresent()) {
throw new UsernameNotFoundException("username not found");
}
UserPo userPo = userPoOptional.get();
Set roles = userPo.getRoles();
List roleNames = roles.stream().map(rolePo -> "ROLE_" + rolePo.getRoleName()).collect(Collectors.toList());
return UserInfo.builder()
.username(userPo.getUsername())
.password(userPo.getPassword())
.authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", roleNames)))
.build();
}
}
这里使用了Spring JPA来加载UserPo对象。查询较为简单,这里不单独列出来,完整代码可以参考底下的链接。
前面提到过,UserDetails
是Spring Security提供的用户信息接口,因此我们需要实现该接口或者使用Spring Security提供的User类,这里选择前者。
@Data
@Builder
public class UserInfo implements UserDetails {
private String username;
private String password;
private Collection extends GrantedAuthority> authorities;
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- 使用
UserDetailsService
在使用UserDetailsService
之前,还有一件事,那就是用户密码匹配的问题。Spring Security提供了多种PasswordEncoder
,其中官方推荐使用的是BCryptPasswordEncoder
。PasswordEncoder
提供了String encode(CharSequence rawPassword)
和boolean matches(CharSequence rawPassword, String encodedPassword)
两个简单的方法,用于计算明文的摘要,以及根据明文和摘要信息判断密码是否正确。通常在注册用户的时候,要使用其encode
方法,而在认证的时候使用match
方法。
有了UserDetailsService
和PasswordEncoder
之后,我们就可以配置认证的过程了。在Spring Security的默认认证流程中,会调用AuthenticationManager.authenticate()
方法来进行认证,该方法会使用到UserDetailsService.loadUserByUsername(String username)
来加载数据库中的用户信息。因此,我们将上面定义好的UserDetailsService
加入到配置中。配置的方式是在WebSecurityConfigurerAdapter
的实现类中重写protected void configure(AuthenticationManagerBuilder auth)
方法。代码如下面所示。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
配置好自定义的UserDetailsService
之后,我们就完成了用户的认证了。
授权的实现
Spring提供了@PreAuthorize
、 @PostAuthorize
和 @Secured
三个注解,用于控制某个API的权限。其中用的较多的是@PreAuthorize
,在该注解中,可以使用Spring EL表达式来控制对应的权限,如hasRole
, hasAnyRole
, hasAuthority
, hasAnyAuthority
等。而在使用@PreAuthorize(value = "hasRole('ROLE_ADMIN')")
之前,还需要在WebSecurityConfigurerAdapter
配置类中添加@EnableGlobalMethodSecurity(prePostEnabled = true)
注解。
用户的角色往往在UserDetailsService.loadUserByUsername()
方法中,从数据库中检索出该用户的角色。在使用过程中,需要为每个角色添加ROLE_
的前缀,然后将其封装成GrantedAuthority
。
Optional userPoOptional = userRepository.findByUsername(username);
// ...
Set roles = userPo.getRoles();
List roleNames = roles.stream().map(rolePo -> "ROLE_" + rolePo.getRoleName()).collect(Collectors.toList());
// ...
AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", roleNames));
将用户的角色附带给UserDetails
之后,只需要在Controller中的需要权限控制的API上@PreAuthorize
即可。例如:
@PreAuthorize(value = "hasRole('ROLE_ADMIN')")
@GetMapping("/{username}")
public UserPo findByUsername(@PathVariable String username) {
return userService.findByUsername(username);
}
4. 代码链接
https://github.com/sylinder/spring-security-demo.git