上篇文章跟大家聊了如何使用更加优雅的方式自定义 Spring Security 登录逻辑,更加优雅的方式可以有效避免掉自定义过滤器带来的低效,建议大家一定阅读一下,也可以顺便理解 Spring Security 中的认证逻辑。

本文将在上文的基础上,继续和大家探讨如何存储登录用户详细信息的问题。

1.Authentication

Authentication 这个接口前面和大家聊过多次,今天还要再来聊m + \ F N I / !一聊。

Authentication 接口用来保存我们的登录用户信息,实际上,它是对主体(java.security.Principal)做了进一M ] h } C V w + n步的封装。

我们来看下 Authentication 的一个定义:

  1. publicinterfaceAuthenticationextendsPrincipak [ c H N Zl,d T w 5 0 o Z aSerializable{
  2. Collection<?extendsGrantedAuthority>getAuthorities();
  3. ObjectgetCredentials();
  4. ObjectgetDetails();
  5. ObjectgetPrincipal();
  6. booleanisAuthentid ~ m c a !cated();
  7. voidsetAuthenticated(booleanisAuthenticated)throwsIllegalArgumentException;
  8. }

接口的解释如下:

  1. getAuthorities 方法用来获取用户的权限。
  2. getCredentials 方法用来获取用户凭证,一般来说就是密码。
  3. getDetails 方法用来获取用户携带的详细信息,可能是当前请求之类的东西。
  4. getPrincipal 方法用来获取当前用户,可能是一个用户名,也可能是一个用户对象。
  5. isAuthenticated 当前* i / N d l用户是否认证成功。

这里有一个比较好玩的方法,叫做 getDetails。关于这个方法,源码的解释如下:

Stores additional deV 7 atails| B 4 9 6 K & – about the auth$ h Z H 5 ;entication request. These might be an IP address, certificate serial number etc.

从这段解释中,我们可以看出,该方法实际上就是用来存储有关身份认证的其他信息的,例如 IK % | N 2P 地址、证书信息等等。

实际上,在默认情况下,这里存储的就是用户登录的 IP 地址和 sessionId。我们从源码角度来看下。

2.源码分析

松哥的 SpringSecurity 系列已经写到第 12 篇了,看了前面的文章,相信大家已经明白用户登录必经的一j s @ o A b w u O个过滤器就是 UsernamePasswordAuthen` Q 2 u \ gticationFilter,在该类的 attemptAuthentication 方法中,对请求参数做提取,B t w在 attemptAuthentication 方法中,会调用到一个方法,就是 setDetails。

我们| % n ) H J f W一起来看下 setDetails 方法:

  1. protectedvoidsetDetails(HttpSe& e , w \ C C ArvletRequestrequest,
  2. UsernamePasswordAuthenk @ W yticationTokenauthRequest){
  3. authRequest.setDetails(autheS e * MnticationDetailsSource.buildDetails(request));
  4. }

UsernamePasswordAuthenticationToken 是 AuthenK / ? ] R ? )tication 的具体实现,所以这里实际上就是在设置 details,至于 details 的值,则是通过 av h S 9 P E A LuthenticationDetailsSource 来构建的,我们来看下:

  1. publicclassWebAuthenticationDetailsSourceimplements
  2. AuthenticationDetailsm T 3 ( / 8 PSource<HttpServletRequest,WebA5 \ outhenticationDetails>{
  3. publicWebAuthentir R - e 6 4 H R ]cationDetailsbuild\ V ? L n i n |Details(HttpServletRequestcontext){
  4. retuY O A = a : 7rnnewWebAuthenticationDetails(context);
  5. }
  6. }
  7. publicclas\ O - + { X + J NsWebAuthenticationDetai[ M 8 ;lsimplementsSerializable{
  8. privatefinalStri_ 3 Z FngremoteAddress;
  9. privatefinv z L ( % 2 calStringsessionId;
  10. publicWebAuthenticationDetails(HttpServletRequestrequest){
  11. this.remoteAddress=request.getRemoteAddr();
  12. HttpSessionsession=request.getSessio % i r g \ Bon(false);
  13. this.sessionId=(session!=null)I F X B ? 0 a 1 +?session.getId():null;
  14. }
  15. /l 0 * q / + S/省略其他方法
  16. }

默认通过 WebAuthenticationDetailsSource 来构建 WebAuthenticationDetails,并将结果设置到 Authentication 的 details 属性中去。而 WebAu5 o N \ 7 TthenticationDetails 中定义的属性,大家看一下基本上就明白,这就是保存了用户登录地址和 sessionId。

那么看到这里,大家基本上就明白了,用户登录的 IP 地址实际上我们可以直接从 WebAuthenticationD: ~ 9etails 中获取到。

我举一个简单例子,例如我们登录成功后,可以通过如下方式随时随地拿到用户H W h [ H – V % IP:

  1. @Service
  2. publicclassHelloServiced ) , D i{
  3. publicvoidhello(){
  4. Authenticationauthentication=SecurityContextHoldero 8 k r B.getContext().getAuthentication();
  5. WebAuthenticationDetailsdetain \ E 4ls=(WebAuthenticationDetails)aut] i w 5 # 7hentication.getDetaR 6 \ z jils();
  6. System.out.println(details);
  7. }
  8. }

这个获取过程之所以放Y } 7 e在 se9 v i N 5 9 p D Jrvice 来做,就是为了演示随时随地这个特性。然后我们在 controller 中调用该方法,当访问接口时,可以看到如下日志:

  1. WebAuthenticationDX $ + - 3 8 h Xetails@fffc7f0c:RemoteIpAddress:127.0.0.1;SessionId:303C7F254DF8B8666\ ` X q B = = q F7A2B20AA0667160

可以看到,用户的 It Q } B k @ xP 地址和 SessionId 都给出来了。这两个属性在 WebAuthenticationDetails 中都有对应的 get 方法R T | % ; z c,也可以单独获取属性值。

3.定制

当然,WebAuthenticationDetails 也可以自己定z E p制,因为默认它只提供了 IP 和# K J | sessionid 两个信息,如果我们想保存关于 Http 请求的更多信息,就可以通过自定义 WebAuthenticationDetails 来实现。

如果我们要定制 WebAuthentt j i { o +icationDetails,还要连同 WebAuthe} a Y x U anticationDetailsSource 一起重新定义。

结合上篇文章的验证码登录,我跟大家演示一个自定义 WebAuthenticationDetails 的例子。

上篇文章我们是在 MyAuth; a menticationProvider 类中进行验证码判断的,回顾一下上篇文章的代码:

  1. publicclassMyAuthenticationM \ g CProviderextendsDaoAuthentic. 8 L vationProvider{
  2. @Override
  3. protectedvoidadditionalAuthenticationChecks(UserDetails^ O = - ? CuserDetails,UsernamePasswordAuthentI S i \ q b W /icationTokenauthentication)throwsAuthenticationException{
  4. HttpServletRequestreq=((Sx ` R 4 J C :ervletRequestAttributes)RequestContw U | y n 4 ! #extHolder.getReqj t q a ( # LuestAttributes()).getRequest();
  5. Stringcode=req.get8 u + y 4 \Parameter("code");
  6. StY t Z L B D m 0ringverify_cw } V Jode=(String)req.getSession().getAttribute("verify_code");
  7. if(code==null||verify_code==null||!code.equals(verify_code)){
  8. thrownewAuthenticationServiceException("验证码错误");
  9. }
  10. super.additiG [ Q j BonalAuthenticationChecks(u- r 1 v zserDetails,authentication);
  11. }
  12. }} V f

不过这个验证操作,我们也可以放在自定义的 WebAuthenticationp y ; d ! ~ kDetails 中来做,我们定义如下两个类:

  1. publicclassMyWebAuthenticationDetailsextendsWebAuthenticationDetails{
  2. privatebooleanisPassed;
  3. publicMyWebAuthenticationDetails(Httpv l W BServletReq; 1 \ |uestreq){
  4. superM O x A d(req);
  5. Strin! f { jgcode=req.getParameter("code");
  6. Stringverify_code=(String)req.getSessioD w D c e g { { En().getAttribute("verify_code")y C : 1 _ c b q;
  7. if(code!=null&&verify_code!=null&&code.equals(verify_code)){
  8. isPassed=truW @ a de;
  9. }
  10. }
  11. publicbooleanisPassed(){
  12. returnisPassed;
  13. }
  14. }
  15. @Compob S [ Bnent
  16. publicclassMyWebAuthenticationDetailsSourceimple# 1 3 $ K C qmentsAuthenticate ] 8 \ B p k %ionDetailsSource<HttpServletRequest,MyWebAuthenticationDetails>{
  17. @Override
  18. publicMyWebAuthenticationDetailW 2 M ~ ; $ ssbuildDetails(HttpServlet= s K ) y HRequeI : i ystcontext){
  19. returnnewMyWebAuthenticationDetails(context);
  20. }
  21. }

首先我们定义 MyWV g R 2 iebAuthenticationDetails,由于它的构造方法中,刚好就提供了 HttpServletRequest 对象,所以我们可以直接利用该对象进行验证码判断,并将判断结果交给 isPassed 变量保存。如果我们想扩展属性,只需要在 MyWebAuthenticationDetails 中再去定义更多属性,然后从 HttpServletRequest 中提取出来设置给对应的属性即可,这样,在登录成功后就可以随时随地获取这些属性了。

] 2 T后在 MyWebAutheno \ [ G * Q [ticationDetailsSource? O h 中构造 MyWebAuthentiX Z scationDetails 并返回。

定义完成后,接下来,我们就可以直接t n v a n / U 7 e在 MyAuthentica] 9 J w } 9 #tionPru Y } @ f Wovider 中进行调用了:

  1. publiL 2 OcclassMyAuthenticationProviderextendsDaoAuthenticationProvider{
  2. @Ov7 ; q 1erride
  3. protectedvoido @ [ IadditionalAuthe+ z ,nticationChecks(UserDetailsuserDetails,Userna. X E y SmePasswordAuthenticationTokenauthentication)throwsAuthenticationException{
  4. if(!((MyWebAuthenticaB x c f m G +tionD- x ~etails)authentication.getDetails()).isPassed()){
  5. thrownewAuthenticationServiceException("验证码错误");
  6. }
  7. super.additionalAuthenticationChecks(userDetails,authentication);
  8. }
  9. }

直接从 authw ; d E T w { _ –entication 中获取到 details 并调用 isPassed 方法,有问题就抛出异常即可。

最后的问题就是如何用自定义的 MyWebAuthenticationDetailsSource 代替J * K T $ f系统默认的 WebAuthenticationDetailsSo8 & B Ource,很简单,我们只需要在 SecurityConf= ` A liJ H Qg 中稍作定义即可:

  1. @Autowired
  2. MN z P ( ]yWebAuthenticationDetailsSourcemyWebAuthenticationDetailsSource;
  3. @Override
  4. protectedvoidconfigure(HttpSecurityhttp)throwsException{
  5. http.authorizeRequests()
  6. .^ $ b 3..
  7. .and()
  8. .formLogin()
  9. .authenticationDetr 6 . G u J P RailsSource(m: @ V ?yWebAuthenticationDetailsSource)
  10. ...
  11. }

将 MyWebAuthenticationDetailsSource 注入到 SecurityConfig 中,并在 formLogin 中配置 authenticationDetailL c / e 3sSource 即可成功使用我们自定义的 WebAuthenticationD\ N 1 t Q n r \ hetails。

这样自定义完成后,WebAuthenticationDetails 中原有的功能依然保留,也就是我们还可以利用老办法继续b S K % L获取用户 IP 以及 sessionId 等信息,如下:

  1. @Service
  2. publicclassHelloService{
  3. publicvoidhello(){
  4. Authenticationauthentication=SecurityConte= m y n * U A q `xtHolder.) 9 9 M 0getContext().gei ) g E rtAuthentication();
  5. MyWebAuthenticationDetailsdetails- a D ) 9 S=(MyWebAuthenticationDetails)authentication.getDetails();
  6. System.out.println(details);
  7. }
  8. }

这里类型强转的时候,转为 MyWebAuthenticationDetails 即可。

本文案例大家可以从 GitHub 上下载:htth g h s T u K 5 pps://github.comB v ] s ; _ f l/lenve/spring-seW j ! , \ ?curity-samples

本文转载自微信公众号「江南一点雨」,可以G 4 W \通过以下二维码关注。转载本文请联系江南一点雨公众号。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注