# springsecurity学习 **Repository Path**: jkh95/spring-security-learning ## Basic Information - **Project Name**: springsecurity学习 - **Description**: springsecurity学习 尚硅谷 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2021-08-31 - **Last Updated**: 2024-08-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 0.为什么 OAuth 里除了 Access Token 之外,还需要 Refresh Token? 直接上连接https://www.jianshu.com/p/bf017aa3fb84 ![image-20230131140509649](assets/aabbcc.png) ## 1.SpringSecurity集成多种登录方式 https://blog.csdn.net/hou_ge/article/details/120291287?spm=1001.2014.3001.5502 ## 2.Oauth2之/oauth/authorize(AuthorizationEndpoint) 主要负责下面几个活: A.如果没登录,则跳转到授权服务器的登录页面 B.登陆后,跳转到/oauth/confirm_access,跳转到用户【授权】与【取消】的页面(此处可以自定义,可以“信任”) C.点击授权的话,则调用POST /oauth/authorize,然后重定向回用户的页面+code 文章:https://blog.csdn.net/hou_ge/article/details/122322056?spm=1001.2014.3001.5502 ## 3.SpringSession+Redis 在DXP里面用到了SpringSession,场景是这样的:现在用dxp与flow两个服务,且他们都是2个实例,那么现在就是有4台服务器,且他们走的都是同一个网关:http://xxx.xxx.xx.xx:8077/ 那么他们会用一套Cookie,此时比如访问dxp,进行登录了,Session里也有了。此时请求又转发到dxp的另外一个实例上,因为同一个IP,请求也带着 第一次登录后的登录Cookie过来,但是你dxp的另个实例没存Session,咋办,GG。。。。但是你用了SpringSession的话,他会在第一个登录的时候,同步Session到所有的服务器,那么第二次的请求到dxp的另个实例的时候 拿着Cookie,就能匹配上Session了,flow也是同理的。 但是SpringSession有缺点,当并发量大,或者用户登录很多的时候,Session就要耗费大量的服务器资源,更别说要同步给其他服务器。在同步的时差上也会出问题。优化:服务器不存session了,存到redis里,大家都去redis里取。所有SpringSession+Redis的方案,可行。但是不是很好的一个方案。 ## 4.WebSecurityConfigurerAdapter顺序问题 ​ 起因:DXP系统里有很多WebSecurityConfigurerAdapter,我就像这么多玩意,他们如果控制了同一个url,看谁起效果?比如/org/add 有2个WebSecurityConfigurerAdapter都匹配到了,一个不许认证就可以访问,一个需求,那听谁的? ​ 先说结论,谁先听谁的 分两种情况, 在同一个WebSecurityConfigurerAdapter中,谁先听谁的 ![image-20221216133139303](assets/image-20221216133139303.png) 如图,此时访问con1,是不需要登录的 ![image-20221216133212551](assets/image-20221216133212551.png) 如果反过来的话,会跳转到login页面 ********************************************************* 在多个WebSecurityConfigurerAdapter中,看Order ![image-20221216133340743](assets/image-20221216133340743.png) ![image-20221216133353115](assets/image-20221216133353115.png) 可以看到,第一个类Order是99,会在第二个类前被加载,所以,优先级更高,所以如果我们访问/con2,是需要登陆后访问的。 ## 5.requestMatchers和authorizeRequests的区别? ​ 起因:在配置WebSecurityConfigurerAdapter的configure的时候,需要对匹配的URL进行认证,但是看到requestMatchers、authorizeRequests都能够antMatchers("/xxx/**")进行授权认证,所以有啥区别吗? ​ 理解这两个方法的区别首先要知道springsecurity中,每声明一个WebSecurityConfigurerAdapter实例,就会产生一条过滤器链,一个请求过来要走哪个过滤器链就是由requestMatchers()方法配置的url决定的。请求匹配上requestMatchers()配置的过滤器链后,在进一步的详细控制则是authorizeRequests()决定的。 ​ 所以咱们的眼光如果只放在一个WebSecurityConfigurerAdapter上看,看不出来任何问题,但是如果一个项目里有N个WebSecurityConfigurerAdapter,就有区别了。 ​ 两个WebSecurityConfigurerAdapter对同一个url都匹配上了 就要看Order来决定他们的优先级了。 ​ 多提一嘴:requestMatchers().anyRequest() 等价于 http.authorizeRequests().anyRequest().permitAll(); ![image-20221216140205366](assets/image-20221216140205366.png) ![image-20221216140211997](assets/image-20221216140211997.png) 访问/con1 是不需要登录的,虽然第二个图优先级高,但是他的requestMatchers没有去管理/con1,而第一个图是管理con1,所以要看第一个图对于con1 是怎么管理的 ## 6.Oauth2几个模式的区别 发现一篇讲的很好,言简意赅的文章:https://blog.csdn.net/Cr1556648487/article/details/127318889 ## 7.(重点)SpringSecurity执行原理 疑问?DXP里有那么多的WebSecurityConfigurerAdapter,他们是怎么工作的,不会相互干扰嘛?比如我写一个过滤器,到底拦截谁呢? ### 7.1多个WebSecurityConfigurerAdapter 实际上一个WebSecurityConfigurerAdapter的实例就可以配置一条过滤器链。如图,我这个项目配置了2个Adapter。(我随意访问一个接口,会被FilterChainProxy拦截) image-20221222164113591 image-20221222164235063 从图中可以看到,我有两个过滤器链,并且他们的管控范围就是每个adapter的requestMatchers()进行配置的。这个在第5章节提到了。这里再次验证了,每个adapter只会管控自己指定的范围,外面的东西,他不管。 流程:请求进来,被FileChainProxy拦截,调用getFilters进行匹配,看他被哪个adapter管,然后得到这个adapter所有的filter集合,然后调用所有的filter。 ![image-20221222165008950](assets/image-20221222165008950.png) 注意看这个this就很精髓,因为我们都知道filter执行完 必须放行,放行之后还是调用自己,形成一个循环调用,不过在上面有一个Position指数器,来实现把list一个一个执行完。 这也解释了我们请求的url,只会被管控自己的adapter配置的filter所来拦截,比如我adapter自己配了2个filter,我这个url被他管控,我来请求的时候,会被这2个filter拦截。但是如果我不是这个adapter所管控,这2个filter不会拦截我。 ## 8.授权码模式省去授权确认步骤 实现的效果:调用 /oauth/authorize直接直接获取code,不需要再让用户点击授权,才能拿到Code。 授权码模式想要拿授权码Code,是访问接口GET /oauth/authorize 关注下面这个代码即可: ```java //检验用户是否授权,默认使用的是DefaultUserApprovalHandler对象,实现逻辑等同于是直接判断authorizationRequest对象的approved属性 authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication) principal); boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal); authorizationRequest.setApproved(approved); // 如果已经经过用户授权,如果responseTypes包含token这调用getImplicitGrantResponse()方法,进行跳转, // 如果responseTypes包含code,则调用getAuthorizationCodeResponse()方法,进行跳转。 if (authorizationRequest.isApproved()) { if (responseTypes.contains("token")) { return getImplicitGrantResponse(authorizationRequest); } if (responseTypes.contains("code")) { return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal)); } } // 如果没有经过用户授权,就会保存authorizationRequest对象,然后调用getUserApprovalPageResponse()方法,跳转到"forward:/oauth/confirm_access";即,登录授权确认页面,该地址在WhitelabelApprovalEndpoint类中定义,并提供了授权确认的接口"/oauth/confirm_access"。 model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest); model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest)); ``` 其关键就在于`userApprovalHandler.isApproved`。他是读取oauth_client表里的approve字段(每个系统都不一样,但是都会有这么个字段的)。 userApprovalHandler也是可以个性化的。 ```java @Bean public UserApprovalHandler userApprovalHandler() { OauthUserApprovalHandler userApprovalHandler = new OauthUserApprovalHandler(); userApprovalHandler.setOauthService(oauthService); userApprovalHandler.setTokenStore(tokenStore); userApprovalHandler.setClientDetailsService(this.clientDetailsService); userApprovalHandler.setRequestFactory(oAuth2RequestFactory()); return userApprovalHandler; } ``` ```java @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenServices(tokenServices) .tokenStore(tokenStore) .authorizationCodeServices(authorizationCodeServices) .userDetailsService(userDetailsService) .userApprovalHandler(userApprovalHandler()) .authenticationManager(authenticationManager); } ``` oauth_client每个字段具体什么含义,可以去看开源项目spring-oauth-server里面的db_table_description.html。 ## 9.SpringSession + CAS实现统一登出 背景:有个项目,希望实现授权服务器的退出后,当前浏览器的所有子系统统一登出。我们之前一直只关注登入,确实没实现过登出。我看了下Ngportal项目,他们因为使用了SpringSecurity,所以可以基于`session策略`完成登出。但是这个项目很老,没有用到SpringSecurity。此时我突然想打`SSO团队`他们当时的文档里,有提到`CAS登出过滤器`。我一搜,果然CAS开源项目有一个方案,基于java 过滤器的实现。可参考[文章](https://www.cnblogs.com/xiangkejin/p/8963089.html) 但是CAS是内部维护了一个Map进行管理的,而项目上,大概率是集群部署的。那么CAS的套路就不行了,故 我们可以自己照着CAS的思路去实现一版。 下面是我的思路: - 重写`SessionMappingStorage`,因为CAS基于它做session的维护的,而他不支持集群,故要重写,写一个redis版本的`RedisBackedSessionMappingStorage`,代码的话可以参考[RedisBackedSessionMappingStorage](https://ningyu1.github.io/site/post/54-cas-server/)。 - 重写`SingleSignOutFilter`,因为它比较蛋疼,它是内部new出来的,所以自己重写一下,并且set自己的`RedisBackedSessionMappingStorage`。 下面是幻想时刻: 那么现在按理说,通过CAS的SingleSignOutFilter可以找到需要注销的HttpSession,并且执行它的`session.invalidate();`即可注销Session,也同步到SpringSession里,这样整个集群都会退出。 它会执行`org.springframework.session.web.http.SessionRepositoryFilter.SessionRepositoryRequestWrapper.HttpSessionWrapper#invalidate`,清除redis缓存。