Spring Security Oauth2-授权码模式(Finchley版本)

一、授权码模式原理解析(来自理解OAuth 2.0)

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。其具体的流程如下:
授权码模式流程图
具体步骤:

  • A:用户访问客户端(client),客户端告知浏览器(user-Agent)重定向到授权服务器
  • B:呈现授权界面给用户,用户选择是否给予客户端授权
  • C:假设用户给予授权,授权服务器(Authorization Server)将用户告知浏览器重定向(重定向地址为Redirection URI)到客户端,同时附上授权码(code)
  • D:客户端收到授权码,附上早先的重定向URL(Redirection URI),向授权服务器申请令牌(access token),这一步在客户端的后台的服务器上完成,对用户不可见
  • E:授权服务器核对授权码(code)和重定向URI,确认无误后,向客户端发放(access token)和更新令牌(refresh token)

在步骤A中客户端告知浏览器重定向到授权服务器的URI包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为"code"
  • client_id:表示客户端的ID,必选项
  • client_secret:客户端的密码,可选
  • redirect_uri:表示重定向URI,可选项
  • scope:表示申请的权限范围,可选项
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
Host: server.example.com

在C步骤中授权服务器回应客户端的URI,包含以下参数:

  • code:表示授权码,该码有效期应该很短,通常10分钟,客户端只能使用一次,否则会被授权服务器拒绝,该码与客户端 ID 和 重定向 URI 是一一对应关系
  • state:如果客户端请求中包含这个参数,授权服务器的回应也必须一模一样包含这个参数
HTTP/1.1 302 Found
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
&state=xyz

在D步骤中客户端向授权服务器申请令牌的HTTP请求,包含以下参数:

  • grant_type:表示使用的授权模式,必选,此处固定值为“authorization_code”
  • code:表示上一步获得的授权吗,必选
  • redirect_uri:重定向URI,必选,与步骤 A 中保持一致
  • client_id:表示客户端ID,必选
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&client_id=s6BhdRkqt3
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

在E步骤中授权服务器发送的HTTP回复,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
    "access_token":"2YotnFZFEjr1zCsicMWpAA",
    "token_type":"example",
    "expires_in":3600,
    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
    "example_parameter":"example_value"
}

更新令牌

如果用户访问的时候,客户端的访问令牌access_token已经过期,则需要使用更新令牌refresh_token申请一个新的访问令牌。
客户端发出更新令牌的HTTP请求,包含以下参数:

  • grant_type:表示使用的授权模式,此处的值固定为”refresh_token”,必选项。
  • refresh_token:表示早前收到的更新令牌,必选项。
  • scope:表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。

二、授权码示例

示例代码包含授权服务和资源服务

服务名 端口号 说明
auth-server 8080 授权服务器
resource-server 8088 资源服务器

2.1 授权服务器

2.1.1 添加依赖

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

2.1.2 授权服务配置

/**
 * 授权服务器配置
 *
 * @author simon
 * @create 2018-10-29 11:51
 **/
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  UserDetailsService userDetailsService;

  // 使用最基本的InMemoryTokenStore生成token
  @Bean
  public TokenStore memoryTokenStore() {
    return new InMemoryTokenStore();
  }

  /**
   * 配置客户端详情服务
   * 客户端详细信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
   * @param clients
   * @throws Exception
   */
  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
            .withClient("client1")//用于标识用户ID
            .authorizedGrantTypes("authorization_code","refresh_token")//授权方式
            .scopes("test")//授权范围
            .secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"));//客户端安全码,secret密码配置从 Spring Security 5.0开始必须以 {bcrypt}+加密后的密码 这种格式填写;
  }

  /**
   * 用来配置令牌端点(Token Endpoint)的安全约束.
   * @param security
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    /* 配置token获取合验证时的策略 */
    security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
  }

  /**
   * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
   * @param endpoints
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // 配置tokenStore,需要配置userDetailsService,否则refresh_token会报错
    endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore()).userDetailsService(userDetailsService);
  }
}

2.1.3 spring security配置

/**
 * 配置spring security
 *
 * @author simon
 * @create 2018-10-29 16:25
 **/
@EnableWebSecurity//开启权限验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  /**
   * 配置这个bean会在做AuthorizationServerConfigurer配置的时候使用
   * @return
   * @throws Exception
   */
  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }

  /**
   * 配置用户
   * 使用内存中的用户,实际项目中,一般使用的是数据库保存用户,具体的实现类可以使用JdbcDaoImpl或者JdbcUserDetailsManager
   * @return
   */
  @Bean
  @Override
  protected UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("admin").password(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("admin")).authorities("USER").build());
    return manager;
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService());
  }
}

2.1.4 开启授权服务

在启动类上添加注解@EnableAuthorizationServer开启授权服务

2.2 资源服务

2.2.1 添加依赖

<?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>

  <groupId>priv.simon.resource</groupId>
  <artifactId>resource-server</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>resource-server</name>
  <description>资源服务器</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.6.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.SR2</spring-cloud.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-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </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>

2.2.2 配置资源服务

auth-server-url: http://localhost:8080 # 授权服务地址

server:
  port: 8088
security:
  oauth2:
    client:
      client-id: client1
      client-secret: 123456
      scope: test
      access-token-uri: ${auth-server-url}/oauth/token
      user-authorization-uri: ${auth-server-url}/oauth/authorize
    resource:
      token-info-uri: ${auth-server-url}/oauth/check_token #检查令牌

2.2.3 开启资源服务

在启动类上新增注解@EnableResourceServer开启资源服务,并提供资源获取接口


@EnableResourceServer
@RestController
@SpringBootApplication
public class ResourceServerApplication {

  private static final Logger log = LoggerFactory.getLogger(ResourceServerApplication.class);

  public static void main(String[] args) {
    SpringApplication.run(ResourceServerApplication.class, args);
  }

  @GetMapping("/user")
  public Authentication getUser(Authentication authentication) {
    log.info("resource: user {}", authentication);
    return authentication;
  }
}

2.3 测试

  1. 获取授权码
    发送GET请求获取授权码,回调地址随意写就可以
http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://baidu.com

如果没有登录,浏览器会重定向到登录界面
登录界面
输入用户名和密码(admin/admin)点击登录,这时会进入授权页
授权页面
点击授权,浏览器会从定向到回调地址上,携带code参数
授权成功

  1. 获取令牌
    通过post请求获取令牌
    postman请求
    请求失败,报401 authentication is required的错误

经过研究发现:
/oauth/token端点:

  • 这个如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter来保护
  • 如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护

那么授权服务器配置修改如下:

/**
   * 用来配置令牌端点(Token Endpoint)的安全约束.
   * @param security
   * @throws Exception
   */
  @Override
  public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    /* 配置token获取合验证时的策略 */
    security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()").allowFormAuthenticationForClients();
  }

重新发送请求
postman请求

返回数据如下:

{
    "access_token": "c0fa303f-77ad-427b-8f50-c5b3e031d109",
    "token_type": "bearer",
    "refresh_token": "a9b4a4c5-4e27-4328-ae1b-91cdd7c85f15",
    "expires_in": 43199,
    "scope": "test"
}
  1. 从资源服务获取资源
    携带access_token参数请求资源
http://localhost:8088/user?access_token=c0fa303f-77ad-427b-8f50-c5b3e031d109

结果返回:

{
    "authorities": [
        {
            "authority": "ROLE_test"
        }
    ],
    "details": {
        "remoteAddress": "0:0:0:0:0:0:0:1",
        "sessionId": null,
        "tokenValue": "c0fa303f-77ad-427b-8f50-c5b3e031d109",
        "tokenType": "Bearer",
        "decodedDetails": null
    },
    "authenticated": true,
    "userAuthentication": {
        "authorities": [
            {
                "authority": "ROLE_test"
            }
        ],
        "details": null,
        "authenticated": true,
        "principal": "admin",
        "credentials": "N/A",
        "name": "admin"
    },
    "principal": "admin",
    "credentials": "",
    "oauth2Request": {
        "clientId": "client1",
        "scope": [
            "test"
        ],
        "requestParameters": {
            "client_id": "client1"
        },
        "resourceIds": [],
        "authorities": [],
        "approved": true,
        "refresh": false,
        "redirectUri": null,
        "responseTypes": [],
        "extensions": {},
        "refreshTokenRequest": null,
        "grantType": null
    },
    "clientOnly": false,
    "name": "admin"
}
  1. 刷新token
    刷新token

github下载源码

猜你喜欢

转载自blog.csdn.net/AaronSimon/article/details/83546827