Spring Security 认证流程(一)
稍微了解一点Spring Security的都知道,Spring Security主要有两大功能:认证和授权。网上有许多相关教程,但是大多比较晦涩。
我相信,刚刚接触到安全框架的同学,应该有许多人和我一样,有种无从下手的感觉。 但是今天看到两个大佬的分析笔记,受益匪浅,在这里记录一下自己的理解。大佬们的链接我会放在帖子底部。
基础使用
Spring Security最简单的应用就是用户登录功能了。 实现这点需要的工作只有3步:
1. 进行security核心配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests();
}
//密码加密方式
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//用它帮我们进行认证操作,调用这个Bean的authenticate方法会由Spring Security自动帮我们做认证。
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
在上述SpringSecurityConfig.java中,配置了密码加密方式BCryptPasswordEncoder(),将Bean组件authenticationManager()交给了spring容器。
2. 登录认证具体实现
实现访问接口
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/login")
public ApiResult login(@Valid @RequestBody LoginInfo loginInfo) {
return authService.login(loginInfo.getLoginAccount(), loginInfo.getPassword());
}
...
}
接口中调用了authService.login()的服务,需要进行实现。
实现对应服务
@Slf4j
@Service
public class AuthServiceImpl implements AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtProvider jwtProvider;
@Autowired
private Cache caffeineCache;
@Override
public ApiResult login(String loginAccount, String password) {
log.debug("进入login方法");
// 1 创建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernameAuthentication = new UsernamePasswordAuthenticationToken(loginAccount, password);
// 2 认证
Authentication authentication = this.authenticationManager.authenticate(usernameAuthentication);
// 3 保存认证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
// 4 生成自定义token
AccessToken accessToken = jwtProvider.createToken((UserDetails) authentication.getPrincipal());
return ApiResult.ok(accessToken);
}
...
}
这里一共4个步骤,大概只有前四步是比较陌生的:
- 传入用户名和密码创建了一个UsernamePasswordAuthenticationToken对象,这是我们前面说过的Authentication的实现类,传入用户名和密码做构造参数,这个对象就是我们创建出来的未认证的Authentication对象。
- 使用我们先前已经声明过的Bean:authenticationManager调用它的authenticate方法进行认证,返回一个认证完成的Authentication对象。(在配置类中,使用@Bean方法声明的)
- 认证完成没有出现异常,就会走到第三步,使用SecurityContextHolder获取SecurityContext之后,将认证完成之后的Authentication对象,放入上下文对象。
- 从Authentication对象中拿到我们的UserDetails对象,之前我们说过,认证后的Authentication对象调用它的getPrincipal()方法就可以拿到我们先前数据库查询后组装出来的UserDetails对象,然后创建token。
到这里,其实就已经完成了,感觉很简单,因为主要认证操作都会由authenticationManager.authenticate()帮我们完成。
authenticate()实际也是去查对象数据库,如何查数据库呢?查询数据库的方法就是调用userDetailsService中的loadUserByUsername()。
大家一般都会自定义实现一下UserDetailsService,就像下面这样。
3. 自定义实现UserDetailsService
@Slf4j
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private RoleInfoService roleInfoService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("开始登陆验证,用户名为: {}", s);
// 根据用户名验证用户
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(UserInfo::getUsername, s);
UserInfo userInfo = userService.getOne(queryWrapper);
if (userInfo == null) {
throw new UsernameNotFoundException("用户名不存在,登陆失败。");
}
// 构建UserDetail对象
UserDetails userDetail = new UserDetails();
userDetail.setUserInfo(userInfo);
return userDetail;
}
}
完成以上步骤就可以去进行简单测试了。
ps:基本的实体类、统一返回结果类ApiResult还有token生成工具类jwtProvider 需要自己提前准备好。
原理分析
如上图,一个请求想要访问到API就会以从左到右的形式经过蓝线框框里面的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分负责异常处理,橙色部分则是负责授权。
我们上面这个案例基本都是绿色部分的内容。
案例解析
下面是security中一种表单登录模式的流程示意图。
在一开始的配置过程中,我们就使用了formLogin方法。这是我们在会经常用到的一段代码,大家应该都比较熟悉,加上.formLogin()后就代表着我们采用了form表单认证方式,框架会自动帮助我们进行用户认证。后续部分介绍的就是框架帮助我们进行用户认证的具体方法。
- HttpSecurity.formLogion() 方法会返回一个 FormLoginConfigurer 对象。这个构造器对象内部就包含着一个UsernamePasswordAuthenticationFilter 过滤器。
- UsernamePasswordAuthenticationFilter 过滤器的构造函数内绑定了 POST 类型的 /login 请求,也就是说,如果配置了 formLogin 的相关信息,那么在使用 POST 类型的 /login URL进行登录的时候就会被这个过滤器拦截,并进行登录验证 这就是加入.formLogion()就会进行用户身份验证的原因。
- 此外,UsernamePasswordAuthenticationFilter 过滤器会解析请求,获得username和password,封装到UsernamePasswordAuthenticationToken 中,然后委托给AuthenticationManager,交由 AuthenticationManager 完成实际的登录认证过程。自身只要在登录成功之后,将认证后的 Authentication 对象存储到请求线程上下文,以方便授权阶段使用就可以了。这就是UsernamePasswordAuthenticationFilter 过滤器的主要工作:解析请求+委托认证+成功存储上下文
- 其实到 AuthenticationManager这里大家就又会比较熟悉了,因为在前面的案例中,我们也主动调用authenticationManager.authenticate()进行过用户认证。其实那就是一种手动的简化版身份认证流程。authenticationManager只是一个接口,具体的实现方法在ProviderManager中,然而ProviderManager又将任务委托给AuthenticationProvider。这里就是一系列的委托再委托,直到交给AuthenticationProvider。
- 所以AuthenticationProvider需要完成AuthenticationManager的工作:用户身份验证,即需要完成:获取验证需要的用户信息的工作 + 密码验证,账号状态验证等逻辑验证的工作。检查和验证的工作基本一致,但是用户信息加载方式常有不同,而我们常用的DaoAuthenticationProvide就是一种通过DAO方式获取验证需要的用户信息的一个具体实现类。
- UserDetailsService则是被DaoAuthenticationProvider 调用,获取验证信息的一个工具类。前面的案例中我们也自定义实现了该类,核心就是这个loadUserByUsername(String username)方法。
总结:
-
整体流程:HttpSecurity.formLogion()作为入口,生成一个包含UsernamePasswordAuthenticationFilter 过滤器的对象,所以才能实现表单登录认证。
-
UsernamePasswordAuthenticationFilter过滤器工作流程:过滤器解析请求获得username和password,委托给AuthenticationManager进行身份认证,成功后存储在上下文中。
-
AuthenticationManager工作流程:将任务多级下派到一个合适的AuthenticationProvider中,获取验证需要的用户信息,并进行多方面验证(密码,账号状态等)。
-
UserDetailsService就是一个获取验证需要的用户信息的工具类。
后续会介绍,如何结合jwt,进行登录认证优化。
作为一个未正式工作的学生,十分感谢各位大佬们的经验分享,也希望我的分享可以帮助到大家。如果有哪些错误的地方,也请大家不吝指出。
本篇文章是在学习两位大佬笔记后的收获整理,下面的链接是大佬们原文。
参考链接
条理清晰,剖析源码:www.cnblogs.com/xifengxiaom…
案例介绍,浅显易懂:juejin.cn/post/684668…