1. 介绍

本插件是提供作为oauth2授权服务器,资源服务器的功能.

使用本插件前,确认已理解oauth2的相关概念,建议阅读如下三篇入门文章:

2. 使用

使用在线initilizer工具时,会随着gb组件的选择自动添加 gradle中

implementation('org.yunchen.gb:gb-plugin-springsecurity-oauth2provider:1.4.0.0.M1')

3. 配置

配置application.yml文件

gb:
    oauth2provider:
      active: true
      delimiter: '?'
      registerExceptionTranslationFilter: true
      registerStatelessFilter: true
      registerBasicAuthenticationFilter: true
      onlySupportStatelessOAuth2Authentication: false  (1)
      realmName: gb OAuth2 Realm
      credentialsCharset: UTF-8
      authorizationEndpointUrl: /oauth2/authorize
      tokenEndpointUrl:  /oauth2/token
      userApprovalEndpointUrl: /oauth2/confirm_access
      userApprovalParameter: user_oauth_approval
      errorEndpointUrl: /oauth2/error
      tokenServices:
        registerTokenEnhancers: true
        #accessTokenValiditySeconds: 60*60*12   # 12小时
        #refreshTokenValiditySeconds: 60*60*24*30 #30天
        reuseRefreshToken: false
        supportRefreshToken: true
      grantTypes:
        authorizationCode: true
        implicit: true
        refreshToken: true
        clientCredentials: true
        password: true
      authorization:
        requireRegisteredRedirectUri: true
        requireScope: true
      approval:
        auto: EXPLICIT  # TOKEN_STORE  APPROVAL_STORE  EXPLICIT  (2)
        #approvalValiditySeconds: 60 * 60 * 24 * 30  #30天
        scopePrefix: scope.
      authorizationCodeLookup:
        className: org.yunchen.gb.example.*****.domain.oauth2.provider.AuthorizationCode
        authenticationPropertyName: authentication
        codePropertyName: code
      accessTokenLookup:
        className: org.yunchen.gb.example.*****.domain.oauth2.provider.AccessToken
        authenticationKeyPropertyName: authenticationKey
        authenticationPropertyName: authentication
        usernamePropertyName: username
        clientIdPropertyName: clientId
        valuePropertyName: value
        tokenTypePropertyName: tokenType
        expirationPropertyName: expiration
        refreshTokenPropertyName: refreshToken
        scopePropertyName: scope
        additionalInformationPropertyName: additionalInformation
      refreshTokenLookup:
        className: org.yunchen.gb.example.*****.domain.oauth2.provider.RefreshToken
        authenticationPropertyName: authentication
        valuePropertyName: value
        expirationPropertyName: expiration
      clientLookup:
        className: org.yunchen.gb.example.*****.domain.oauth2.provider.Client
        clientIdPropertyName: clientId
        clientSecretPropertyName: clientSecret
        accessTokenValiditySecondsPropertyName: accessTokenValiditySeconds
        refreshTokenValiditySecondsPropertyName: refreshTokenValiditySeconds
        authoritiesPropertyName: authorities
        authorizedGrantTypesPropertyName: authorizedGrantTypes
        resourceIdsPropertyName: resourceIds
        scopesPropertyName: scopes
        autoApproveScopesPropertyName: autoApproveScopes
        redirectUrisPropertyName: redirectUris
        additionalInformationPropertyName: additionalInformation
      approvalLookup:
        className: org.yunchen.gb.example.*****.domain.oauth2.provider.Approval
        usernamePropertyName: username
        clientIdPropertyName: clientId
        scopePropertyName: scope
        approvedPropertyName: approved
        expirationPropertyName: expiration
        lastModifiedPropertyName: lastModified
1 此处是配置项目只支持无状态验证的.改为true后,将不支持session,cookie模式,工程无法从页面登录
2 默认的批准级别.若改为APPROVAL_STORE,则会查看Client表中的auto_approval设置
需要将以上配置文件中的*位置的package更换为实际项目的名称

4. 开发

创建相应的domain类,存储验证数据

4.1. AccessToken类

import org.yunchen.gb.core.annotation.Title
import grails.gorm.annotation.Entity

@Entity
@Title(zh_CN = "AccessToken")
class AccessToken {
    String authenticationKey
    byte[] authentication

    String username
    String clientId

    String value
    String tokenType

    Date expiration
    Map<String, Object> additionalInformation

    static hasOne = [refreshToken: String]
    static hasMany = [scope: String]

    static constraints = {
        username nullable: true
        clientId nullable: false, blank: false
        value nullable: false, blank: false, unique: true
        tokenType nullable: false, blank: false
        expiration nullable: false
        scope nullable: false
        refreshToken nullable: true
        authenticationKey nullable: false, blank: false, unique: true
        authentication nullable: false, minSize: 1, maxSize: 1024 * 4
        additionalInformation nullable: true
    }

    static mapping = {
        version false
        scope lazy: false
    }
}

4.2. Approval 类

import org.yunchen.gb.core.annotation.Title
import grails.gorm.annotation.Entity

@Entity
@Title(zh_CN = "Approval")
class Approval {

    String username
    String clientId

    String scope
    boolean approved

    Date expiration
    Date lastModified

    static constraints = {
        username nullable: false, blank: false
        clientId nullable: false, blank: false
        scope nullable: false, blank: false
        expiration nullable: false
        lastModified nullable: false
    }
}

4.3. AuthorizationCode 类

import org.yunchen.gb.core.annotation.Title
import grails.gorm.annotation.Entity

@Entity
@Title(zh_CN = "AuthorizationCode")
class AuthorizationCode {
    byte[] authentication
    String code

    static constraints = {
        code nullable: false, blank: false, unique: true
        authentication nullable: false, minSize: 1, maxSize: 1024 * 4
    }

    static mapping = {
        version false
    }
}

4.4. Client 类

import org.yunchen.gb.core.GbSpringUtils
import org.yunchen.gb.core.annotation.Title
import grails.gorm.annotation.Entity

@Entity
@Title(zh_CN = "Client")
class Client {
    private static final String NO_CLIENT_SECRET = ''


    String clientId
    String clientSecret

    Integer accessTokenValiditySeconds
    Integer refreshTokenValiditySeconds

    Map<String, Object> additionalInformation

    static hasMany = [
            authorities: String,
            authorizedGrantTypes: String,
            resourceIds: String,
            scopes: String,
            autoApproveScopes: String,
            redirectUris: String
    ]

    static constraints = {
        clientId blank: false, unique: true
        clientSecret nullable: true

        accessTokenValiditySeconds nullable: true
        refreshTokenValiditySeconds nullable: true

        authorities nullable: true
        authorizedGrantTypes nullable: true

        resourceIds nullable: true

        scopes nullable: true
        autoApproveScopes nullable: true

        redirectUris nullable: true
        additionalInformation nullable: true
    }

    def beforeInsert() {
        encodeClientSecret()
    }

    def beforeUpdate() {
        if(isDirty('clientSecret')) {
            encodeClientSecret()
        }
    }

    protected void encodeClientSecret() {
        clientSecret = clientSecret ?: NO_CLIENT_SECRET
        clientSecret = GbSpringUtils.getBean("passwordEncoder").encode(clientSecret)?:clientSecret
    }
}

4.5. RefreshToken 类

import org.yunchen.gb.core.annotation.Title
import grails.gorm.annotation.Entity

@Entity
@Title(zh_CN = "RefreshToken")
class RefreshToken {
    String value
    Date expiration
    byte[] authentication

    static constraints = {
        value nullable: false, blank: false, unique: true
        expiration nullable: true
        authentication nullable: false, minSize: 1, maxSize: 1024 * 4
    }

    static mapping = {
        version false
    }
}

4.6. 模拟应用网址注册

在Startup类的init方法中添加:

        Client client = new Client(
                clientId: 'my-client',
                clientSecret: '123456789',
                authorizedGrantTypes: ['authorization_code', 'refresh_token', 'implicit', 'password', 'client_credentials'],
                authorities: ['ROLE_CLIENT'],
                scopes: ['read', 'write'],
                redirectUris: ['http://demo.groovyboot.org/business/client/callback']
        );
        client.save(flush: true)
        client.addToAutoApproveScopes("read")
        client.save(flush: true)

4.7. 创建页面

在thymeleaf3/oauth2目录下创建两个页面

4.7.1. confirm_access.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <title>Confirm Access</title>
</head>

<body>
    <div th:if="${lastException==null}" style="text-align: center">
            <div class='fheader'>请确认</div>
            <div><span sec:authentication="principal.username"/>: 您是否授权 <span th:text="${client_id}"></span> 读取您收保护的资源信息.</div>
            <p th:text="'响应类型:'+${response_type}"></p>
            <p th:text="'权限范围:'+${scope}"></p>
            <form method='POST' id='confirmationForm' class='cssform'>
                <p>
                    <input name='user_oauth_approval' type='hidden' value='true' />
                    <label><input name="authorize" value="同意" type="submit" /></label>
                </p>
            </form>
            <form method='POST' id='denialForm' class='cssform'>
                <p>
                    <input name='user_oauth_approval' type='hidden' value='false' />
                    <label><input name="deny" value="拒绝" type="submit" /></label>
                </p>
            </form>
    </div>
</body>
</html>

4.7.2. error.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"  >
<head>
    <title>OAuth2 Error</title>
</head>
<body>
<h1>OAuth2 Error</h1>

<div id="error" th:text="${error.summary}"></div>
</body>
</html>

5. 使用oauth2

启动项目.

以下模拟’authorization_code + refresh_token', implicit, password, 'client_credentials’四种方式使用oauth2

5.1. authorization_code +refresh_token

response_type是code

5.1.1. 获取code

用户端被引导访问

http://localhost:8080/oauth2/authorize?response_type=code&client_id=my-client&scope=read

用户登录系统后,会引导至授权页面,

用户点击界面中的同意按钮,会被引导至:

http://demo.groovyboot.org/business/client/callback?code=E3LRmu

其中地址是Client注册的回调地址, E3LRmu为code的值

5.1.2. 换取access_token 和 refresh_token

http://demo.groovyboot.org/business 这个服务的应用在服务器端与oauth2服务器通信, 用code换取refresh_token

访问:

curl -X POST \
     -d "client_id=my-client" \
     -d "grant_type=authorization_code" \
     -d "code=139R59" http://localhost:8080/oauth2/token

会得到类似如下的json:

{
    "access_token": "a1ce2915-8d79-4961-8abb-2c6f0fdb4aba",
    "token_type": "bearer",
    "refresh_token": "6540222d-0fb9-4b01-8d45-7be2bdfb68f9",
    "expires_in": 43199,
    "scope": "read"
}
以后就可以使用access_token访问oauth2服务器,或是resource资源服务器了.

5.1.3. 使用refresh_token

访问地址,换取新的access_token和refresh_token

curl -X POST \
     -d "client_id=my-client" \
     -d "grant_type=refresh_token" \
     -d "refresh_token=269afd46-0b41-45c2-a920-7d5af8a38d56" \
     -d "scope=read" http://localhost:8080/oauth2/token

5.2. implicit

隐式模式相对简单,response_type是token

访问:

http://localhost:8080/oauth2/authorize?response_type=token&client_id=my-client&scope=read

用户登录系统后,会引导至授权页面,

用户点击界面中的同意按钮,会被引导至:

http://demo.groovyboot.org/business/client/callback#access_token=d9bb2020-e569-4f2b-9e7f-54fbb677692d&token_type=bearer&expires_in=43199

5.3. password

密码模式会明码传递用户的用户名及密码,建议只用于服务器间通讯.

curl -X POST \
     -d "client_id=my-client" \
     -d "grant_type=password" \
     -d "username=my-user" \
     -d "password=my-password" \
     -d "scope=read" http://localhost:8080/oauth2/token

5.4. client_credentials

此模式也只建议在服务器间通讯,需要在进行如下访问前获取登录后的JSESSIONID并放入cookie

curl -X POST \
     -d "client_id=my-client" \
     -d "client_Secret=123456789" \
     -d "grant_type=client_credentials" \
     -d "scope=read" http://localhost:8080/oauth2/token
也可访问时传入basic Auth认证

6. 事件订阅

内部事件AppOauth2ApprovalAgreeEvent、AppOauth2ApprovalDenyEvent都是事件基类AppEvent的子类,是在用户授权点击“同意”或“拒绝”按钮 触发的服务端事件。

详细订阅细节可参看Core.html的事件订阅部分