最简单易懂的Spring Security 身份认证流程讲解

导言

相信大伙对Spring Security这个框架又爱又恨,爱它的强大,恨它的繁琐,其实这是一个误区,Spring Security确实非常繁琐,繁琐到让人生厌。讨厌也木有办法呀,作为JavaEE的工程师们还是要面对的,在开始之前,先打一下比方(比方好可怜):

Spring Security 就像一个行政服务中心,如果我们去里面办事,可以办啥事呢?可以小到咨询简单问题、查询社保信息,也可以户籍登记、补办身份证,同样也可以大到企业事项、各种复杂的资质办理。但是我们并不需要跑一次行政服务中心,就挨个把业务全部办理一遍,现实中没有这样的人吧。

啥意思呢,就是说选择您需要的服务(功能),无视那些不需要的,等有需要的时候再了解不迟。这也是给众多工程师们的一个建议,特别是体系异常庞大的Java系,别动不动就精通,撸遍源码之类的,真没啥意义,我大脑的存储比较小,人生苦短,没必要。

回到正题!本文会以一种比较轻松的方式展开,不会是堆代码。

关于身份认证

Web 身份认证是一个后端工程师永远无法避开的领域,身份认证Authentication,和授权Authorization是不同的,Authentication指的是用户身份的认证,并不介入这个用户能够做什么,不能够做什么,仅仅是确认存在这个用户而已。而Authorization授权是建立的认证的基础上的,存在这个用户了,再来约定这个用户能补能够做一件事,这点大家要区分开。本文讲的是Authentication的故事,并不会关注权限。

热热身,让我们来温习一下身份认证的方式演变:

  • 先是最著名的入门留言板程序,相信很多做后端的工程师都做过留言板,那是一个基本没有框架的阶段,回想一下是怎么认证的。表单输入用户名密码Submit,然后后端取到数据数据库查询,查不到的话无情地抛出一个异常,哦,密码错了;查到了,愉快的将用户ID和相关信息加密写入到Session标识中存起来,响应写入Cookie,后续的请求都解密后验证就行了,对吧。是的,身认证真可以简单到仅仅是匹配Session标识而已。令人沮丧的是现代互联网的发展早已经过了 Web2.0 的时代,客户端的出现让身份认证更加复杂。我们继续

  • 随着移动端的崛起,Android和ios占据主导,同样是用户登录认证,取到用户信息,正准备按图索骥写入Session回写Cookie的时候,等等!啥?Android不支持Cookie?这听起来不科学是吧,有点反人类是吧,有点手足无措是吧。

    嘿嘿,聪明的人儿也许想到了办法,嗯,Android客户端不是有本地存储吗?把回传的数据存起来不就行了吗?又要抱歉了,Android本地存储并没有浏览器Cookie那么人性化,不会自动过期。没事,再注明过期时间,每次读取的时候判断就行啦,貌似可以了。

    等等。客户端的Api接口要求轻量级,某一天一个队友想实现个性化的事情,竟然往Cookie了回传了一串字符串,貌似很方便,嗯。于是其他队友也效仿,然后Cookie变得更加复杂。此时Android队友一声吼,你们够了!STOP!我只要一个认证标识而已,够简单你们知道吗?还有Cookie过期了就要重新登陆,用户体验极差,产品经理都找我谈了几十次了,用户都快跑光了,你们还在往Cookie里加一些奇怪的东西。

  • Oauth 2.0来了

有问题总要想办法解决是吧。客户端不是浏览器,有自己特有的交互约定,Cookie还是放弃掉了。这里就要解决五个问题:

  • [ ] 只需要简单的一个字符串标识,不需要遵守Cookie的规则
  • [ ] 服务器端需要能够轻松认证这个标识,最好是做成标准化
  • [ ] 不要让用户反复输入密码登录,能够自动刷新
  • [ ] 这段秘钥要安全,从网络传输链路层到客户端本地层都要是安全的,就算被中途捕获,也可以让其失效
  • [ ] 多个子系统的客户端需要独立的认证标识,让他们能够独立存在(例如淘宝的认证状态不会影响到阿里旺旺的登录认证状态)

需求一旦确定,方案呼之欲出,让我们来简单构思一下。

  • [x] 首先是标识,这个最简单了,将用户标识数据进行可逆加密,OK,这个搞定。
  • [x] 然后是标识认证的标准化,最好轻量级,并且让她不干扰请求的表现方式,例如Get和Post数据,聪明的你想到了吧,没错,就是Header,我们暂且就统一成 Userkey 为Header名,值就是那个加密过的标识,够简洁粗暴吧,后端对每一个请求都拦截处理,如果能够解密成功并且表示有效,就告诉后边排队的小伙伴,这个家伙是自己人,叫xxx,兜里有100块钱。这个也搞定了。
  • [x] 自动刷新,因为加密标识每次请求都要传输,不能放在一起了,而且他们的作用也不一样,那就颁发加密标识的时候顺便再颁发一个刷新的秘钥吧,相当于入职的时候给你一张门禁卡,这个卡需要随身携带,开门签到少不了它,此外还有一张身份证明,这证明就不需要随身携带了,放家里都行,门禁卡掉了,没关系,拿着证明到保安大哥那里再领一张门禁卡,证明一次有效,领的时候保安大哥贴心的再给你一张证明。
  • [x] 安全问题,加密可以加强一部分安全性。传输链路还用说吗?上Https传输加密哟。至于客户端本地的安全是一个哲学问题,嗯嗯嗯。哈哈。我们暂时认为本地私有空间存储是安全的的,俗话说得好,计算机都被人破解了,还谈个鸡毛安全呀(所以大家没事还是不要去ROOT手机了,ROOT之后私有存储可以被访问侬造吗)
  • [x] 子系统独立问题,这个好办了。身份认证过程再加入一个因子,暂且叫 Client 吧。这样标识就互不影响了。

打完收工,要开始实现这套系统了。先别急呀,难道没觉得似曾相识吗?没错就是 Oauth 2.0 的 password Grant 模式!

Spring Security 是怎么认证的

先来一段大家很熟悉的代码:

  1. http.formLogin()
  2. .loginPage("/auth/login")
  3. .permitAll()
  4. .failureHandler(loginFailureHandler)
  5. .successHandler(loginSuccessHandler);

Spring Security 就像一个害羞的大姑娘,就这么一段鬼知道他是怎么认证的,封装的有点过哈。不着急先看一张图:

这里做了一个简化,

根据JavaEE的流程,本质就是Filter过滤请求,转发到不同处理模块处理,最后经过业务逻辑处理,返回Response的过程。

当请求匹配了我们定义的Security Filter的时候,就会导向Security 模块进行处理,例如UsernamePasswordAuthenticationFilter,源码献上:

  1. public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  2. public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
  3. public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
  4. private String usernameParameter = "username";
  5. private String passwordParameter = "password";
  6. private boolean postOnly = true;
  7. public UsernamePasswordAuthenticationFilter() {
  8. super(new AntPathRequestMatcher("/login", "POST"));
  9. }
  10. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  11. if (this.postOnly && !request.getMethod().equals("POST")) {
  12. throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
  13. } else {
  14. String username = this.obtainUsername(request);
  15. String password = this.obtainPassword(request);
  16. if (username == null) {
  17. username = "";
  18. }
  19. if (password == null) {
  20. password = "";
  21. }
  22. username = username.trim();
  23. UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
  24. this.setDetails(request, authRequest);
  25. return this.getAuthenticationManager().authenticate(authRequest);
  26. }
  27. }
  28. protected String obtainPassword(HttpServletRequest request) {
  29. return request.getParameter(this.passwordParameter);
  30. }
  31. protected String obtainUsername(HttpServletRequest request) {
  32. return request.getParameter(this.usernameParameter);
  33. }
  34. protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
  35. authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
  36. }
  37. public void setUsernameParameter(String usernameParameter) {
  38. Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
  39. this.usernameParameter = usernameParameter;
  40. }
  41. public void setPasswordParameter(String passwordParameter) {
  42. Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
  43. this.passwordParameter = passwordParameter;
  44. }
  45. public void setPostOnly(boolean postOnly) {
  46. this.postOnly = postOnly;
  47. }
  48. public final String getUsernameParameter() {
  49. return this.usernameParameter;
  50. }
  51. public final String getPasswordParameter() {
  52. return this.passwordParameter;
  53. }
  54. }

有点复杂是吧,不用担心,我来做一些伪代码,让他看起来更友善,更好理解。注意我写的单行注释

  1. public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  2. public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
  3. public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
  4. private String usernameParameter = "username";
  5. private String passwordParameter = "password";
  6. private boolean postOnly = true;
  7. public UsernamePasswordAuthenticationFilter() {
  8. //1.匹配URL和Method
  9. super(new AntPathRequestMatcher("/login", "POST"));
  10. }
  11. public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  12. if (this.postOnly && !request.getMethod().equals("POST")) {
  13. //啥?你没有用POST方法,给你一个异常,自己反思去
  14. throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
  15. } else {
  16. //从请求中获取参数
  17. String username = this.obtainUsername(request);
  18. String password = this.obtainPassword(request);
  19. //我不知道用户名密码是不是对的,所以构造一个未认证的Token先
  20. UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
  21. //顺便把请求和Token存起来
  22. this.setDetails(request, token);
  23. //Token给谁处理呢?当然是给当前的AuthenticationManager喽
  24. return this.getAuthenticationManager().authenticate(token);
  25. }
  26. }
  27. }

是不是很清晰,问题又来了,Token是什么鬼?为啥还有已认证和未认证的区别?别着急,咱们顺藤摸瓜,来看看Token长啥样。上UsernamePasswordAuthenticationToken:

  1. public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
  2. private static final long serialVersionUID = 510L;
  3. private final Object principal;
  4. private Object credentials;
  5. public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
  6. super((Collection)null);
  7. this.principal = principal;
  8. this.credentials = credentials;
  9. this.setAuthenticated(false);
  10. }
  11. public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
  12. super(authorities);
  13. this.principal = principal;
  14. this.credentials = credentials;
  15. super.setAuthenticated(true);
  16. }
  17. public Object getCredentials() {
  18. return this.credentials;
  19. }
  20. public Object getPrincipal() {
  21. return this.principal;
  22. }
  23. public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
  24. if (isAuthenticated) {
  25. throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
  26. } else {
  27. super.setAuthenticated(false);
  28. }
  29. }
  30. public void eraseCredentials() {
  31. super.eraseCredentials();
  32. this.credentials = null;
  33. }
  34. }

一坨坨的真闹心,我再备注一下:

  1. public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
  2. private static final long serialVersionUID = 510L;
  3. //随便怎么理解吧,暂且理解为认证标识吧,没看到是一个Object么
  4. private final Object principal;
  5. //同上
  6. private Object credentials;
  7. //这个构造方法用来初始化一个没有认证的Token实例
  8. public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
  9. super((Collection)null);
  10. this.principal = principal;
  11. this.credentials = credentials;
  12. this.setAuthenticated(false);
  13. }
  14. //这个构造方法用来初始化一个已经认证的Token实例,为啥要多此一举,不能直接Set状态么,不着急,往后看
  15. public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
  16. super(authorities);
  17. this.principal = principal;
  18. this.credentials = credentials;
  19. super.setAuthenticated(true);
  20. }
  21. //便于理解无视他
  22. public Object getCredentials() {
  23. return this.credentials;
  24. }
  25. //便于理解无视他
  26. public Object getPrincipal() {
  27. return this.principal;
  28. }
  29. public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
  30. if (isAuthenticated) {
  31. //如果是Set认证状态,就无情的给一个异常,意思是:
  32. //不要在这里设置已认证,不要在这里设置已认证,不要在这里设置已认证
  33. //应该从构造方法里创建,别忘了要带上用户信息和权限列表哦
  34. //原来如此,是避免犯错吧
  35. throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
  36. } else {
  37. super.setAuthenticated(false);
  38. }
  39. }
  40. public void eraseCredentials() {
  41. super.eraseCredentials();
  42. this.credentials = null;
  43. }
  44. }

搞清楚了Token是什么鬼,其实只是一个载体而已啦。接下来进入核心环节,AuthenticationManager是怎么处理的。这里我简单的过渡一下,但是会让你明白。

AuthenticationManager会注册多种AuthenticationProvider,例如UsernamePassword对应的DaoAuthenticationProvider,既然有多种选择,那怎么确定使用哪个Provider呢?我截取了一段源码,大家一看便知:

  1. public interface AuthenticationProvider {
  2. Authentication authenticate(Authentication var1) throws AuthenticationException;
  3. boolean supports(Class<?> var1);
  4. }

这是一个接口,我喜欢接口,简洁明了。里面有一个supports方法,返回时一个boolean值,参数是一个Class,没错,这里就是根据Token的类来确定用什么Provider来处理,大家还记得前面的那段代码吗?

  1. //Token给谁处理呢?当然是给当前的AuthenticationManager喽
  2. return this.getAuthenticationManager().authenticate(token);

因此我们进入下一步,DaoAuthenticationProvider,继承了AbstractUserDetailsAuthenticationProvider,恭喜您再坚持一会就到曙光啦。这个比较复杂,为了不让你跑掉,我将两个复杂的类合并,摘取直接触达接口核心的逻辑,直接上代码,会有所删减,让你看得更清楚,注意看注释:

  1. public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
  2. //熟悉的supports,需要UsernamePasswordAuthenticationToken
  3. public boolean supports(Class<?> authentication) {
  4. return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
  5. }
  6. public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  7. //取出Token里保存的值
  8. String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
  9. boolean cacheWasUsed = true;
  10. //从缓存取
  11. UserDetails user = this.userCache.getUserFromCache(username);
  12. if (user == null) {
  13. cacheWasUsed = false;
  14. //啥,没缓存?使用retrieveUser方法获取呀
  15. user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
  16. }
  17. //...删减了一大部分,这样更简洁
  18. Object principalToReturn = user;
  19. if (this.forcePrincipalAsString) {
  20. principalToReturn = user.getUsername();
  21. }
  22. return this.createSuccessAuthentication(principalToReturn, authentication, user);
  23. }
  24. protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  25. try {
  26. //熟悉的loadUserByUsername
  27. UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
  28. if (loadedUser == null) {
  29. throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
  30. } else {
  31. return loadedUser;
  32. }
  33. } catch (UsernameNotFoundException var4) {
  34. this.mitigateAgainstTimingAttack(authentication);
  35. throw var4;
  36. } catch (InternalAuthenticationServiceException var5) {
  37. throw var5;
  38. } catch (Exception var6) {
  39. throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
  40. }
  41. }
  42. //检验密码
  43. protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
  44. if (authentication.getCredentials() == null) {
  45. this.logger.debug("Authentication failed: no credentials provided");
  46. throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
  47. } else {
  48. String presentedPassword = authentication.getCredentials().toString();
  49. if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
  50. this.logger.debug("Authentication failed: password does not match stored value");
  51. throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
  52. }
  53. }
  54. }
  55. }

到此为止,就完成了用户名密码的认证校验逻辑,根据认证用户的信息,系统做相应的Session持久化和Cookie回写操作。

Spring Security的基本认证流程先写到这里,其实复杂的背后是一些预定,熟悉了之后就不难了。

Filter->构造Token->AuthenticationManager->转给Provider处理->认证处理成功后续操作或者不通过抛异常

有了这些基础,后面我们再来扩展短信验证码登录,以及基于Oauth 2.0 的短信验证码登录。

最简单易懂的Spring Security 身份认证流程讲解的更多相关文章

  1. SpringBoot Spring Security 核心组件 认证流程 用户权限信息获取详细讲解

    前言 Spring Security 是一个安全框架, 可以简单地认为 Spring Security 是放在用户和 Spring 应用之间的一个安全屏障, 每一个 web 请求都先要经过 Sprin ...

  2. [转]Spring Security Oauth2 认证流程

    1.本文介绍的认证流程范围 本文主要对从用户发起获取token的请求(/oauth/token),到请求结束返回token中间经过的几个关键点进行说明. 2.认证会用到的相关请求 注:所有请求均为po ...

  3. shrio 身份认证流程-Realm

    身份认证流程 流程如下: 1.首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils. setSecuri ...

  4. Spring Cloud实战 | 第九篇:Spring Cloud整合Spring Security OAuth2认证服务器统一认证自定义异常处理

    本文完整代码下载点击 一. 前言 相信了解过我或者看过我之前的系列文章应该多少知道点我写这些文章包括创建 有来商城youlai-mall 这个项目的目的,想给那些真的想提升自己或者迷茫的人(包括自己- ...

  5. Spring Security 接口认证鉴权入门实践指南

    目录 前言 SpringBoot 示例 SpringBoot pom.xml SpringBoot application.yml SpringBoot IndexController SpringB ...

  6. 【认证与授权】Spring Security的授权流程

    上一篇我们简单的分析了一下认证流程,通过程序的启动加载了各类的配置信息.接下来我们一起来看一下授权流程,争取完成和前面简单的web基于sessin的认证方式一致.由于在授权过程中,我们预先会给用于设置 ...

  7. 学习Spring Boot:(二十八)Spring Security 权限认证

    前言 主要实现 Spring Security 的安全认证,结合 RESTful API 的风格,使用无状态的环境. 主要实现是通过请求的 URL ,通过过滤器来做不同的授权策略操作,为该请求提供某个 ...

  8. Spring Security 安全认证

    Spring Boot 使用 Mybatis 依赖 <dependency> <groupId>org.mybatis.spring.boot</groupId> ...

  9. 学习Spring Security OAuth认证(一)-授权码模式

    一.环境 spring boot+spring security+idea+maven+mybatis 主要是spring security 二.依赖 <dependency> <g ...

随机推荐

  1. python 使用 thrift 教程

    一.前言: Thrift 是一种接口描述语言和二进制通信协议.以前也没接触过,最近有个项目需要建立自动化测试,这个项目之间的微服务都是通过 Thrift 进行通信的,然后写自动化脚本之前研究了一下. ...

  2. 杭电ACM2014--青年歌手大奖赛_评委会打分

    青年歌手大奖赛_评委会打分 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Tot ...

  3. Linux,在不使用U盘的情况下使用wubi.exe程序在Win7上安装ubuntu-14.04.3版系统

    本文介绍如何在不使用U盘的情况下使用wubi.exe程序在Win7上安装ubuntu-14.04.3版系统. 花了一天的时间终于安装上了Ubuntu14.04,过程坎坷,是血泪史,开始报“cannot ...

  4. spring-boot的spring-cache中的扩展redis缓存的ttl和key名

    原文地址:spring-boot的spring-cache中的扩展redis缓存的ttl和key名 前提 spring-cache大家都用过,其中使用redis-cache大家也用过,至于如何使用怎么 ...

  5. Promise原理讲解 && 实现一个Promise对象 (遵循Promise/A+规范)

    1.什么是Promise? Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一 2.对于几种常见异步编程方案 回调函数 事件监听 发布/ ...

  6. Shell基础命令(一)

    Shell 教程 Shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁.Shell 既是一种命令语言,又是一种程序设计语言. Shell 是指一种应用程序,这个应用程序提供了一个 ...

  7. 广州.NET微软技术俱乐部休闲活动 - 每周三五晚周日下午爬白云山活动

    基于如下原因: 正如我们在<广州.NET微软技术俱乐部与其他技术群的区别>里面提到的:有人在活动中表达"少了一点自由交流的时间, 我们来自五湖四海, 希望多点时间彼此认识&quo ...

  8. 解决Centos7 yum 出现could not retrieve mirrorlist 错误

    刚通过VMware12安装了centos7.x后,使用ip addr查看centos局域网的ip发现没有,使用yum安装一些工具包时也出现报错: Loaded plugins: fastestmirr ...

  9. [ SHELL编程 ] 字符串空格和文件空行删除

    1.删除字符串中空格 (1)删除行首空格 (2)删除行尾空格 (3)删除前.后空格,不删除中间空格 (4) 删除字符串中所有空格 echo " 123 567 " | sed 's ...

  10. 【图解】FlexGrid Explorer 全功能问世

    前言 在去年的时候,我们推出了FlexGrid Demo,包含了FlexGrid的常用功能,如分组.滚动.冻结.自定义单元格类型.搜索面板.表格过滤器.树形结构.合并单元等,目前我们又在里面添加很多了 ...