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

基于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步,

在这里插入图片描述

OAuth2涉及角色

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

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

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

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

协议流程

在这里插入图片描述

授权模式

密码凭证授权模式
第三方Web服务器端应用与第三方原生App
在这里插入图片描述
在这里插入图片描述

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

授权码授权模式
密码模式:第一方单页应用与第一方原生App
在这里插入图片描述

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

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

简化授权模式
第三方单页面应用
在这里插入图片描述
简化模式相对于授权码模式省略了,提供授权码,然后通过服务端发送授权码换取AccessToken的过程。

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

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

客户端凭据模式
没有用户参与的,完全信任的服务器端服务
在这里插入图片描述

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

代码解读

在这里插入图片描述
在这里插入图片描述
debug可以看见所有的授权模式
在这里插入图片描述
以下是我操作跟踪源代码的步骤,还有遇到的一些问题。

1.访问授权地址如上图,state是推荐项可以不写
http://www.clouds1000.com/oauth/authorize?response_type=code&client_id=026f49c0-1a53-4031-9a2a-7899819548ec&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=123456
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":""}]

总体的代码结构和调用跟踪,绿色是接口,黄色的是类。
在这里插入图片描述
继续源码解读,标红的地方注意下就好。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述

在我们系统的中设计

基于APP的实现使用流程。在这里插入图片描述
但是这个在单一的SPA应用下是可以的,如果在多个SPA应用下不能适用。所以在这种情况下我们需要利用主域的session来做token的交换。所以这样我们前后端分离是好事,但是分离以后却带来了不好的事情。

SSO实现流程分析

基于Oauth2前后端分离SSO失败流程
在这里插入图片描述

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

基于Oauth2的SSO适用前后端分离单应用
在这里插入图片描述
这种只能适用于SPA的模式,不能应用于多个SPA之间的跳转。

基于Oauth2的SSO适用多个SPA应用
在这里插入图片描述
这种支持简单的实现了跨域跳转基于session会话的单点登录。需要做一些优化,要不有安全问题。分享过的PPT,可以随意下载。
https://download.csdn.net/download/u013642886/11216813

参考文献

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/u013642886/11501079

https://github.com/ljz0721cx/passport

发布了39 篇原创文章 · 获赞 16 · 访问量 2万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览