1. Oauth2 authorization_code 认证协议说明
Oauth2 Authorization_code 模式是 Oauth2 认证中最完善的认证方式。其完整的认证模式如下:
- (A)用户访问客户端,客户端将用户引导向认证服务器。
- (B)用户选择是否给予客户端授权。
- (C)如用户给予授权,认证服务器将用户引导向客户端指定的redirection uri,同时加上授权码code。
- (D)客户端收到code后,通过后台的服务器向认证服务器发送code和redirection uri。
- (E)认证服务器验证code和redirection uri,确认无误后,响应客户端访问令牌(access token)和刷新令牌(refresh token)
以上就是Oauth2 code 认证的基本方式!
2.SpringSecurtiy中 Oauth2 code认证
在SpringSecurtiy中 Oauth2 code认证过程基本上符合上面所说的Oauth2 authorization_code 协议的认证过程。SpringSecurity中整合了Oauth2协议,由于SpringSecurity的本质是一系列的过滤器链组成。所以在加入Oauth2协议以后,相当于在SpringSecurity的过滤器链中,加上Oauth2认证的过滤链实现认证服务。
3.SpringSecurtiy中 Oauth2 code的代码实现
3.1 Maven 依赖,spring-boot版本为:2.0.5.RELEASE
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR1</spring-cloud.version>
<mybatis.version>1.3.2</mybatis.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</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.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--redis连接池需要此依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 SpringSecurity 安全配置类实现:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().and().authorizeRequests()
.antMatchers("/oauth/**","/login")
.permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable();
}
@Bean
@Override
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("hello1").password(passwordEncoder.encode("123456")).authorities("USER").build());
manager.createUser(User.withUsername("hello2").password(passwordEncoder.encode("123456")).authorities("USER").build());
return manager;
}
}
3.3 资源服务器实现代码
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/oauth/**")
.permitAll()
.anyRequest()
.authenticated();
}
}
3.4 认证服务器实现代码
@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.realm("oauth2-resources")
//url:/oauth/token_key,exposes public key for token verification if using JWT tokens
.tokenKeyAccess("permitAll()")
//url:/oauth/check_token allow check token
.checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("secret"))
.redirectUris("http://example.com")
.authorizedGrantTypes("authorization_code", "client_credentials", "refresh_token",
"password", "implicit")
.scopes("all")
.resourceIds("oauth2-resource")
.accessTokenValiditySeconds(1200)
.refreshTokenValiditySeconds(50000);
}
}
整个认证服务器和资源服务器的代码基本上已经实现。下面我们进行测试配合code认证的流程进行说明。
4.请求认证
4.1 请求认证参数URL
http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://example.com&scop=all
参数说明:
client_id:客户端的ID,必选项
redirect_uri:重定向URI,必选项
scope:申请的权限范围,可选项
state:任意值,认证服务器会原样返回,用于抵制CSRF(跨站请求伪造)攻击。
如果当前用户没有登录的话,会重定向到登录页面,强制用户进行登录认证。
这个步骤相当于:
- (A)用户访问客户端,客户端将用户引导向认证服务器。
- (B)用户选择是否给予客户端授权。
登录后认证服务器会进行授权,授权通过之后会产生一个code码:
http://example.com/?code=03cQZu
code码只能使用一次。
注意:这里的redirect_uri=http://example.com 必须配置在ClientDetailsServiceConfigurer 中,不然会抛出异常:
{"error":"invalid_grant","error_description":"Redirect URI mismatch."}
4.2 携带code获取token
curl -i -d "grant_type=authorization_code&code=gnyW4f&client_id=client&client_secret=secret&redirect_uri=http://example.com" -X POST http://localhost:8080/oauth/token
请求参数说明:
client_id:客户端的ID,必选项。
client_secret:客户端的密钥,必选项。
grant_type:表示使用的授权模式,必选项。
refresh_token:表示早前收到的更新令牌,必选项。
redirect_uri:当前版本必选项
返回值:
{
"access_token": "10337f46-fe9b-4914-9f1b-15d3bfd6227b",
"token_type": "bearer",
"refresh_token": "1cc55626-9fc0-47ac-8f3f-685abd11deaf",
"expires_in": 990,
"scope": "all"
}
返回参数说明:
access_token:访问令牌,必选项。
token_type:令牌类型,该值大小写不敏感,必选项。
expires_in:过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
refresh_token:更新令牌,用来获取下一次的访问令牌,可选项。
scope:权限范围,如果与客户端申请的范围一致,此项可省略。
4.3 check_token
curl -i -X POST -H "Accept: application/json" -u "client:secret" http://localhost:8080/oauth/check_token?token=a388c2fa-2243-4b7a-b8c5-09973cde4fb2
返回值:
{
"aud": [
"oauth2-resource"
],
"user_name": "hello1",
"scope": [
"all"
],
"active": true,
"exp": 1539929197,
"authorities": [
"USER"
],
"client_id": "client"
}
4.4 refresh_token
curl -i -d "grant_type=refresh_token&refresh_token=8b6c262d-22cb-4e5b-af08-a32cbee5c5c2" -u "client:secret" -X POST http://localhost:8080/oauth/token
返回结果:
{
"access_token": "3de40530-a9cf-44ce-a0ef-1165a11add1d",
"token_type": "bearer",
"refresh_token": "8b6c262d-22cb-4e5b-af08-a32cbee5c5c2",
"expires_in": 1199,
"scope": "all"
}