一、OAuth 2简介
OAuth是一个开放的标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无须将用户名和密码提供给第三方应用。实现这一功能是通过提供一个令牌(token) ,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时间段内访问特定的资源 。这样,OAuth让用户可以授权第三方网站灵活地访问存储在另外一些资源服务器的特定信息,而非所有内容 。例如用户想通过QQ登录知乎,这时知乎就是一个第三方应用,知乎要访问用户的一些基本信息就需要得到用户的授权,如果用户把自己的QQ用户名和密码告诉知乎,那么知乎就能访问用户的所有数据,并且只有用户修改密码才能收回授权,这种授权方式安全隐患很大,如果使用OAuth,就能很好地解决这一问题。 采用令牌的方式可以让用户灵活地对第三方应用授权或者收回权限 。OAuth2是OAuth协议的下一版本,但不向下兼容OAuth1.0。OAuth2关注客户端开发者的简易性,同时为Web应用、桌面应用、移动设备、起居室设备提供专门的认证流程。传统的Web开发登录认证一般都是基于Session的,但是在前后端分离的架构中继续使用Session会有许多不便 ,因为移动端(Android、iOS、微信小程序等),要么使用非常不便,对于这些问题,使用OAuth 2认证都能解决。
二、OAuth2 角色
要了解OAuth2,需要先了解OAuth 2中几个基本的角色。
- 资源所有者: 资源所有者即用户,具有头像、照片、视频等资源。
- 客户端:客户端即第三方应用,例如上文提到的知乎。
- 授权服务器:授权服务器用来验证用户提供的信息是否正确,并返回一个令牌给第三方应用。
- 资源服务器:资源服务器是提供给用户资源的服务器,例如头像、照片、视频等。
一般来说,授权服务器和资源服务器可以是同一台服务器。
三、OAuth 2授权流程
OAuth2的授权流程到底是什么样的呢?如下图 这是OAuth2一个大致的授权流程图,具体步骤如下:
- 客户端(第三方应用)向用户请求授权。
- 用户单击客户端所呈现的服务授权页面上的同意授权按钮后,服务端返回一个授权许可凭证给客户端。
- 客户端拿着授权许可凭证去授权服务器申请令牌。
- 授权服务器验证信息无误后,发放令牌给客户端。
- 客户端拿着令牌去资源服务器访问资源。
- 资源服务器验证令牌无误后开放资源。
这是一个大致的流程,因为OAuth2中有4中不同的授权模式,每种授权模式的授权流程又会有差异,基本流程如图所示:
四、授权模式
OAuth协议的授权模式共分为4种,分别说明如下:
- 授权码模式:授权码模式(authorization code)是功能最完整、流程最严谨的授权模式。它的特点就是通过客户端的服务器与授权服务器进行交互,国内常见的第三方平台登录功能基本都是使用这种模式。
- 简化模式:简化模式不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌,一般若网站是纯静态页面,则可以采用这种方式。
- 密码模式:密码模式就是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌。这需要用户对客户端高度信任,例如客户端应用和服务提供商是同一家公司。
- 客户端模式:客户端模式是指客户端使用自己的名义而不是用户的名义向服务提供者申请授权。严格来说,客户端模式并不能算作OAuth协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者移动端提供的认证授权服务器上使用这种模式还是非常方便的。
这4种模式各有千秋,分别适用于不同的开发场景,开发者要根据实际情况进行选择。
五、实践
本案例要介绍的是在前后端分离应用(或者移动端、微信小程序等)提供的认证服务器中如何搭建OAuth服务,因此主要介绍密码模式。搭建步骤如下:
- 创建项目,添加依赖
创建Spring Boot Web项目,添加如下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.oauthtest</groupId>
<artifactId>oauthtest</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.3.RELEASE</version>
</dependency>
</dependencies>
</project>
由于SpringBoot中的OAuth协议是在Spring Security的基础上完成的,因此首先要添加Spring Security依赖,要用到OAuth2,因此添加OAuth 2相关依赖,令牌可以存储在Redis缓存服务器上,同时Redis具有过期等功能,很适合令牌的存储,因此也加入Redis依赖。 项目创建成功后,接下来在application.properties中配置一下Redis服务器的连接信息,代码如下:
spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456
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
- 配置授权服务器
授权服务器和资源服务器可以是同一台服务器,也可以不同服务器,本案例中假设是同一台服务器,通过不同的配置分别开启授权服务器和资源服务器,首先是授权服务器:
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
@Autowired
UserDetailsService userDetailsService;
@Bean
PasswordEncoder passwordEncoder() {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123");
System.out.println("password = " + password);
return new BCryptPasswordEncoder();
}
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("password")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(1800)
.resourceIds("rid")
.scopes("all")
.secret("$2a$10$zozsK7CgtOeinXRdOwiiCOjZsICagZBA1yvJ2PrUiMBpwlUz4zpCa");
}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
}
- 配置资源服务器
接下里配置资源服务器,代码如下:
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("rid").stateless(true);
}
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated();
}
}
- 配置Security
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
@Bean
protected UserDetailsService userDetailsService() {
return super.userDetailsService();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("$2a$10$zozsK7CgtOeinXRdOwiiCOjZsICagZBA1yvJ2PrUiMBpwlUz4zpCa")
.roles("admin")
.and()
.withUser("sang")
.password("$2a$10$zozsK7CgtOeinXRdOwiiCOjZsICagZBA1yvJ2PrUiMBpwlUz4zpCa")
.roles("user");
}
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/oauth/**").authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.and()
.csrf()
.disable();
}
}
这里Spring Security的配置基本上和前文一致,唯一不同的是多了两个Bean,这里两个Bean将注入服务器配置类中使用。另外,这里的HttpSecurity配置主要是配置“/oauth/** ”模式的URL,这样一类的请求直接方行。在Spring Security配置和资源服务器配置中,一共涉及两个HttpSecurity,其中Spring Security中的配置优先级高度资源服务器中的位置,即请求地址先经过Spring Security的HttpSecurity,再经过资源服务器的HttpSecurity。 5. 测试验证
@Controller
public class HelloController {
@GetMapping("/admin/hello")
@ResponseBody
public String admin() {
return "hello admin!";
}
@GetMapping("/user/hello")
@ResponseBody
public String user() {
return "hello user!";
}
@GetMapping("/db/hello")
@ResponseBody
public String dba() {
return "hello dba!";
}
}
- 实验结果
根据前文的配置,要请求这三个地址,分别需要admin角色、user角色以及登录后访问的。 所有配置完成后,启动Redis服务器,再启动Spring Boot项目,首先发送一个POST请求获取token, 请求地址如下(注意这里是一个POST请求,为了显示方便,将参数写在地址栏中) :http://localhost:8080/oauth/token?username=admin&password=123&grant_type=password&client_id=password&scope=all&client_secret=123 请求地址中包含参数有用户名、密码、授权模式、客户端id、scope以及客户端密码,基本就是授权 服务器中所配置的数据,请求结果如下: 返回结果有access_token、token_type、refresh_token、expires_in、scope ,其中access_token是获取其他资源时要用的令牌,refresh_token用来刷新令牌,expires_in表示access_token的过期时间,当access_token过期后,使用refresh_token重新获取新的access_token(前提是refresh_token未过期),请求地址如下(注意这里是POST请求): http://localhost:8080/oauth/token?username=sang&password=123&grant_type=refresh_token&refresh_token=3b975d69-61b3-47d6-b9ba-0b322f4e3dd7&client_id=password&scope=all&client_secret=123 获取新的access_token时需要携带上refresh_token,同时授权模式设置为refresh_token,在获取结果中access_token会变化,同时access_token有效期也会变化,如下图:
访问结果如下:
- 问题
- authenticationManager无法注入问题
参照如下解决方法 在配置spring cloud security的过程中出现如下异常信息导致无法启动项目
Field authenticationManager in com.clark.online.edu.config.AuthorizationServerConfig required a bean of type 'org.springframework.security.authentication.AuthenticationManager' that could not be found.
Action:
Consider defining a bean of type 'org.springframework.security.authentication.AuthenticationManager' in your configuration.
看错误信息就很明白了,就是需要authenticationManager,但是没有在config里面注入,但是实际上在我的config配置里面已经注入了,代码如下:
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
RedisTokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(redisTokenStore()).authenticationManager(authenticationManager);
}
其实原因我不知道,解决方法很简单就是重新实例化bean就OK
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- 解决升级 Spring Boot 2.6后,因循环引用导致启动时报错 BeanCurrentlyInCreationException 的问题
在application.properties中添加如下配置:
spring.main.allow-circular-references=true
- Encoded password does not look like BCrypt 异常处理
spirng boot 1.5.* 升级到spring boot 2.0.*,当再次访问授权服务器时出现Encoded password does not look like BCrypt异常 分析:断点跟踪,发现是密码格式不匹配导致的。 在系统调用matches的时候,会对密码的格式进行校验。 解决:修改密码加密的方式。示例: 原先: 改为: 或者可以采用这种方式生成密码,将生成的密码复制粘贴进行测试使用
|