1、什么是 OAuth 2?
- OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。
原文出自:www.hangge.com 转载请保留原文链接:https://www.hangge.com/blog/cache/detail_2683.html - 每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。这样,OAuth 让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容。目前主流的 qq,微信等第三方授权登录方式都是基于 OAuth2 实现的。
- OAuth 2 是 OAuth 协议的下一版本,但不向下兼容 OAuth 1.0。OAuth 2 关注客户端开发者的简易性,同时为 Web 应用、桌面应用、移动设备、起居室设备提供专门的认证流程。
- 传统的 Web 开发登录认证一般都是基于 Session 的,但是在前后端分离的架构中继续使用 Session 会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持Cookie(微信小程序),要么使用非常不便,对于这些问题,使用 OAuth 2 认证都能解决。
2、OAuth 2 角色
OAuth 2
标准中定义了以下几种角色: - 资源所有者(Resource Owner):即代表授权客户端访问本身资源信息的用户,客户端访问用户帐户的权限仅限于用户授权的“范围”
- 客户端(Client):即代表意图访问受限资源的第三方应用。在访问实现之前,它必须先经过用户者授权,并且获得的授权凭证将进一步由授权服务器进行验证。
- 授权服务器(Authorization Server):授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
- 资源服务器(Resource Server):资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。
3、OAuth 2 授权流程
下面是 OAuth 2 一个大致的授权流程图:
4、OAuth 2 授权模式
OAuth 协议的授权模式共分为 4 种,分别说明如下:
- 授权码模式:授权码模式(authorization
code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本都是使用这种模式。 - 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器中请令牌,一般若网站是纯静态页面,则可以采用这种方式。
- 密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器中请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。
1,添加依赖
由于 Spring Boot
中的 OAuth 协议是在 Spring Security
基础上完成的。因此首先编辑 pom.xml,添加 Spring Security 以及 OAuth 依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
2,配置授权服务器
下面是授权服务器配置代码。创建一个自定义类继承自 AuthorizationServerConfigurerAdapter
,完成对授权服务器的配置,然后通过 @EnableAuthorizationServer
注解开启授权服务器:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 该对象用来支持 password 模式
@Autowired
AuthenticationManager authenticationManager;
// 该对象用来将令牌信息存储到内存中
@Autowired(required = false)
TokenStore inMemoryTokenStore;
// 该对象将为刷新token提供支持
@Autowired
UserDetailsService userDetailsService;
// 指定密码的加密方式
@Bean
PasswordEncoder passwordEncoder() {
// 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
return new BCryptPasswordEncoder();
}
// 配置 password 授权模式
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password", "refresh_token") //授权模式为password和refresh_token两种
.accessTokenValiditySeconds(1800) // 配置access_token的过期时间
.resourceIds("rid") //配置资源id
.scopes("all")
.secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密后的密码
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(inMemoryTokenStore) //配置令牌的存储(这里存放在内存中)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
// 表示支持 client_id 和 client_secret 做登录认证
security.allowFormAuthenticationForClients();
}
}
3,配置资源服务器
接下来配置资源服务器。自定义类继承自 ResourceServerConfigurerAdapter
,并添加 @EnableResourceServer
注解开启资源服务器配置。
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("rid") // 配置资源id,这里的资源id和授权服务器中的资源id一致
.stateless(true); // 设置这些资源仅基于令牌认证
}
// 配置 URL 访问权限
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated();
}
}
4,配置 Security
这里 Spring Security 的配置与传统的 Security 大体相同,不同在于:
- 这里多了两个
Bean
,这两个Bean
将注入授权服务器配置类中使用。 - 另外,这里的
HttpSecurity
配置主要是配置“oauth/**”模式的 URL,这一类的请求直接放行。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
@Override
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
.roles("admin")
.and()
.withUser("sang")
.password("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq") //123
.roles("user");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**").authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and().csrf().disable();
}
}
5,添加测试接口
接着在 Conctoller
中添加如下三个接口用于测试,它们分别需要 admin 角色、use 角色以及登录后访问。
@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";
}
}
6,开始测试
(1)启动项目,首先通过 POST 请求获取 token:
- 请求地址:oauth/token
- 请求参数:用户名、密码、授权模式、客户端 id、scope、以及客户端密码
- 返回结果:
access_token
表示获取其它资源是要用的令牌,refresh_token
用来刷新令牌,expires_in
表示access_token
过期时间。
(2)当access_token
过期后,可以使用refresh_token
重新获取新的access_token
(前提是access_token
未过期),这里也是 POST 请求: - 请求地址:oauth/token(不变)
- 请求参数:授权模式(变成了
refresh_token
)、refresh_token
、客户端 id、以及客户端密码 - 返回结果:与获取前面登录获取 token 返回的内容项一样。不过每次请求,
access_token
和access_token
有效期都会变化。
(3)访问资源时,我们只需要携带上access_token
参数即可:
(4)如果非法访问一个资源,比如 admin 用户访问“user/hello
”接口,结果如下:
1,增加依赖
1)首先编辑 pom.xml
文件,在前面的基础上增加 Redis
相关依赖:
<!-- spring security OAuth2 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
(2)接着在 application.properties 中配置 Redis 连接信息:
# 基本连接信息配置
spring.redis.database=0
spring.redis.host=192.168.60.133
spring.redis.port=6379
spring.redis.password=123
# 连接池信息配置
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.max-wait=-1ms
spring.redis.jedis.pool.min-idle=0
2,修改授权服务器配置
最后我们修改授权服务器配置 AuthorizationServerConfig
中令牌保存相关代码,将其改成保存到 Redis 上即可。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// 该对象用来支持 password 模式
@Autowired
AuthenticationManager authenticationManager;
// 该对象用来将令牌信息存储到Redis中
@Autowired
RedisConnectionFactory redisConnectionFactory;
// 该对象将为刷新token提供支持
@Autowired
UserDetailsService userDetailsService;
// 指定密码的加密方式
@Bean
PasswordEncoder passwordEncoder() {
// 使用BCrypt强哈希函数加密方案(密钥迭代次数默认为10)
return new BCryptPasswordEncoder();
}
// 配置 password 授权模式
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password", "refresh_token") //授权模式为password和refresh_token两种
.accessTokenValiditySeconds(1800) // 配置access_token的过期时间
.resourceIds("rid") //配置资源id
.scopes("all")
.secret("$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq"); //123加密后的密码
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory)) //配置令牌存放在Redis中
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
// 表示支持 client_id 和 client_secret 做登录认证
security.allowFormAuthenticationForClients();
}
}
3,运行测试
启动项目,再次通过 /oauth/token
接口获取令牌。然后查看下 Redis 中的数据,可以发现令牌确实以及保存在 Redis 上了: