springcloud单点登录_单点登录与oauth2

后端 (3) 2024-07-11 21:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
springcloud单点登录_单点登录与oauth2,希望能够帮助你!!!。

文章目录

  • 基于Oauth2,springsecurity单点登录SSO,前后端分离和SPA方式实现方式。
    • 发展历史
    • OAuth2涉及角色
    • 协议流程
    • 授权模式
    • 代码解读
    • 在我们系统的中设计
    • SSO实现流程分析
    • 参考文献
    • 具体代码

基于Oauth2,springsecurity单点登录SSO,前后端分离和SPA方式实现方式。

在接到需求要做SPA方式的单点登录的需求,发现好多的坑,之前我们接触的只是浏览器的单点登录,基于session的或者是基于app的基于token的,app类似SPA方式,但是有个不同点,就是在多个app或者多个SPA下怎么做单点登录。一开始以为很容易。但是在搞一段时间啊后发现自己越走越黑,越走越远,总结下来自己对协议理解还是不够透彻,对之前理解的前后端分离的SSO还是止步于session的交互方式。在涉及到多个域之间换取token还是有一些问题。
废话不说了。希望对现在在做了前后端分离的你有所帮助。

发展历史

从OAuth1到OAuth2
1.0协议每个token都有一个加密,2.0则不需要。这样来看1.0似乎更加安全,但是2.0要求使用https协议,安全性也更高一筹。
1.0只有一个用户授权流程。2.0可以从多种途径获取访问令牌
a)授权码 b)客户端私有证书 c)资源拥有者密码证书 d)刷新令牌 e)断言证书
2.0的用户授权过程有2步,1.0的授权分3步,

springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第1张

OAuth2涉及角色

资源拥有者
可以是一个人也可以是一个公司实体,对资源持有的实体。

资源服务
受保护的资源,可以使用token令牌来访问

客户端
需要请求资源的应用客户端,PC,APP

认证服务
发放令牌的服务,验证资源所有者并获得授权

协议流程

springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第2张

授权模式

密码凭证授权模式
第三方Web服务器端应用与第三方原生App
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第3张
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第4张

密码模式(resource owner password credentials)
这种模式是最不推荐的,因为client可能存了用户密码
这种模式主要用来做遗留项目升级为oauth2的适配方案
当然如果client是自家的应用,也可以.
支持refresh token

授权码授权模式
密码模式:第一方单页应用与第一方原生App
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第5张

授权码模式是四种模式中最繁琐也是最安全的一种模式。

1.client向资源服务器请求资源,被重定向到授权服务器(AuthorizationServer)
2.浏览器向资源拥有者索要授权,之后将用户授权发送给授权服务器
3.授权服务器将授权码(AuthorizationCode)转经浏览器发送给client
4.client拿着授权码向授权服务器索要访问令牌
5.授权服务器返回Access Token和Refresh Token给cilent

简化授权模式
第三方单页面应用
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第6张
简化模式相对于授权码模式省略了,提供授权码,然后通过服务端发送授权码换取AccessToken的过程。

1.client请求资源被浏览器转发至授权服务器
2.浏览器向资源拥有者索要授权,之后将用户授权发送给授权服务器
3.授权服务器将AccessToken以Hash的形式存放在重定向uri的fargment中发送给浏览器
4.浏览器访问重定向URI
5.资源服务器返回一个脚本,用以解析Hash中的AccessToken
6.浏览器将Access Token解析出来
7.将解析出的Access Token发送给client

一般简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法使用授权码模式。

客户端凭据模式
没有用户参与的,完全信任的服务器端服务
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第7张

这是一种最简单的模式,只要client请求,我们就将AccessToken发送给它。
(A)客户端向认证服务器进行身份认证,并要求一个访问令牌。
(B)认证服务器确认无误后,向客户端提供访问令牌。

代码解读

springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第8张
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第9张
debug可以看见所有的授权模式
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第10张
以下是我操作跟踪源代码的步骤,还有遇到的一些问题。

1.访问授权地址如上图,state是推荐项可以不写 http://www.clouds1000.com/oauth/authorize?response_type=code&client_id=026f49c0-1a53-4031-9a2a-ec&redirect_uri=http://www.clouds1000.com&scope=all 2.出现User must be authenticated with Spring Security before authorization can be completed.需要登录,增加配置 @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SelfAuthenticationSuccessHandler selfAuthenticationSuccessHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.httpBasic(); super.configure(http); } } 3.error="invalid_request", error_description="At least one redirect_uri must be registered with the client." 说明没有做授权地址覆盖重写ClientDetailsServiceConfigurer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { //添加客户端信息 clients.inMemory() // 使用in-memory存储客户端信息 .withClient("janle") .redirectUris("http://www.clouds1000.com"); } } 4.出现错误error="invalid_grant", error_description="A client must have at least one authorized grant type." clients.inMemory() // 使用in-memory存储客户端信息 .withClient("janle") .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit") .redirectUris("http://www.clouds1000.com"); 5.出现授权信息,选择授权,跳转地址如下,返回对应的code码 http://www.clouds1000.com/?code=OZiJN8 6.访问时候一直弹出basic的页面,不能登录,检查密码加解密是否正确 7.所有的都好了以后返回401,需要检查代码中的客户端的配置是否正确, clients.inMemory() // 使用in-memory存储客户端信息 .withClient("janle") .secret("{bcrypt}" + new BCryptPasswordEncoder().encode("janleSecret")) .authorizedGrantTypes("password", "authorization_code", "refresh_token", "client_credentials") .scopes("all") .authorities("oauth2") //是否遗漏该项 .redirectUris("http://www.clouds1000.com"); 8.这一步测试code码模式已经好了,发现使用密码模式时候找不到对应的token生成TokenGranter,由于authenticationManager为空的话会构建CompositeTokenGranter对应的4个授权模式,具体代码在org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters 调整代码如下: EnableWebSecurity中: /** * 密码模式需要重写配置 * * @return * @throws Exception */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @EnableAuthorizationServer中: @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //在WebSecurityConfigurerAdapter的实现类当中,重写,使用密码模式引入,不然不会加载这种模式 endpoints.authenticationManager(authenticationManager); super.configure(endpoints); } 9.测试密码模式 post提交http://www.clouds1000.com/oauth/token grant_type=password username=user_1 password= scope=all 10.再次测试code码模式输入输入对应返回code参数 [{"key":"grant_type","value":"authorization_code","description":""},{"key":"client_id","value":"janle","description":""},{"key":"redirect_uri","value":"http://www.clouds1000.com","description":""},{"key":"client_secret","value":"janleSecret","description":""},{"key":"code","value":"yD2dHH","description":""}] 

总体的代码结构和调用跟踪,绿色是接口,黄色的是类。
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第11张
继续源码解读,标红的地方注意下就好。
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第12张

springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第13张
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第14张
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第15张
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第16张springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第17张

在我们系统的中设计

基于APP的实现使用流程。springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第18张
但是这个在单一的SPA应用下是可以的,如果在多个SPA应用下不能适用。所以在这种情况下我们需要利用主域的session来做token的交换。所以这样我们前后端分离是好事,但是分离以后却带来了不好的事情。

SSO实现流程分析

基于Oauth2前后端分离SSO失败流程
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第19张

具体需要注意的地方解释说明了:
4.登录成功后携带code码跳转到前端client.7bule.com
8.重定向返回的是api.7bule.com的请求地址,由于应用api.7bule.com做了session的请求处理,
9.前端只能看到后台跳转走地址,不能获取任何后台返回来的信息,所以不能拿到请求后台的token值

基于Oauth2的SSO适用前后端分离单应用
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第20张
这种只能适用于SPA的模式,不能应用于多个SPA之间的跳转。

基于Oauth2的SSO适用多个SPA应用
springcloud单点登录_单点登录与oauth2_https://bianchenghao6.com/blog_后端_第21张
这种支持简单的实现了跨域跳转基于session会话的单点登录。需要做一些优化,要不有安全问题。分享过的PPT,可以随意下载。
https://download.csdn.net/download/u0/

参考文献

https://projects.spring.io/spring-security-oauth/docs/oauth2.html
https://tools.ietf.org/html/rfc6749#section-1.3.1
http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html 恩谦提供

具体代码

环境准备

前端页面地址: fe.clouds1000.com 前端调用后台接口地址 api.clouds1000.com SSO认证服务地址 passport.clouds1000.com 前端无权限处理页 fe.clouds1000.com/oauth 

具体伪代码如下:

axios 响应拦截器处理 无权限后会将当前页面地址进行缓存 Axios.interceptors.response.use(res => { NProgress.done() const status = Number(res.status) || 200 const message = res.data.msg || errorCode[status] || errorCode['default'] if (status === 401) { store.dispatch('FedLogOut').then(() => { setStore({name: 'history_path', location.href }) // 伪代码 router.push({ path: '/oauth' }) }) return } )} //路由导航守卫处理 router.beforeEach((to, from, next) => { // 缓冲设置 if (to.meta.keepAlive === true && store.state.tags.tagList.some(ele => { return ele.value === to.fullPath })) { to.meta.$keepAlive = true } else { NProgress.start() if (to.meta.keepAlive === true && validatenull(to.meta.$keepAlive)) { to.meta.$keepAlive = true } else { to.meta.$keepAlive = false } } const meta = to.meta || {} if (store.getters.access_token) { if (store.getters.isLock && to.path !== lockPage) { next({ path: lockPage }) } else if (to.path === '/login') { next({ path: '/' }) } else { if (store.getters.roles.length === 0) { let loading = Loading.service({ lock: true, text: `登录中,请稍后。。。`, spinner: 'el-icon-loading' }) store.dispatch('GetUserInfo').then(() => { loading.close() next({ ...to, replace: true }) }).catch(() => { loading.close() store.dispatch('FedLogOut').then(() => { setStore({name: 'history_path', location.href }) // 伪代码 next({ path: '/oauth' }) }) }) } else { const value = to.query.src || to.fullPath const label = to.query.name || to.name if (meta.isTab !== false && !validatenull(value) && !validatenull(label)) { store.commit('ADD_TAG', { label: label, value: value, params: to.params, query: to.query, group: router.$avueRouter.group || [] }) } next() } } } else { if (meta.isAuth === false) { next() } else { setStore({name: 'history_path', location.href }) // 伪代码 next('/oauth') } } }) 

无权限处理

在oauth页做如下配置 1.无权限跳转至/oauth 页 ,需要请求跳转页的地址 请求地址为 api.clouds1000.com/oauth/loginuri 2.拿到响应后需要做decodeURIComponent ,然后通过location.href 进行跳转 3.在 sso 认证中心进行登录 passport.clouds1000.com/login 4.登录成功后,认证中心会携带code值重定向回前端无权限处理页 5. 通过获取querySting内的code,调业务系统的获取token的接口,设置token api.clouds1000.com/auth/token 6. 拿到响应后设置token, 请求相应业务接口 7. 从缓存中获取之前存储的历史记录页,跳转回无权限之前的页面 代码: created () { if (this.$route.query.code) { let query = this.$route.query AdminService.login({ code: query.code }).then(res => { console.log(res) this.loading.close() this.$store.commit('SET_ACCESS_TOKEN', res.access_token) AdminService.infos().then(() => { // TODO 拿缓存跳转原页面 }) AdminService.user() }) } else { AdminService.oauth().then(res => { window.location.href = decodeURIComponent(res) }) } } 

后端的配置

 <dependency> <groupId>com.thclouds.ppassport</groupId> <artifactId>ppassport-auth</artifactId> <version>1.0-SNAPSHOT</version> </dependency> @SpringBootApplication public class UiApplication { public static void main(String[] args) { SpringApplication.run(UiApplication.class, args); } } @EnableResourceServer @Configuration public class Oauth2ClientConfig extends AbstractSecurityConfig { } security: path: //需要忽略的地址。 ignores: /,/index,/static/**,/css/**, /image/**, /favicon.ico, /js/**,/plugin/**,/avue.min.js,/img/**,/fonts/** oauth2: client: client-id: 业务系统的client-id client-secret: 业务系统的client-secret user-authorization-uri: http://passport.clouds1000.com/oauth/authorize access-token-uri: http://passport.clouds1000.com/oauth/token scope: all registered-redirect-uri: http://前端业务地址/oauth resource: token-info-uri: http://passport.clouds1000.com/oauth/check_token user-info-uri: http://passport.clouds1000.com/user jwt: #需要携带client key-uri: http://passport.clouds1000.com/oauth/token_key key-value: janle 

具体的demo地址 https://download.csdn.net/download/u0/

https://github.com/ljz0721cx/passport

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

发表回复