OAuth2
OAuth2简介
OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等〉,而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌( token ),而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样, OAuth 让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。例如,用户想通过QQ 登录知乎,这时知乎就是一个第三方应用,知乎要访问用户的一些基本信息就需要得到用户的授权,如果用户把自己的QQ 用户名和密码告诉知乎,那么知乎就能访问用户的所有数据,井且只有用户修改密码才能收回授权,这种授权方式安全隐患很大,如果使用OAuth ,就能很好地解决这一问题。采用令牌的方式可以让用户灵活地对第三方应用授权或者收回权限。OAuth 2 是OAuth 协议的下一版本,但不向下兼容OAuth 1.0 。OAuth 2 关注客户端开发者的简易性,同时为Web 应用、桌面应用、移动设备、起居室设备提供专门的认证流程。传统的Web 开发登录认证一般都是基于Session 的,但是在前后端分离的架构中继续使用S巳ssion 会有许多不便,因为移动端( Android 、iOS 、微信小程序等)要么不支持Cookie (微信小程序〉,要么使用非常不便,对于这些问题,使用OAuth 2认证都能解决。
OAuth2角色
要了解OAuth2 ,需要先了解OAuth2中几个基本的角色
• 资源所有者:资源所有者即用户,具有头像、照片、视频等资源。
• 客户端: 客户端即第三方应用,例如上文提到的知乎。
• 授权服务器:投权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
• 资源、服务器:资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。
一般来说,授权服务器和资源服务器可以是同一台服务器。
OAuth2授权流程
OAuth 2 的授权流程到底是什么样的呢?如图10-9 所示。
这是OAuth 2 一个大致的授权流程图,具体步骤如下:
- 步骤01 客户端(第三方应用)向用户请求授权。
- 步骤02 用户单击客户端所呈现的服务授校页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端。
- 步骤03 客户端拿着授权许可凭证去授权服务器申请令牌。
- 步骤04 授权服务器验证信息无误后,发放令牌给客户端。
- 步骤05 客户端拿着令牌去资源服务器访问资源。
- 步骤06 资源服务器验证令牌无误后开放资源。
这是一个大致的流程,因为OAuth 2 中有4 种不同的授权模式,每种授权模式的授权流程又会有差异,基本流程如图所示。
授权模式
- OAuth 协议的授权模式共分为4 种, 分别说明如下。
- 授权码模式: 技权码模式( authorization code )是功能最完整、流程最严谨的技权模式。它的特点就是通过客户端的服务器与才是权服务器进行交王,国内常见的第二方平台登录功能基本都是使用这种模式。
- 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向投非又服务器申请令牌, 一般若网站是纯静态页面,则可以采用这种方式。
- 密码模式: 密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向技权服务器申请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言, 在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
注:
这里的oauth2不是springboot那个,这是springsecurity。
Oauth2一共有四种认证模式是
本篇是password的认证模式,用于前后端分离登陆
第三方登陆一般是授权码模式
相关配置和代码
注:配置授权服务器和资源服务器
一般是分开来的,这里就不分开了
application.properties
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.password=root
创建授权服务
- 实现AuthorizationServiceConfigurerAdapter
@Configuration
@EnableAuthorizationServer//开启授权服务
public class AuthorizationServiceConfig extends AuthorizationServerConfigurerAdapter {
- 注入AuthenticationManager、RedisConnectionFactory、UserDetailService
@Autowired
AuthenticationManager authenticationManager;//表示支持password认证模式
@Autowired
RedisConnectionFactory redisConnectionFactory; //登录成功后token需要存在redis里面,因为里面有过期机制
@Autowired
UserDetailsService userDetailsService;//里面存放着用户信息
- AuthenticationManager表示支持password认证模式
- RedisConnectionFactory登陆成功后的token需要存在redis里面,因为redis里面有过期机制
- UserDetailsService里面存放着用户信息
3.重写方法
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//这个好比账号
.withClient("client_id")
//授权同意的类型
.authorizedGrantTypes("password","refresh_token")
//有效时间
.accessTokenValiditySeconds(1800)
.refreshTokenValiditySeconds(60*60*2)
.resourceIds("rid")
//作用域,范围
.scopes("all")
//密码
.secret("$2a$10$dBcNmmqNjcp94160vLlfrOHHdh8LadpnzG9QyT18rwwRoCwe14w1a");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
//身份验证管理
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许客户端表单身份验证
security.allowFormAuthenticationForClients();
}
完整代码
@Configuration
@EnableAuthorizationServer//开启授权服务
public class AuthorizationServiceConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;//表示支持password认证模式
@Autowired
RedisConnectionFactory redisConnectionFactory; //登录成功后token需要存在redis里面,因为里面有过期机制
@Autowired
UserDetailsService userDetailsService;//里面存放着用户信息
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//这个好比账号
.withClient("client_id")
//授权同意的类型
.authorizedGrantTypes("password","refresh_token")
//有效时间
.accessTokenValiditySeconds(1800)
.refreshTokenValiditySeconds(60*60*2)
.resourceIds("rid")
//作用域,范围
.scopes("all")
//密码
.secret("$2a$10$dBcNmmqNjcp94160vLlfrOHHdh8LadpnzG9QyT18rwwRoCwe14w1a");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
//身份验证管理
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许客户端表单身份验证
security.allowFormAuthenticationForClients();
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
创建资源服务
- 实现ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer //表示开启资源服务
public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("rid");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated();
}
}
配置Security配置类
@Configuration
public class WebSecurityConfigurerAdapter extends org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter {
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
return super.userDetailsService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password(
"$2a$10$dBcNmmqNjcp94160vLlfrOHHdh8LadpnzG9QyT18rwwRoCwe14w1a").roles(
"admin")
.and()
.withUser("user").password(
"$2a$10$dBcNmmqNjcp94160vLlfrOHHdh8LadpnzG9QyT18rwwRoCwe14w1a").roles(
"user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**")
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
}
创建Controller
@RestController
public class HelloController {
@GetMapping("/admin/hello")
public String admin(){
return "hello admin";
}
@GetMapping("/user/hello")
public String user(){
return "hello user";
}
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
测试
- 获取访问资源服务的token
- 访问资源服务
- 刷新token