授权服务器版本:
1 | xml复制代码<dependency> |
👉 Spring 授权服务器 GitHub
源码里有几个案例比较适合上手
使用 spring-security-oauth2-authorization-server 搭SSO单点登录需要对SpringSecurity和OAuth2有比较深入的了解。
单点的SpringSecurity配置方面坑不多,值得一提的就是
1 | java复制代码http.authorizeRequests().antMatchers("/login/**").permitAll() |
而不能是
1 | java复制代码http.authorizeRequests().antMatchers("/login").permitAll() |
或者
1 | java复制代码http.formLogin().permitAll() |
因为spring-security-oauth2-authorization-server对OAuth2标准的重定向都是使用各种savedRequest
,使用HttpSessionRequestCache
缓存,然后结束后跳转,由于security 对于所有不公开访问的 url 都会进行 saveRequest 操作 所以 这里必须放开/login/**
而不是/login
,否则错误页面/login?error
也会被缓存至 savedRequest
中,导致用户输错密码时会反复登录,或者覆盖掉oauth2的重定向回调
其他就按照官方案例老老实实配就行,一共两个SpringSecurity的配置,另外一个就是常见Security配置,配置一些登录、登出、rememberMe、cors、csrf之类的就不展示了,在用的OIDC的Security配置如下
1 | java复制代码 @Bean |
OIDC登录会重定向至默认Security的登录页面,具体的用户登录判断是在默认Security设置的。
单点登出
重点是logout , OidcProviderConfigurationEndpointFilter
会注册一个标准协议中的/.well-known/openid-configuration
端点,但是这个版本的spring授权服务器这个端点返回的元数据不包括end_session_endpoint
的信息,故需要客户端自定义登出操作。
客户端如果需要单点登出,就参考org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler
自己实现一个,把endSessionEndpoint
方法修改一下(或者偷懒让前端发两个登出请求(笑),再或者内部用WebClinet发一个请求到单点服务器上,不让用户跳转了)
1 | java复制代码 private URI endSessionEndpoint(ClientRegistration clientRegistration) { |
然后在客户端的Security配置里把successHandler注册进去
1 | java复制代码.logout(logout -> |
然后在单点服务器这边,如果没有启用csrf那么LogoutFilter
会拦截所有请求,如果开启了只会拦截POST
,而客户端发起的单点登出实际上是由浏览器的重定向请求(如果登出请求使用Ajax发出的那么浏览器只会发出请求但是不会跳转页面,需要刷新一下页面),且请求中包含了jwt可以通过SpringSecurity的授权过滤器
如果开启csrf,那么需要额外的配置:增加一个登出的controller,因为客户端发起的重定向连接如果没有被LogoutFilter捕获(捕获也就直接登出了)就会到达controller层 在controller层登出是非常简单的,因为org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
对Request进行了包装,直接HttpServletRequest.logout()
即可
1 | java复制代码@GetMapping("/logout") |
传递用户数据
授权服务器搭起来是比较简单的,坑比较多的是客户端,如果想要客户端通过单点共享用户数据,比如共享一个自定义的UserDetails
的实现类UserEntity
,但是客户端通过Oauth2登录,SecurityContextHolder.getContext().getAuthentication().getPrincipal()
就不是UserDetails
的子类而是OidcUser
的子类,需要实现一个自定义的OidcUser
将UserEntity
组装进去,传递UserDetails
可以使用资源服务器形式,但是如果需要在客户端的登录认证阶段就把UserEntity
特别是其中的authorities
加载好,就需要自己实现一个OidcUserService
去发出http请求加载,而且不能使用正常的资源服务器的Oauth2请求流程,而是拿着OIDC的AccessToken去找单点交换资源,否则会登录一半重新跳转到资源服务器的登录。
而且UserEntity
不能直接被jackson JSON序列化传输,否则要么authorities
不序列化,要么SpringSecurity无法加载UserEntity
,需要自定义个一个专门用来传输的UserEntityDTO
1 | java复制代码public class SsoOidcUserService extends OidcUserService { |
前后端分离
然后就是前后端分离的一些问题,前后端分离,访问请求是Ajax发起的,但是Ajax发起的请求不能被302重定向,需要和前端约定好客户端需要登录时返回JSON,让前端自行重定向。后端实现一个AuthenticationEntryPoint
塞到SpringSecurity设置里。 这点跟postman很像,postman测试单点也是不行的,重定向请求会直接后台默默转发,直到401,点一下postman,以为只请求了一次,实际上postman控制台显示发出了n次请求,没有一点提示,也很坑
然后就是登录成功后跳转的问题,Spring OAuth2 登陆完会从HttpSessionRequestCache
取出savedRequest
重定向,这样的问题就是登陆完,页面重定向到了后端接口,显示一大堆JSON数据,这样肯定是不行的,可以自定义AuthenticationSuccessHandler
,塞到loginSuccessHandler
里,跳转到固定页面,或者还是在AuthenticationEntryPoint
里,和前端约定好请求里都带上一个自定义的header,值是JS代码window.location.href
,然后在org.springframework.security.web.AuthenticationEntryPoint#commence
方法执行的时候从header里面取出来,替换掉Session中的savedRequest
,替换方法就不展示了,HttpSessionRequestCache
怎么存的就怎么替换。
然后就是Nginx反向代理,代理时需要设置
1 | bash复制代码proxy_set_header Host $http_host; |
否则org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint#buildRedirectUrlToLoginPage
方法里request.getServerName()
就会取到的是Nginx主机地址而不是原始host地址
顺带一提,如果开发阶段用的IP地址测试,和域名之间是不同的,如果用了域名,但是不用Nginx反向代理,前端页面、后台、单点登录三者不同端口号会导致Cookies不能正确传递,一登录完就丢失会话信息,但是数字ip的不同端口号却又能正确传递Cookies…
开发用了很久,开发完了,部署阶段也被坑了好多次,花了一个星期时间才算部署上去
本文转载自: 掘金