spring security
概述
很早以前我们开发web应用的时候,是面向servlet的编程,我们的每个页面都会对应一个servlet,这样产生了大量的模板代码,spring mvc带来的变革是只生成了一个dispatchservlet,这个组件会接受所有的请求,并将请求分发给对应的controller,这样我们就可以只用关注业务代码,忽略模板代码了。
spring security 就是通过对dispactherservlet的保护来实现整个应用保护的,其通过filter chain实现基于filter的保护机制,具体如下:

上图中spring mvc的边界就是dispatcherservlet,spring security的边界就是filterchain,spring security边界之外对应了tomcat的filter。也就是请求到达spring security之前就已经到达了tomcat容器之中。
详解
快速开始
我们通过在spring的web应用中引入对应的依赖就可以实现应用的安全防护,对应的pom依赖如下:
1 | <dependency> |
这样我们在启动web应用的时候就会启用默认的配置实现应用的防护:

原理讲解
在开始讲解整体的代码逻辑之前,我们先来详细的看一下spring security内部默认的情况下是如何实现安全认证的,如下图所示:

- 认证请求被usernamepasswordauthenticationfilter拦截
- spring security将请求发送给authenmanager
- authenmanager负责管理authenprovider,并将请求依次发送给provider,manager可以认为是一个策略的管理者
- provider可以认为是策略的提供者,会接受manager发送过来的请求进行验证,这里就是验证策略的具体实现
- userdetailservice用来将provider的逻辑和加载用户信息的实现进行解耦
接下来,我们可以看一下默认的处理流程
可以看到其业务的处理流程正如上面所示。
这里的provider的具体实现是 DaoAuthenticationProvider,这个类最终会调用UserDetailService的实现类来完成用户的验证,默认实现了这个接口的类是 InMemoryUserDetailManager,InMemoryUserDetailManager 这个类相当于一个内存数据库,其中存放了用户的信息,如下方法最终会被调用来获取数据库中的用户信息(这里是用内存模拟了一个数据库):
1 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { |
spring mvc 启动的时候会加载DelegatingFilterProxy这个bean,通过这个java bean的名字可以知道,它是一个委派过滤器的代理,也就是说其本质是一个代理类,用于实现请求的委派的filter,在引入了spring security之后,DelegatingFilterProxy的委派代理类就是FilterChainProxy,这个 FilterChainProxy 是一个Filter的集合,里面集中了spring security所有的过滤器,通过这个类就可以完成的,下面为spring security 提供的filter及其默认的顺序:
- org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
- org.springframework.security.web.context.SecurityContextPersistenceFilter
- org.springframework.security.web.header.HeaderWriterFilter
- org.springframework.security.web.authentication.logout.LogoutFilter
- org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
- org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
- org.springframework.security.web.authentication.AnonymousAuthenticationFilter
- org.springframework.security.web.session.SessionManagementFilter
- org.springframework.security.web.access.ExceptionTranslationFilter
- org.springframework.security.web.access.intercept.FilterSecurityInterceptor

spring security在对请求过滤的时候就是按照上面的filter一个一个的进行匹配,doFilter参数里面同样会包含一个filterChain字段,通过filterChain实现了filter的串联。
定制化逻辑
spring security提供了支持扩展的能力,扩展的关键点在于 WebSecurityConfigurerAdapter类,这是一个配置类,用于配置我们自定义的实现类,我们将以jwt token为例,提供以下几种扩展方式:
authenticationProvider:这种情况下我们需要实现
AuthenticationProvider这个接口,实现这个接口需要实现两个方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getPrincipal().toString();
String password = authentication.getCredentials().toString();
String pwd = userDetailsService.loadUserByUsername(userName).getPassword();
if (passwordEncoder.matches(password, pwd)) {
return new UsernamePasswordAuthenticationToken(userName, password, new ArrayList<>());
}
throw new BadCredentialsException("user not found!");
}
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}在quickstart那里我们看到了请求的认证流程,authenticationManager会接受用户信息(authentication),然后传递给authenticationProvider,authenticationProvider自然就需要对接收的参数的合法性进行判定,这就是上述supports的来源,如果我们需要自定义待认证的model的话,这里的support就需要对类型进行识别,为了方便支持多个authentication对象的认证,我们可以实现多个provider,并将其和authenticationManager进行关联即可,在用户认证的时候会首先调用support类,如果返回为true,就继续认证,否则进入下一个provider来验证。我们在实现上述认证的时候并不是通过filter将请求拦截并传递给authenticationManager的,而是主动的调用authenticationManager的auticate方法进行认证,这点和默认的认证机制是不同的,需要明确注意。
上面我们实现了自定义的用户认证逻辑,接下来我们需要将其装配给authenticationManager,装配的过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
protected void configure(HttpSecurity http) throws Exception {
// 禁止csrf是因为我们并不会使用formlogin的方式,而是使用rest的方式进行验证的,因此这里是不必要配置csrf的
http.csrf().disable()
// 只允许login在没有认证的情况下访问,并不允许其他请求的访问
.authorizeRequests().antMatchers("/login").permitAll().anyRequest().authenticated()
// 由于我们并不使用form+session的方式进行认证,因此这里永远不去创建session
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}上面的第一个方法用于返回一个authenticationManager类型的bean,这是spring security给我们提供的默认的实现bean,通过这种方式,我们可以在应用的其他地方通过@Autowired的方式将其引入,并对用户登录的请求进行校验。
接下来的两个配置我们可以根据其传入的参数进行猜测,对于AuthenticationManagerBuilder类型参数的配置方法,其是用来装配authenticationManager的,在这里我们可以指定使用哪种Provider,也可以配置多种provider。多个provider的验证逻辑是:当其中一个provider验证失败的时候会继续进行下一个provider的验证,直到验证成功或者失败。
最后一个configre传入的参数是HttpSecurity类型的,也就是和http请求的安全性相关的配置,其主要是用来实现对请求的拦截,规定了哪些请求可以放过,哪些请求必须认证,由于我们使用了token的方式进行认证,因此这里需要关闭session的创建,也就是
SessionCreationPolicy.STATELESS,由于http的请求涉及到请求的拦截,因此这里我们需要定制一个filter对请求拦截,并将其加入到适当的位置,由于我们只希望用户的请求只被过滤一次,因此该filter需要实现类oncePerRequestFilter(通常就是实现这个类):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
public class CustomAuthenticationFilter extends OncePerRequestFilter {
JwtUtil jwtUtil;
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头,这个请求头是约定的,可以是任意的参数
String authenticationHeader = request.getHeader("Authentication");
if (authenticationHeader == null || authenticationHeader.isEmpty() || !authenticationHeader.startsWith("at")) {
filterChain.doFilter(request,response);
return;
}
String token = authenticationHeader.split(" ")[1].trim();
if (!jwtUtil.valid(token)) {
filterChain.doFilter(request,response);
return;
}
String username = jwtUtil.getUsername(token);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, new ArrayList<>());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request, response);
}
}上述这个filter会对配置的请求进行拦截,只有携带了正确的token才可以继续,否则就会抛出异常;当然也可以是我们再configure里面配置的允许访问的路径。
上面的认证过程中,我们看到了在认证成功后,我们调用了这么一段代码
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);才继续filter的处理的,这一段代码很重要,我们将请求信息(用户名、密码等其他信息)绑定到了请求的上下文里面,这样就和请求关联了起来,使得请求来源的用户信息可以被后续识别出来,这样我们可以在后续的controller通过静态的SecurityContextHolder.getContext()获取到用户的信息,方便我们针对用户开展业务。回想我们使用session的时候如果不将session信息保存到contextholder,那么后续需要获取用户的时候都要在Controller接受的参数里面加上HttpSession类型的参数。UserDetailService:我们也可以不识闲authenticationProvider,而是简单的视线userDetailService来实现验证的逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CustomUserDetailServiceImpl implements UserDetailsService {
PasswordEncoder encoder;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Map<String, String> db = new HashMap<>();
db.put("wes", encoder.encode("123"));
if (db.containsKey(username)) {
return new User(username, db.get(username), new ArrayList<>());
}
throw new UsernameNotFoundException("user not founded!");
}
}实现了上述的代码之后,我们依然需要绑定到authenticationManager->authenticationProvider的整个链路上,这依然是在webSecurityConfiguerAdapter中的configure实现的:
1
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
我们的filter依然是使用上面的filter,http拦截的逻辑也依然是上面的逻辑。
最后顺便提一下jwt token,通常我们的web应用如果是单节点的应用可以使用session的认证机制,如果是多个后端或者将开发的业务集成到现有的框架中通常采用token认证的方式。jwt token带来的好处是服务端变成了无状态的了,也就是说服务端不会留存token的任何信息,用户所有的信息都将保存到token里面了,凭借这个token我们可以知道对应的用户、token失效的时间等,这给后端带来了极大的扩展,多移动端或者前后端分离的web应用。
TODO: 后续继续补充session的相关机制

小结
spring security 推荐学习视频:学习材料