上篇文章跟大家聊了如何使用更加优雅的方式自定义 Spring Security 登录逻辑,更加优雅的方式可以有效避免掉自定义过滤器带来的低效,建议大家一定阅读一下,也可以顺便理解 Spring Security 中的认证逻辑。
本文将在上文的基础上,继续和大家探讨如何存储登录用户详细信息的问题。
1.Authentication
Authentication 这个接口前面和大家聊过多次,今天还要再来聊m + \ F N I / !一聊。
Authentication 接口用来保存我们的登录用户信息,实际上,它是对主体(java.security.Principal)做了进一M ] h } C V w + n步的封装。
我们来看下 Authentication 的一个定义:
- publicinterfaceAuthenticationextendsPrincipak [ c H N Zl,d T w 5 0 o Z aSerializable{
- Collection<?extendsGrantedAuthority>getAuthorities();
- ObjectgetCredentials();
- ObjectgetDetails();
- ObjectgetPrincipal();
- booleanisAuthentid ~ m c a !cated();
- voidsetAuthenticated(booleanisAuthenticated)throwsIllegalArgumentException;
- }
接口的解释如下:
- getAuthorities 方法用来获取用户的权限。
- getCredentials 方法用来获取用户凭证,一般来说就是密码。
- getDetails 方法用来获取用户携带的详细信息,可能是当前请求之类的东西。
- getPrincipal 方法用来获取当前用户,可能是一个用户名,也可能是一个用户对象。
- 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 方法:
- protectedvoidsetDetails(HttpSe& e , w \ C C ArvletRequestrequest,
- UsernamePasswordAuthenk @ W yticationTokenauthRequest){
- authRequest.setDetails(autheS e * MnticationDetailsSource.buildDetails(request));
- }
UsernamePasswordAuthenticationToken 是 AuthenK / ? ] R ? )tication 的具体实现,所以这里实际上就是在设置 details,至于 details 的值,则是通过 av h S 9 P E A LuthenticationDetailsSource 来构建的,我们来看下:
- publicclassWebAuthenticationDetailsSourceimplements
- AuthenticationDetailsm T 3 ( / 8 PSource<HttpServletRequest,WebA5 \ outhenticationDetails>{
- publicWebAuthentir R - e 6 4 H R ]cationDetailsbuild\ V ? L n i n |Details(HttpServletRequestcontext){
- retuY O A = a : 7rnnewWebAuthenticationDetails(context);
- }
- }
- publicclas\ O - + { X + J NsWebAuthenticationDetai[ M 8 ;lsimplementsSerializable{
- privatefinalStri_ 3 Z FngremoteAddress;
- privatefinv z L ( % 2 calStringsessionId;
- publicWebAuthenticationDetails(HttpServletRequestrequest){
- this.remoteAddress=request.getRemoteAddr();
- HttpSessionsession=request.getSessio % i r g \ Bon(false);
- this.sessionId=(session!=null)I F X B ? 0 a 1 +?session.getId():null;
- }
- /l 0 * q / + S/省略其他方法
- }
默认通过 WebAuthenticationDetailsSource 来构建 WebAuthenticationDetails,并将结果设置到 Authentication 的 details 属性中去。而 WebAu5 o N \ 7 TthenticationDetails 中定义的属性,大家看一下基本上就明白,这就是保存了用户登录地址和 sessionId。
那么看到这里,大家基本上就明白了,用户登录的 IP 地址实际上我们可以直接从 WebAuthenticationD: ~ 9etails 中获取到。
我举一个简单例子,例如我们登录成功后,可以通过如下方式随时随地拿到用户H W h [ H – V % IP:
- @Service
- publicclassHelloServiced ) , D i{
- publicvoidhello(){
- Authenticationauthentication=SecurityContextHoldero 8 k r B.getContext().getAuthentication();
- WebAuthenticationDetailsdetain \ E 4ls=(WebAuthenticationDetails)aut] i w 5 # 7hentication.getDetaR 6 \ z jils();
- System.out.println(details);
- }
- }
这个获取过程之所以放Y } 7 e在 se9 v i N 5 9 p D Jrvice 来做,就是为了演示随时随地这个特性。然后我们在 controller 中调用该方法,当访问接口时,可以看到如下日志:
- 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 类中进行验证码判断的,回顾一下上篇文章的代码:
- publicclassMyAuthenticationM \ g CProviderextendsDaoAuthentic. 8 L vationProvider{
- @Override
- protectedvoidadditionalAuthenticationChecks(UserDetails^ O = - ? CuserDetails,UsernamePasswordAuthentI S i \ q b W /icationTokenauthentication)throwsAuthenticationException{
- HttpServletRequestreq=((Sx ` R 4 J C :ervletRequestAttributes)RequestContw U | y n 4 ! #extHolder.getReqj t q a ( # LuestAttributes()).getRequest();
- Stringcode=req.get8 u + y 4 \Parameter("code");
- StY t Z L B D m 0ringverify_cw } V Jode=(String)req.getSession().getAttribute("verify_code");
- if(code==null||verify_code==null||!code.equals(verify_code)){
- thrownewAuthenticationServiceException("验证码错误");
- }
- super.additiG [ Q j BonalAuthenticationChecks(u- r 1 v zserDetails,authentication);
- }
- }} V f
不过这个验证操作,我们也可以放在自定义的 WebAuthenticationp y ; d ! ~ kDetails 中来做,我们定义如下两个类:
- publicclassMyWebAuthenticationDetailsextendsWebAuthenticationDetails{
- privatebooleanisPassed;
- publicMyWebAuthenticationDetails(Httpv l W BServletReq; 1 \ |uestreq){
- superM O x A d(req);
- Strin! f { jgcode=req.getParameter("code");
- Stringverify_code=(String)req.getSessioD w D c e g { { En().getAttribute("verify_code")y C : 1 _ c b q;
- if(code!=null&&verify_code!=null&&code.equals(verify_code)){
- isPassed=truW @ a de;
- }
- }
- publicbooleanisPassed(){
- returnisPassed;
- }
- }
- @Compob S [ Bnent
- publicclassMyWebAuthenticationDetailsSourceimple# 1 3 $ K C qmentsAuthenticate ] 8 \ B p k %ionDetailsSource<HttpServletRequest,MyWebAuthenticationDetails>{
- @Override
- publicMyWebAuthenticationDetailW 2 M ~ ; $ ssbuildDetails(HttpServlet= s K ) y HRequeI : i ystcontext){
- returnnewMyWebAuthenticationDetails(context);
- }
- }
首先我们定义 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 中进行调用了:
- publiL 2 OcclassMyAuthenticationProviderextendsDaoAuthenticationProvider{
- @Ov7 ; q 1erride
- protectedvoido @ [ IadditionalAuthe+ z ,nticationChecks(UserDetailsuserDetails,Userna. X E y SmePasswordAuthenticationTokenauthentication)throwsAuthenticationException{
- if(!((MyWebAuthenticaB x c f m G +tionD- x ~etails)authentication.getDetails()).isPassed()){
- thrownewAuthenticationServiceException("验证码错误");
- }
- super.additionalAuthenticationChecks(userDetails,authentication);
- }
- }
直接从 authw ; d E T w { _ –entication 中获取到 details 并调用 isPassed 方法,有问题就抛出异常即可。
最后的问题就是如何用自定义的 MyWebAuthenticationDetailsSource 代替J * K T $ f系统默认的 WebAuthenticationDetailsSo8 & B Ource,很简单,我们只需要在 SecurityConf= ` A liJ H Qg 中稍作定义即可:
- @Autowired
- MN z P ( ]yWebAuthenticationDetailsSourcemyWebAuthenticationDetailsSource;
- @Override
- protectedvoidconfigure(HttpSecurityhttp)throwsException{
- http.authorizeRequests()
- .^ $ b 3..
- .and()
- .formLogin()
- .authenticationDetr 6 . G u J P RailsSource(m: @ V ?yWebAuthenticationDetailsSource)
- ...
- }
将 MyWebAuthenticationDetailsSource 注入到 SecurityConfig 中,并在 formLogin 中配置 authenticationDetailL c / e 3sSource 即可成功使用我们自定义的 WebAuthenticationD\ N 1 t Q n r \ hetails。
这样自定义完成后,WebAuthenticationDetails 中原有的功能依然保留,也就是我们还可以利用老办法继续b S K % L获取用户 IP 以及 sessionId 等信息,如下:
- @Service
- publicclassHelloService{
- publicvoidhello(){
- Authenticationauthentication=SecurityConte= m y n * U A q `xtHolder.) 9 9 M 0getContext().gei ) g E rtAuthentication();
- MyWebAuthenticationDetailsdetails- a D ) 9 S=(MyWebAuthenticationDetails)authentication.getDetails();
- System.out.println(details);
- }
- }
这里类型强转的时候,转为 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 \通过以下二维码关注。转载本文请联系江南一点雨公众号。