Spring Security 整合OAuth2

OAuth2.0介绍

OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方 应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他 们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。

OAuth协议:rfc6749

协议特点:

  • 简单:不管是OAuth服务提供者还是应用开发者,都很易于理解与使用;

  • 安全:没有涉及到用户密钥等信息,更安全更灵活;

  • 开放:任何服务提供商都可以实现OAuth,任何软件开发商都可以使用OAuth;

应用场景

  • 原生app授权:app登录请求后台接口,为了安全认证,所有请求都带token信息,如果登录验证、 请求后台数据。

  • 前后端分离单页面应用:前后端分离框架,前端请求后台数据,需要进行oauth2安全认证

  • 第三方应用授权登录,比如QQ,微博,微信的授权登录。

基本概念

  • Third-party application:第三方应用程序,又称"客户端"(client),即例子中的"豆瓣"。

  • HTTP service:HTTP服务提供商,简称"服务提供商",即例子中的qq。

  • Resource Owner:资源所有者,又称"用户"(user)。

  • User Agent:用户代理,比如浏览器。

  • Authorization server:授权服务器,即服务提供商专门用来处理认证授权的服务器。

  • Resource server:资源服务器,即服务提供商存放用户生成的资源的服务器。它与授权服务器,可以是同一台服务器,也可以是不同的服务器。

OAuth的作用就是让"客户端"安全可控地获取"用户"的授权,与"服务提供商"进行交互。

优缺点

优点:

  • 更安全,客户端不接触用户密码,服务器端更易集中保护

  • 广泛传播并被持续采用

  • 短寿命和封装的token

  • 资源服务器和授权服务器解耦

  • 集中式授权,简化客户端

  • HTTP/JSON友好,易于请求和传递token

  • 考虑多种客户端架构场景

  • 客户可以具有不同的信任级别

缺点:

  • 协议框架太宽泛,造成各种实现的兼容性和互操作性差

  • 不是一个认证协议,本身并不能告诉你任何用户信息。

运行流程

OAuth 2.0的运行流程如下图,摘自RFC 6749。

(A)用户打开客户端以后,客户端要求用户给予授权。 (B)用户同意给予客户端授权。 (C)客户端使用上一步获得的授权,向认证服务器申请令牌。 (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。 (E)客户端使用令牌,向资源服务器申请获取资源。 (F)资源服务器确认令牌无误,同意向客户端开放资源。

授权模式

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0一共分成四种授权类型(authorization grant)

  • 授权码模式(authorization code)

  • 简化模式(implicit)

  • 密码模式(resource owner password credentials)

  • 客户端模式(client credentials)

授权码模式和密码模式比较常用。

第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。

授权码模式

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。

这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

适用场景:目前主流的第三方验证都是采用这种模式

0

主要流程:

(A)用户访问客户端,后者将前者导向授权服务器。

(B)用户选择是否给予客户端授权。

(C)用户给予授权,授权服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的"重定向URI",向授权服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)授权服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

快速使用

引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>                  

配置授权服务器

注意:实际项目中clinet_id 和client_secret 是配置在数据库中,省略spring security相关配置,可以参考

[Spring Security 入门教程]  https://zhuanlan.zhihu.com/p/429570570 

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
​
    @Autowired
    private PasswordEncoder passwordEncoder;
​
    @Autowired
    private TokenStore tokenStore;
​
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
​
        clients.inMemory()
                //配置client_id
                .withClient("client")
                //配置client_secret
                .secret(passwordEncoder.encode("123123"))
                //配置访问token的有效期
                .accessTokenValiditySeconds(3600)
                //配置刷新token的有效期
                .refreshTokenValiditySeconds(864000)
                //配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                //配置申请的权限范围
                .scopes("all")
                //配置grant_type,表示授权类型
                .authorizedGrantTypes("authorization_code","implicit");
​
​
    }
}

配置资源服务器

@Configuration
@EnableResourceServer
public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {
​
    @Override
    public void configure(HttpSecurity http) throws Exception {
​
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().requestMatchers().antMatchers("/user/**");
        
    }
​
}

1、A网站(客户端)提供一个链接,用户点击后就会跳转到 B (授权服务器)网站,授权用户数据给 A 网站使用。

 https://b.com/oauth/authorize?   
 response_type=code&            # 表示授权类型,必选项,此处的值固定为"code"
 client_id=CLIENT_ID&           # 表示客户端的ID,必选项
 redirect_uri=CALLBACK_URL&     # redirect_uri:表示重定向URI,可选项
 scope=read                     # 表示申请的权限范围,可选项 read只读  

2、用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码

https://a.com/callback?code=AUTHORIZATION_CODE    #code参数就是授权码                   

3、A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。 用户不可见,服务端行为

https://b.com/oauth/token? 
client_id=CLIENT_ID& 
client_secret=CLIENT_SECRET&     # client_id和client_secret用来让 B 确认 A 的身份,client_secret参数是保密的,因此只能在后端发请求 
grant_type=authorization_code&   # 采用的授权方式是授权码
code=AUTHORIZATION_CODE&         # 上一步拿到的授权码 
redirect_uri=CALLBACK_URL        # 令牌颁发后的回调网址         

4、B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,返回数据。

 {    
   "access_token":"ACCESS_TOKEN",     # 令牌
   "token_type":"bearer",
   "expires_in":2592000,
   "refresh_token":"REFRESH_TOKEN",
   "scope":"read",
   "uid":100101,
   "info":{...}
 }

密码模式

如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而授权服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

适用场景:公司搭建的授权服务器

(A)用户向客户端提供用户名和密码。

(B)客户端将用户名和密码发给授权服务器,向后者请求令牌。

(C)授权服务器确认无误后,向客户端提供访问令牌。

配置 spring security

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
  
    @Autowired
    private UserService userService;
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 获取用户信息
        auth.userDetailsService(userService);
    }
​
​
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().permitAll()
                .and().authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest()
                .authenticated()
                .and().logout().permitAll()
                .and().csrf().disable();
    }
    
​
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        // oauth2 密码模式需要拿到这个bean
        return super.authenticationManagerBean();
    }
}
​
​
@Service
public class UserService implements UserDetailsService {
​
    @Autowired
    @Lazy
    private PasswordEncoder passwordEncoder;
​
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String password = passwordEncoder.encode("123456");
        return new User("jack",password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}
​
@Configuration
public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Bean
    public TokenStore tokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
}

配置授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig2 extends AuthorizationServerConfigurerAdapter {
​
    @Autowired
    private PasswordEncoder passwordEncoder;
​
    @Autowired
    private AuthenticationManager authenticationManagerBean;
​
    @Autowired
    private UserService userService;
​
    @Autowired
    private TokenStore tokenStore;
​
​
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
                .tokenStore(tokenStore)  //指定token存储到redis
                .reuseRefreshTokens(false)  //refresh_token是否重复使用
                .userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
                .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
    }
​
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许表单认证
        security.allowFormAuthenticationForClients();
    }
​
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
​
        clients.inMemory()
                //配置client_id
                .withClient("client")
                //配置client-secret
                .secret(passwordEncoder.encode("123123"))
                //配置访问token的有效期
                .accessTokenValiditySeconds(3600)
                //配置刷新token的有效期
                .refreshTokenValiditySeconds(864000)
                //配置redirect_uri,用于授权成功后跳转
                .redirectUris("http://www.baidu.com")
                //配置申请的权限范围
                .scopes("all")
                /**
                 * 配置grant_type,表示授权类型
                 * authorization_code: 授权码模式
                 * implicit: 简化模式
                 * password: 密码模式
                 * client_credentials: 客户端模式
                 * refresh_token: 更新令牌
                 */
                .authorizedGrantTypes("authorization_code","implicit","password","client_credentials","refresh_token");
    }
​
​
}

获取token:

https://oauth.b.com/token?
  grant_type=password&       # 授权方式是"密码式"
  username=USERNAME&
  password=PASSWORD&
  client_id=CLIENT_ID&
  client_secret=123123&
  scope=all
​

客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行 授权。

适用于没有前端的命令行应用,即在命令行下请求令牌。一般用来提供给我们完全信任的服务器端服务。

它的步骤如下:

(A)客户端向授权服务器进行身份认证,并要求一个访问令牌。

(B)授权服务器确认无误后,向客户端提供访问令牌。

A 应用在命令行向 B 发出请求。

https://oauth.b.com/token? 
grant_type=client_credentials& 
client_id=CLIENT_ID& 
client_secret=CLIENT_SECRET              

简化(隐式)模式

有些 Web 应用是纯前端应用,没有后端,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)

简化模式不通过第三方应用程序的服务器,直接在浏览器中向授权服务器申请令牌,所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。

它的步骤如下:

(A)客户端将用户导向授权服务器。

(B)用户决定是否给于客户端授权。

(C)假设用户给予授权,授权服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。

(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。

(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。

(F)浏览器执行上一步获得的脚本,提取出令牌。

(G)浏览器将令牌发给客户端。

A 网站提供一个链接,要求用户跳转到 B 网站,授权用户数据给 A 网站使用。

 https://b.com/oauth/authorize? 
 response_type=token&          # response_type参数为token,表示要求直接返回令牌 
 client_id=CLIENT_ID& 
 redirect_uri=CALLBACK_URL& 
 scope=read     

用户跳转到 B 网站,登录后同意给予 A 网站授权。这时,B 网站就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。

https://a.com/callback#token=ACCESS_TOKEN     #token参数就是令牌,A 网站直接在前端拿到令牌。              

令牌的使用

A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。

此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization字段,令牌就放在这个字段里面。

也可以添加请求参数access_token请求数据

localhost/getUser?access_token=xxxxxxxxxxxxxxxxxxxxxxxxxxx
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值