好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

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

最简单易懂的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

2

3

4

5

http.formlogin()

           .loginpage( "/auth/login" )

           .permitall()

           .failurehandler(loginfailurehandler)

           .successhandler(loginsuccesshandler);

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

这里做了一个简化,

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

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

?

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

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

public class usernamepasswordauthenticationfilter extends abstractauthenticationprocessingfilter {

   public static final string spring_security_form_username_key = "username" ;

   public static final string spring_security_form_password_key = "password" ;

   private string usernameparameter = "username" ;

   private string passwordparameter = "password" ;

   private boolean postonly = true ;

 

   public usernamepasswordauthenticationfilter() {

     super ( new antpathrequestmatcher( "/login" , "post" ));

   }

 

   public authentication attemptauthentication(httpservletrequest request, httpservletresponse response) throws authenticationexception {

     if ( this .postonly && !request.getmethod().equals( "post" )) {

       throw new authenticationserviceexception( "authentication method not supported: " + request.getmethod());

     } else {

       string username = this .obtainusername(request);

       string password = this .obtainpassword(request);

       if (username == null ) {

         username = "" ;

       }

 

       if (password == null ) {

         password = "" ;

       }

 

       username = username.trim();

       usernamepasswordauthenticationtoken authrequest = new usernamepasswordauthenticationtoken(username, password);

       this .setdetails(request, authrequest);

       return this .getauthenticationmanager().authenticate(authrequest);

     }

   }

 

   protected string obtainpassword(httpservletrequest request) {

     return request.getparameter( this .passwordparameter);

   }

 

   protected string obtainusername(httpservletrequest request) {

     return request.getparameter( this .usernameparameter);

   }

 

   protected void setdetails(httpservletrequest request, usernamepasswordauthenticationtoken authrequest) {

     authrequest.setdetails( this .authenticationdetailssource.builddetails(request));

   }

 

   public void setusernameparameter(string usernameparameter) {

     assert .hastext(usernameparameter, "username parameter must not be empty or null" );

     this .usernameparameter = usernameparameter;

   }

 

   public void setpasswordparameter(string passwordparameter) {

     assert .hastext(passwordparameter, "password parameter must not be empty or null" );

     this .passwordparameter = passwordparameter;

   }

 

   public void setpostonly( boolean postonly) {

     this .postonly = postonly;

   }

 

   public final string getusernameparameter() {

     return this .usernameparameter;

   }

 

   public final string getpasswordparameter() {

     return this .passwordparameter;

   }

}

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

?

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

27

28

29

public class usernamepasswordauthenticationfilter extends abstractauthenticationprocessingfilter {

   public static final string spring_security_form_username_key = "username" ;

   public static final string spring_security_form_password_key = "password" ;

   private string usernameparameter = "username" ;

   private string passwordparameter = "password" ;

   private boolean postonly = true ;

 

   public usernamepasswordauthenticationfilter() {

     //1.匹配url和method

     super ( new antpathrequestmatcher( "/login" , "post" ));

   }

 

   public authentication attemptauthentication(httpservletrequest request, httpservletresponse response) throws authenticationexception {

     if ( this .postonly && !request.getmethod().equals( "post" )) {

       //啥?你没有用post方法,给你一个异常,自己反思去

       throw new authenticationserviceexception( "authentication method not supported: " + request.getmethod());

     } else {

       //从请求中获取参数

       string username = this .obtainusername(request);

       string password = this .obtainpassword(request);

       //我不知道用户名密码是不是对的,所以构造一个未认证的token先

       usernamepasswordauthenticationtoken token = new usernamepasswordauthenticationtoken(username, password);

       //顺便把请求和token存起来

       this .setdetails(request, token);

       //token给谁处理呢?当然是给当前的authenticationmanager喽

       return this .getauthenticationmanager().authenticate(token);

     }

   }

}

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

?

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

27

28

29

30

31

32

33

34

35

36

37

38

39

40

public class usernamepasswordauthenticationtoken extends abstractauthenticationtoken {

   private static final long serialversionuid = 510l;

   private final object principal;

   private object credentials;

 

   public usernamepasswordauthenticationtoken(object principal, object credentials) {

     super ((collection) null );

     this .principal = principal;

     this .credentials = credentials;

     this .setauthenticated( false );

   }

 

   public usernamepasswordauthenticationtoken(object principal, object credentials, collection<? extends grantedauthority> authorities) {

     super (authorities);

     this .principal = principal;

     this .credentials = credentials;

     super .setauthenticated( true );

   }

 

   public object getcredentials() {

     return this .credentials;

   }

 

   public object getprincipal() {

     return this .principal;

   }

 

   public void setauthenticated( boolean isauthenticated) throws illegalargumentexception {

     if (isauthenticated) {

       throw new illegalargumentexception( "cannot set this token to trusted - use constructor which takes a grantedauthority list instead" );

     } else {

       super .setauthenticated( false );

     }

   }

 

   public void erasecredentials() {

     super .erasecredentials();

     this .credentials = null ;

   }

}

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

?

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

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

public class usernamepasswordauthenticationtoken extends abstractauthenticationtoken {

   private static final long serialversionuid = 510l;

   //随便怎么理解吧,暂且理解为认证标识吧,没看到是一个object么

   private final object principal;

   //同上

   private object credentials;

 

   //这个构造方法用来初始化一个没有认证的token实例

   public usernamepasswordauthenticationtoken(object principal, object credentials) {

     super ((collection) null );

     this .principal = principal;

     this .credentials = credentials;

     this .setauthenticated( false );

   }

   //这个构造方法用来初始化一个已经认证的token实例,为啥要多此一举,不能直接set状态么,不着急,往后看

   public usernamepasswordauthenticationtoken(object principal, object credentials, collection<? extends grantedauthority> authorities) {

     super (authorities);

     this .principal = principal;

     this .credentials = credentials;

     super .setauthenticated( true );

   }

   //便于理解无视他

   public object getcredentials() {

     return this .credentials;

   }

   //便于理解无视他

   public object getprincipal() {

     return this .principal;

   }

 

   public void setauthenticated( boolean isauthenticated) throws illegalargumentexception {

     if (isauthenticated) {

       //如果是set认证状态,就无情的给一个异常,意思是:

       //不要在这里设置已认证,不要在这里设置已认证,不要在这里设置已认证

       //应该从构造方法里创建,别忘了要带上用户信息和权限列表哦

       //原来如此,是避免犯错吧

       throw new illegalargumentexception( "cannot set this token to trusted - use constructor which takes a grantedauthority list instead" );

     } else {

       super .setauthenticated( false );

     }

   }

 

   public void erasecredentials() {

     super .erasecredentials();

     this .credentials = null ;

   }

}

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

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

?

1

2

3

4

5

public interface authenticationprovider {

   authentication authenticate(authentication var1) throws authenticationexception;

 

   boolean supports( class <?> var1);

}

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

?

1

2

//token给谁处理呢?当然是给当前的authenticationmanager喽

  return this .getauthenticationmanager().authenticate(token);

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

?

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

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

public class daoauthenticationprovider extends abstractuserdetailsauthenticationprovider {

   //熟悉的supports,需要usernamepasswordauthenticationtoken

   public boolean supports( class <?> authentication) {

       return usernamepasswordauthenticationtoken. class .isassignablefrom(authentication);

     }

 

   public authentication authenticate(authentication authentication) throws authenticationexception {

       //取出token里保存的值

       string username = authentication.getprincipal() == null ? "none_provided" : authentication.getname();

       boolean cachewasused = true ;

       //从缓存取

       userdetails user = this .usercache.getuserfromcache(username);

       if (user == null ) {

         cachewasused = false ;

 

         //啥,没缓存?使用retrieveuser方法获取呀

         user = this .retrieveuser(username, (usernamepasswordauthenticationtoken)authentication);

       }

       //...删减了一大部分,这样更简洁

       object principaltoreturn = user;

       if ( this .forceprincipalasstring) {

         principaltoreturn = user.getusername();

       }

 

       return this .createsuccessauthentication(principaltoreturn, authentication, user);

     }

      protected final userdetails retrieveuser(string username, usernamepasswordauthenticationtoken authentication) throws authenticationexception {

     try {

       //熟悉的loaduserbyusername

       userdetails loadeduser = this .getuserdetailsservice().loaduserbyusername(username);

       if (loadeduser == null ) {

         throw new internalauthenticationserviceexception( "userdetailsservice returned null, which is an interface contract violation" );

       } else {

         return loadeduser;

       }

     } catch (usernamenotfoundexception var4) {

       this .mitigateagainsttimingattack(authentication);

       throw var4;

     } catch (internalauthenticationserviceexception var5) {

       throw var5;

     } catch (exception var6) {

       throw new internalauthenticationserviceexception(var6.getmessage(), var6);

     }

   }

   //检验密码

   protected void additionalauthenticationchecks(userdetails userdetails, usernamepasswordauthenticationtoken authentication) throws authenticationexception {

     if (authentication.getcredentials() == null ) {

       this .logger.debug( "authentication failed: no credentials provided" );

       throw new badcredentialsexception( this .messages.getmessage( "abstractuserdetailsauthenticationprovider.badcredentials" , "bad credentials" ));

     } else {

       string presentedpassword = authentication.getcredentials().tostring();

       if (! this .passwordencoder.matches(presentedpassword, userdetails.getpassword())) {

         this .logger.debug( "authentication failed: password does not match stored value" );

         throw new badcredentialsexception( this .messages.getmessage( "abstractuserdetailsauthenticationprovider.badcredentials" , "bad credentials" ));

       }

     }

   }

}

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

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

filter->构造token->authenticationmanager->转给provider处理->认证处理成功后续操作或者不通过抛异常

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

查看更多关于详解最简单易懂的Spring Security 身份认证流程讲解的详细内容...

  阅读:15次