Spring Boot 整合 Oauth 2.0 的密码模式实战(含完整代码)

小七学习网,助您升职加薪,遇问题可联系:客服微信【1601371900】 备注:来自网站

Oauth 2.0 有四种授权方式,常用方式是授权码和密码模式两种,我使用“密码模式”写了一个 Demo,功能有登录(即获取 Token)、刷新 Token、退出(销毁 Token)、创建用户、获取用…

Oauth 2.0 有四种授权方式,常用方式是授权码和密码模式两种,我使用“密码模式”写了一个 Demo,功能有登录(即获取 Token)、刷新 Token、退出(销毁 Token)、创建用户、获取用户列表、创建客户端。我会详细列出获取 Token、刷新 Token 等核心流程,并且会一步一步的讲解并贴出完整代码。

本文将会获得以下知识:

  1. Oauth 2.0 获取 Token 的流程
  2. Oauth 2.0 刷新 Token 的流程
  3. Oauth 2.0 销毁 Token 的流程
  4. Oauth 2.0 密码模式的完整代码

适合人群: Java 中级或以上开发。



前言

我所在项目组刚好接到一个权限控制的需求,具体需求是当用户登录后才能访问相关页面,经分析后,确认选用 Oauth 2.0 的密码模式来实现,用户登录后(即授权)发放一个唯一的令牌,调用接口时带上此令牌,后端验证令牌是否有效和是否过期,验证通过后才能调用后端接口,从而实现用户登录后才能访问相关页面需求。其实发放令牌、校验令牌等操作 Oauth 2.0 都已经封装好了,我们在 Spring Boot 工程中整合它即可使用了。

表设计、涉及概念介绍

数据库表结构

CREATE TABLE `user` (  `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'主键\',  `userName` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'用户名\',  `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'密码\',  `roles` tinyblob NOT NULL COMMENT \'角色(允许多个)\',  `lastPswdChangeTimestamp` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT \'密码最后修改时间\',  PRIMARY KEY (`id`),  UNIQUE KEY `userName` (`userName`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;CREATE TABLE `client` (  `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'主键\',  `accessTokenValiditySeconds` int(11) DEFAULT NULL COMMENT \'access token 有效期,单位:秒\',  `additionalInformation` mediumblob COMMENT \'token 额外信息\',  `authorities` tinyblob COMMENT \'授予客户端的权限,不是用户权限,与 scope 类似\',  `authorizedGrantTypes` tinyblob COMMENT \'授权方式,比如:password、refresh_token\',  `clientId` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'客户端 id\',  `clientSecret` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT \'客户端密钥\',  `refreshTokenValiditySeconds` int(11) DEFAULT NULL COMMENT \'refresh token 有效期,单位:秒\',  `registeredRedirectUri` tinyblob COMMENT \'路径匹配规则\',  `resourceId` tinyblob COMMENT \'资源 id,限制客户端可以访问的哪些资源服务,不设置则可以访问全部资源服务\',  `scope` tinyblob COMMENT \'作用域,即权限范围,比如:read、write\',  PRIMARY KEY (`id`),  UNIQUE KEY `clientId` (`clientId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;CREATE TABLE `accesstoken` (  `id` varchar(255) NOT NULL COMMENT \'主键\',  `authentication` mediumtext COMMENT \'序列化并 base64 编码后的认证信息\',  `authenticationId` varchar(255) NOT NULL COMMENT \'认证 id\',  `clientId` varchar(255) DEFAULT NULL COMMENT \'客户端 id\',  `refreshToken` mediumtext COMMENT \'MD5 后的 refreshToken\',  `token` mediumtext COMMENT \'序列化并 base64 编码后的 access_token\',  `tokenId` varchar(255) DEFAULT NULL COMMENT \'access_token\',  `username` varchar(255) DEFAULT NULL COMMENT \'用户名\',  PRIMARY KEY (`id`),  UNIQUE KEY `authenticationId` (`authenticationId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;CREATE TABLE `refreshtoken` (  `id` varchar(255) NOT NULL COMMENT \'主键\',  `authentication` mediumtext COMMENT \'序列化并 base64 编码后的认证信息\',  `token` mediumtext COMMENT \'序列化并 base64 编码后的 refresh_token\',  `tokenId` varchar(255) NOT NULL COMMENT \'refresh_token\',  PRIMARY KEY (`id`),  UNIQUE KEY `tokenId` (`tokenId`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

概念介绍

  • Oauth 2.0 密码模式:通过用户名和密码申请令牌,暴露密码给应用,此方式存在一定风险,适用于高度信任的应用。
  • Oauth 2.0 授权码模式:类似微信授权登录,先申请授权码 code,再通过 code 换取令牌。
  • 授权服务器:配置自定义的认证服务器、验证客户端 id 和密钥、发放和存储 token。
  • 资源服务器:配置什么角色可以访问什么资源。
  • 认证服务器:验证当前用户是否存在。
  • 令牌端点:授权成功后发送令牌的地方。
  • 校验端点:校验令牌是否合法,比如:传入的令牌到数据库查找是否存在并未过期。

Oauth 2.0 核心流程分析

获取 token 流程

1. 请求获取 token 接口,即 /oauth/token,参数需提供用户账号、密码(经过 base64 编码)、授权方式,请求提供客户端 id 和 secret,比如:

http://127.0.0.1:8080/oauth/token?username=oauth&password=b2F1dGgxMjM=&grant_type=password

Authorization:Basic ZGVtb0NsaWVudElkOmRlbW9DbGllbnRTZWNyZXQ=

在这里插入图片描述

2. 过滤器拦截请求,校验接口路径是否 /oauth/token,如果不是则提示 unauthorized,不继续执行直接返回。

在这里插入图片描述

3. 过滤器根据 clientId(即 demoClientId)查询数据库检查客户端 id 是否存在,存在则根据客户端和登录用户相关信息,组装 ClientDetails 接口和 Authentication 接口。

4. 请求被转发到 Oauth2 框架中的 TokenEndpoint.java 类的 postAccessToken 方法,校验客户端 id 与数据库存储的是否一致、校验客户端 scope 是否有权限、授权(即步骤 5)。

    TokenEndpoint.java 源代码:    @RequestMapping(        value = {\"/oauth/token\"},        method = {RequestMethod.POST}    )    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {        if (!(principal instanceof Authentication)) {            throw new InsufficientAuthenticationException(\"There is no client authentication. Try adding an appropriate authentication filter.\");        } else {            String clientId = this.getClientId(principal);            ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);            TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);            if (clientId != null && !clientId.equals(\"\") && !clientId.equals(tokenRequest.getClientId())) {                throw new InvalidClientException(\"Given client ID does not match authenticated client\");            } else {                if (authenticatedClient != null) {                    this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);                }                if (!StringUtils.hasText(tokenRequest.getGrantType())) {                    throw new InvalidRequestException(\"Missing grant type\");                } else if (tokenRequest.getGrantType().equals(\"implicit\")) {                    throw new InvalidGrantException(\"Implicit grant type not supported from token endpoint\");                } else {                    if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {                        this.logger.debug(\"Clearing scope of incoming token request\");                        tokenRequest.setScope(Collections.emptySet());                    }                    if (this.isRefreshTokenRequest(parameters)) {                        tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get(\"scope\")));                    }                    OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);                    if (token == null) {                        throw new UnsupportedGrantTypeException(\"Unsupported grant type: \" + tokenRequest.getGrantType());                    } else {                        return this.getResponse(token);                    }                }            }        }    }

5. 授权流程,先校验 authorizedGrantTypes 是否有权限(比如:password、refresh_token)、校验 username(即 oauth)用户是否存在数据库、创建 access_token 及保存到数据库、创建 refresh_token 及保存到数据库。

6. 授权成功后,返回 access_token 给客户端,客户端在参数中带上 access_token 就可以调用接口了。

刷新 token 流程

1. 请求刷新 token 接口,即 /oauth/token,参数需提供授权方式,refresh_token,请求提供客户端 id 和 secret,比如:

http://127.0.0.1:8080/oauth/token?granttype=refreshtoken&refresh_token=05d96c67-bba6-418c-9a09-dc0a9b39ae2a

Authorization:Basic ZGVtb0NsaWVudElkOmRlbW9DbGllbnRTZWNyZXQ=

在这里插入图片描述

2. 过滤器拦截请求,校验接口路径是否 /oauth/token,如果不是则提示 unauthorized,不继续执行直接返回。

3. 过滤器根据 clientId(即 demoClientId)查询数据库检查客户端 id 是否存在,存在则根据客户端和登录用户相关信息,组装 ClientDetails 接口和 Authentication 接口。

4. 请求被转发到 Oauth2 框架中的 TokenEndpoint.java 类的 postAccessToken 方法,校验客户端 id 与数据库存储的是否一致、校验客户端 scope 是否有权限、刷新 token(即步骤 5)。

5. 刷新 token,先校验 authorizedGrantTypes 是否有权限(比如:password、refresh_token)、校验 refresh_token 是否存在数据库、删除原来的 access_token 和 refresh_token 的数据库记录、创建 access_token 及保存到数据库、创建 refresh_token 及保存到数据库。

登出流程(即销毁 token)

1. 请求登出接口,接口名称随意,此处我定义为 /oauth/logout,参数只需在请求头提供 refresh_token,比如:

http://127.0.0.1:8080/oauth/logout Authorization:106dfc9e-0a2f-400b-be96-5b71ffa8d6b6

    /**     * 登出     * @param request     */    @PostMapping(value = \"/oauth/logout\")    public void logoutUser(HttpServletRequest request) {        String authorization = request.getHeader(\"Authorization\");        if (authorization != null && authorization.contains(\"Bearer\")) {            String tokenId = authorization.substring(\"Bearer\".length() + 1);            tokenService.revokeToken(tokenId);        }    }

2. 校验 access_token 是否存在数据库。

3. 判断 access_token 是否已过期。

4. 删除 access_token、refresh_token 的数据库记录、清除 Oauth 内部相关缓存。

具体开发过程、代码分析、接口测试

代码结构

在这里插入图片描述

工程引用 jar(即 pom.xml)

下面是工程需要使用的所有 jar 和 maven 打包策略。

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd\">    <modelVersion>4.0.0</modelVersion>    <groupId>com.example</groupId>    <artifactId>oauth2</artifactId>    <version>0.0.1-SNAPSHOT</version>    <name>oauth2</name>    <description>Demo project for Spring Boot</description>    <parent>        <groupId>org.springframework.boot</groupId>        <artifactId>spring-boot-starter-parent</artifactId>        <version>2.1.6.RELEASE</version>        <relativePath/>    </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>Greenwich.SR2</spring-cloud.version>    </properties>    <dependencies>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-data-jpa</artifactId>        </dependency>        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>            <scope>runtime</scope>        </dependency>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-oauth2</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-configuration-processor</artifactId>            <optional>true</optional>        </dependency>        <dependency>            <groupId>io.springfox</groupId>            <artifactId>springfox-swagger2</artifactId>            <version>2.6.1</version>        </dependency>        <dependency>            <groupId>io.springfox</groupId>            <artifactId>springfox-swagger-ui</artifactId>            <version>2.6.1</version>        </dependency>        <dependency>            <groupId>commons-validator</groupId>            <artifactId>commons-validator</artifactId>            <version>1.6</version>        </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>                <configuration>                    <executable>true</executable>                </configuration>            </plugin>        </plugins>    </build></project>

工程启动类和 application.yml 文件

下面是启动类比较简单加上 @SpringBootApplication 即可,配置文件也只是配置了 JPA 和数据库。

@SpringBootApplicationpublic class Oauth2Application {    public static void main(String[] args) {        SpringApplication.run(Oauth2Application.class, args);    }}server:  port: 8080spring:  jpa:    hibernate:      ddl-auto: updatemysql:  oauth:    datasource:      username: root      password: 123456      url: jdbc:mysql://127.0.0.1:3306/oauth?useSSL=false&useUnicode=yes&characterEncoding=utf-8&serverTimezone=GMT%2B8

操作数据库的实体类和 repository 接口

下面的实体类基本都是固定写法,与数据库字段一一对应,加上相关注解即可;repository 接口写操作自身实体类的自定义接口。

用户实体类:import org.hibernate.annotations.GenericGenerator;import javax.persistence.*;/** * 用户实体类 */@Entity@Table(name = \"user\")public class User {    @Id    @GeneratedValue(generator = \"system-uuid\")    @GenericGenerator(name = \"system-uuid\", strategy = \"uuid\")    private String id;    /**     * 用户名     */    @Column(name = \"userName\", nullable = false)    private String userName;    /**     * 密码,已通过 BCryptPasswordEncoder 进行加密     */    @Column(name = \"password\", nullable = false)    private String password;    /**     * 角色     */    @Column(name = \"roles\", nullable = false)    private String roles[];    /**     * 最后修改密码时间     */    @Column(name = \"lastPswdChangeTimestamp\")    private String lastPswdChangeTimestamp;    public User() {        this.id = null;    }    public String getUserName() {        return userName;    }    public void setUserName(String userName) {        this.userName = userName;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }    public String[] getRoles() {        return roles;    }    public void setRoles(String[] roles) {        this.roles = roles;    }    public String getLastPswdChangeTimestamp() {        return lastPswdChangeTimestamp;    }    public void setLastPswdChangeTimestamp(String lastPswdChangeTimestamp) {        this.lastPswdChangeTimestamp = lastPswdChangeTimestamp;    }}客户端实体类:import com.example.oauth2.util.ObjectToBytesAttributeConverter;import org.hibernate.annotations.GenericGenerator;import javax.persistence.*;import java.util.Map;/** * 客户端实体类 */@Entity@Table(name = \"client\")public class Client {    @Id    @GeneratedValue(generator = \"system-uuid\")    @GenericGenerator(name = \"system-uuid\", strategy = \"uuid\")    private String id;    /**     * 客户端 id     */    @Column(name = \"clientId\", nullable = false)    private String clientId;    /**     * 客户端密钥     */    @Column(name = \"clientSecret\", nullable = false)    private String clientSecret;    /**     * 资源 id,限制客户端可以访问的哪些资源服务,不设置则可以访问全部资源服务     */    @Column(name = \"resourceId\")    private String resourceId[];    /**     * 作用域,即权限范围,比如:read、write     */    @Column(name = \"scope\")    private String scope[];    /**     * 授权方式,比如:password、refresh_token     */    @Column(name = \"authorizedGrantTypes\")    private String authorizedGrantTypes[];    /**     * 路径匹配规则     */    @Column(name = \"registeredRedirectUri\")    private String registeredRedirectUri[];    /**     * access token 有效期,单位:秒     */    @Column(name = \"accessTokenValiditySeconds\")    private Integer accessTokenValiditySeconds;    /**     * refresh token 有效期,单位:秒     */    @Column(name = \"refreshTokenValiditySeconds\")    private Integer refreshTokenValiditySeconds;    /**     * 授予客户端的权限,不是用户权限,与 scope 类似     */    @Column(name = \"authorities\")    private String authorities[];    /**     * token 额外信息     */    @Column(name = \"additionalInformation\", columnDefinition = \"mediumblob\")    @Convert(converter = ObjectToBytesAttributeConverter.class)    private Map<String, Object> additionalInformation;    public Client() {        this.id = null;    }    public String getClientId() {        return clientId;    }    public void setClientId(String clientId) {        this.clientId = clientId;    }    public String getClientSecret() {        return clientSecret;    }    public void setClientSecret(String clientSecret) {        this.clientSecret = clientSecret;    }    public String[] getResourceId() {        return resourceId;    }    public void setResourceId(String[] resourceId) {        this.resourceId = resourceId;    }    public String[] getScope() {        return scope;    }    public void setScope(String[] scope) {        this.scope = scope;    }    public String[] getAuthorizedGrantTypes() {        return authorizedGrantTypes;    }    public void setAuthorizedGrantTypes(String[] authorizedGrantTypes) {        this.authorizedGrantTypes = authorizedGrantTypes;    }    public String[] getRegisteredRedirectUri() {        return registeredRedirectUri;    }    public void setRegisteredRedirectUri(String[] registeredRedirectUri) {        this.registeredRedirectUri = registeredRedirectUri;    }    public Integer getAccessTokenValiditySeconds() {        return accessTokenValiditySeconds;    }    public void setAccessTokenValiditySeconds(Integer accessTokenValiditySeconds) {        this.accessTokenValiditySeconds = accessTokenValiditySeconds;    }    public Integer getRefreshTokenValiditySeconds() {        return refreshTokenValiditySeconds;    }    public void setRefreshTokenValiditySeconds(Integer refreshTokenValiditySeconds) {        this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;    }    public String[] getAuthorities() {        return authorities;    }    public void setAuthorities(String[] authorities) {        this.authorities = authorities;    }    public Map<String, Object> getAdditionalInformation() {        return additionalInformation;    }    public void setAdditionalInformation(Map<String, Object> additionalInformation) {        this.additionalInformation = additionalInformation;    }}access_token 实体类:import org.hibernate.annotations.GenericGenerator;import javax.persistence.*;/** * access_token 实体类 */@Entity@Table(name = \"accesstoken\")public class AccessToken {    @Id    @GeneratedValue(generator = \"system-uuid\")    @GenericGenerator(name = \"system-uuid\", strategy = \"uuid\")    private String id;    /**     * access_token     */    @Column(name = \"tokenId\")    private String tokenId;    /**     * 序列化并 base64 编码后的 access_token     */    @Column(name = \"token\", columnDefinition = \"mediumtext\")    private String token;    /**     * 认证 id     */    @Column(name = \"authenticationId\", nullable = false)    private String authenticationId;    /**     * 用户名     */    @Column(name = \"username\")    private String username;    /**     * 客户端 id     */    @Column(name = \"clientId\")    private String clientId;    /**     * 序列化并 base64 编码后的认证信息     */    @Column(name = \"authentication\", columnDefinition = \"mediumtext\")    private String authentication;    @Column(name = \"refreshToken\", columnDefinition = \"mediumtext\")    private String refreshToken;    public AccessToken() {        this.id = null;    }    public String getTokenId() {        return tokenId;    }    public void setTokenId(String tokenId) {        this.tokenId = tokenId;    }    public String getToken() {        return token;    }    public void setToken(String token) {        this.token = token;    }    public String getAuthenticationId() {        return authenticationId;    }    public void setAuthenticationId(String authenticationId) {        this.authenticationId = authenticationId;    }    public String getUsername() {        return username;    }    public void setUsername(String username) {        this.username = username;    }    public String getClientId() {        return clientId;    }    public void setClientId(String clientId) {        this.clientId = clientId;    }    public String getAuthentication() {        return authentication;    }    public void setAuthentication(String authentication) {        this.authentication = authentication;    }    public String getRefreshToken() {        return refreshToken;    }    public void setRefreshToken(String refreshToken) {        this.refreshToken = refreshToken;    }}refresh_token 实体类:import org.hibernate.annotations.GenericGenerator;import javax.persistence.*;/** * refresh_token 实体类 */@Entity@Table(name = \"refreshtoken\")public class RefreshToken {    @Id    @GeneratedValue(generator = \"system-uuid\")    @GenericGenerator(name = \"system-uuid\", strategy = \"uuid\")    private String id;    /**     * refresh_token     */    @Column(name = \"tokenId\", nullable = false)    private String tokenId;    /**     * 序列化并 base64 编码后的 refresh_token     */    @Column(name = \"token\", columnDefinition = \"mediumtext\")    private String token;    /**     * 序列化并 base64 编码后的认证信息     */    @Column(name = \"authentication\", columnDefinition = \"mediumtext\")    private String authentication;    public RefreshToken() {        this.id = null;    }    public String getTokenId() {        return tokenId;    }    public void setTokenId(String tokenId) {        this.tokenId = tokenId;    }    public String getToken() {        return token;    }    public void setToken(String token) {        this.token = token;    }    public String getAuthentication() {        return authentication;    }    public void setAuthentication(String authentication) {        this.authentication = authentication;    }}用户 Repository 类:import com.example.oauth2.entry.User;import org.springframework.data.jpa.repository.JpaRepository;import java.util.List;public interface UserRepository extends JpaRepository<User, String> {    User findByUserName(String userName);    List<User> findAll();}客户端 Repository 类:import com.example.oauth2.entry.Client;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.transaction.annotation.Transactional;import java.util.List;public interface ClientRepository extends JpaRepository<Client, String> {    Client findByClientId(String clientId);    List<Client> findAll();    @Transactional    void deleteClientByClientId(String clientId);}访问令牌 Repository 类:import com.example.oauth2.entry.AccessToken;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.transaction.annotation.Transactional;import java.util.List;public interface AccessTokenRepository extends JpaRepository<AccessToken, String> {    AccessToken findOAuthAccessTokenByTokenId(String tokenId);    AccessToken findOAuthAccessTokenByAuthenticationId(String authenticationid);    List<AccessToken> findAll();    List<AccessToken> findAllByClientIdAndUsername(String clientId, String username);    List<AccessToken> findAllByClientId(String clientId);    @Transactional    void deleteAllByTokenId(String tokenId);    @Transactional    void deleteAllByRefreshToken(String refreshToken);}刷新令牌 Repository 类:import com.example.oauth2.entry.RefreshToken;import org.springframework.data.jpa.repository.JpaRepository;import org.springframework.transaction.annotation.Transactional;import java.util.List;public interface RefreshTokenRepository extends JpaRepository<RefreshToken, String> {    RefreshToken findOAuthRefreshTokenByTokenId(String tokenId);    List<RefreshToken> findAll();    @Transactional    void deleteOAuthRefreshTokenByTokenId(String tokenId);}

VO 类和工具类

下面是工程所用到的数据传输类;还有所有工具类,比如:统一响应类、序列化包装类、属性转换器类等。

用户 VO 类:public class UserVO {    private String userName;    private String password;    private String roles[];    public String getUserName() {        return userName;    }    public void setUserName(String userName) {        this.userName = userName;    }    public String getPassword() {        return password;    }    public void setPassword(String password) {        this.password = password;    }    public String[] getRoles() {        return roles;    }    public void setRoles(String[] roles) {        this.roles = roles;    }}客户端 VO 类:import java.util.Map;public class ClientVO {    private String clientId;    private String clientSecret;    private String resourceId[];    private String scope[];    private String authorizedGrantTypes[];    private String registeredRedirectUri[];    private Integer accessTokenValiditySeconds;    private Integer refreshTokenValiditySeconds;    private String authorities[];    private Map<String, Object> additionalInformation;    public String getClientId() {        return clientId;    }    public void setClientId(String clientId) {        this.clientId = clientId;    }    public String getClientSecret() {        return clientSecret;    }    public void setClientSecret(String clientSecret) {        this.clientSecret = clientSecret;    }    public String[] getResourceId() {        return resourceId;    }    public void setResourceId(String[] resourceId) {        this.resourceId = resourceId;    }    public String[] getScope() {        return scope;    }    public void setScope(String[] scope) {        this.scope = scope;    }    public String[] getAuthorizedGrantTypes() {        return authorizedGrantTypes;    }    public void setAuthorizedGrantTypes(String[] authorizedGrantTypes) {        this.authorizedGrantTypes = authorizedGrantTypes;    }    public String[] getRegisteredRedirectUri() {        return registeredRedirectUri;    }    public void setRegisteredRedirectUri(String[] registeredRedirectUri) {        this.registeredRedirectUri = registeredRedirectUri;    }    public Integer getAccessTokenValiditySeconds() {        return accessTokenValiditySeconds;    }    public void setAccessTokenValiditySeconds(Integer accessTokenValiditySeconds) {        this.accessTokenValiditySeconds = accessTokenValiditySeconds;    }    public Integer getRefreshTokenValiditySeconds() {        return refreshTokenValiditySeconds;    }    public void setRefreshTokenValiditySeconds(Integer refreshTokenValiditySeconds) {        this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;    }    public String[] getAuthorities() {        return authorities;    }    public void setAuthorities(String[] authorities) {        this.authorities = authorities;    }    public Map<String, Object> getAdditionalInformation() {        return additionalInformation;    }    public void setAdditionalInformation(Map<String, Object> additionalInformation) {        this.additionalInformation = additionalInformation;    }}接口统一响应类:public class APIResponse {    public static final Integer OK = 0;    public static final Integer ERROR = -1;    private Integer responseCode;    private String reasonCode=\"\";    private Object data;    public Integer getResponseCode() {        return responseCode;    }    public void setResponseCode(Integer responseCode) {        this.responseCode = responseCode;    }    public String getReasonCode() {        return reasonCode;    }    public void setReasonCode(String reasonCode) {        this.reasonCode = reasonCode;    }    public Object getData() {        return data;    }    public void setData(Object data) {        this.data = data;    }}序列化包装类:import java.io.Serializable;/** *  序列化包装类 */public class SerializationWrapper implements Serializable {    private static final long serialVersionUID = -1320613094306260457L;    private Object wrapObject;    public SerializationWrapper(Object wrapObject) {        this.wrapObject = wrapObject;    }    public Object getWrapObject() {        return wrapObject;    }    public void setWrapObject(Object wrapObject) {        this.wrapObject = wrapObject;    }}属性转换器类:import org.apache.commons.lang.SerializationUtils;import javax.persistence.AttributeConverter;/** * 属性转换器 * 作用:插入数据库时,把对象转为字符串,查询数据时,字符串转为实体对象 */public class ObjectToBytesAttributeConverter implements AttributeConverter<Object, byte[]> {    @Override    public byte[] convertToDatabaseColumn(Object o) {        SerializationWrapper serializationWrapper = new SerializationWrapper(o);        return SerializationUtils.serialize(serializationWrapper);    }    @Override    public Object convertToEntityAttribute(byte[] bytes) {        SerializationWrapper serializationWrapper = (SerializationWrapper) SerializationUtils.deserialize(bytes);        return serializationWrapper.getWrapObject();    }}

数据库配置类和 Oauth 2.0 配置类(核心代码)

下面是数据库相关配置代码,比如:数据源配置、实体扫类描路径等。

import com.zaxxer.hikari.HikariDataSource;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.data.jpa.repository.config.EnableJpaRepositories;import org.springframework.orm.jpa.JpaTransactionManager;import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;import org.springframework.transaction.PlatformTransactionManager;import javax.sql.DataSource;import java.util.HashMap;import java.util.Map;/** * 数据库相关配置 */@Configuration@EnableJpaRepositories(basePackages = \"com.example.oauth2.repository\", entityManagerFactoryRef = \"oauthEntityManagerFactory\", transactionManagerRef = \"oauthTransactionFactory\")public class DataBaseConfig {    @Bean    @Primary    @ConfigurationProperties(\"mysql.oauth.datasource\")    public DataSourceProperties oauthDataSourceProperties() {        return new DataSourceProperties();    }    @Bean    @Primary    @ConfigurationProperties(\"mysql.oauth.datasource.configuration\")    public DataSource oauthDataSource() {        return oauthDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();    }    @Primary    @Bean(name = \"oauthEntityManagerFactory\")    public LocalContainerEntityManagerFactoryBean oauthEntityManagerFactory(EntityManagerFactoryBuilder builder) {        Map<String, Object> properties = new HashMap<String, Object>();        properties.put(\"hibernate.hbm2ddl.auto\", \"update\");        properties.put(\"hibernate.dialect\", \"org.hibernate.dialect.MySQL5InnoDBDialect\");        properties.put(\"hibernate.show_sql\", \"false\");        return builder.dataSource(oauthDataSource())                .properties(properties)                .packages(\"com.example.oauth2.entry\")                .persistenceUnit(\"oauthPersistenceUnit\").build();    }    @Primary    @Bean(name = \"oauthTransactionFactory\")    public PlatformTransactionManager oauthTransactionManager(            final @Qualifier(\"oauthEntityManagerFactory\") LocalContainerEntityManagerFactoryBean oauthEntityManagerFactory) {        return new JpaTransactionManager(oauthEntityManagerFactory.getObject());    }}

下面是 Oauth 2.0 的核心配置,比如:Spring Security 配置、Oauth2 资源服务器配置、Oauth2 授权服务器配置、Oauth2 认证配置等。

Spring Security 配置代码:import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;/** * Spring Security 配置 */@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {    @Bean    @Override    public AuthenticationManager authenticationManagerBean() throws Exception {        return super.authenticationManagerBean();    }}oauth2 资源服务器配置代码:import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;/** * oauth2 资源服务器配置 */@Configuration@EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter {    @Override    public void configure(HttpSecurity http) throws Exception {        http            .authorizeRequests()            .antMatchers(\"/oauth/**\").permitAll()  //配置允许访问的 url            .anyRequest().authenticated()  //其它所有请求都需要鉴权认证,即需校验 token        ;        //所有请求不做鉴权认证,注意请求头不能设置 Authorization,如果设置了也会进行鉴权认证        //http.authorizeRequests().anyRequest().permitAll().and().logout().permitAll();    }}oauth2 授权服务器配置:import com.example.oauth2.service.*;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Primary;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.authentication.AuthenticationProvider;import org.springframework.security.authentication.ProviderManager;import org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;import org.springframework.security.oauth2.provider.ClientDetailsService;import org.springframework.security.oauth2.provider.CompositeTokenGranter;import org.springframework.security.oauth2.provider.OAuth2RequestFactory;import org.springframework.security.oauth2.provider.TokenGranter;import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;import org.springframework.security.oauth2.provider.token.DefaultTokenServices;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;import java.util.ArrayList;import java.util.Arrays;import java.util.List;/** * oauth2 授权服务器配置 */@Configuration@EnableAuthorizationServerpublic class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {    @Qualifier(\"authenticationManagerBean\")    @Autowired    private AuthenticationManager authenticationManager;    @Autowired    private CustomUserDetailsService customUserDetailsService;    @Autowired    private CustomClientDetailsService customClientDetailsService;    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();    @Override    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {        oauthServer                .allowFormAuthenticationForClients()    //允许表单认证,针对/oauth/token 端点。                .tokenKeyAccess(\"permitAll()\")          //访问权限为 permitAll(),允许访问                .checkTokenAccess(\"isAuthenticated()\")  //访问权限为 isAuthenticated(),需要验证权限                .passwordEncoder(passwordEncoder)        ;    }    @Override    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {        clients.withClientDetails(customClientDetailsService);    }    @Override    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {        endpoints                .userDetailsService(customUserDetailsService)                .authenticationManager(authenticationManager)                .tokenStore(tokenStore())                .tokenServices(defaultTokenServices())                .tokenGranter(getDefaultTokenGranters(endpoints));    }    private TokenGranter getDefaultTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {        ClientDetailsService clientDetails = endpoints.getClientDetailsService();        AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();        AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();        OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();        List<TokenGranter> tokenGranters = new ArrayList();        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, requestFactory));        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));        ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);        tokenGranters.add(implicit);        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));        if (this.authenticationManager != null) {            CustomResourceOwnerPasswordTokenGranter customResourceOwnerPasswordTokenGranter = new CustomResourceOwnerPasswordTokenGranter(this.authenticationManager, tokenServices, clientDetails, requestFactory);            tokenGranters.add(customResourceOwnerPasswordTokenGranter);        }        return new CompositeTokenGranter(tokenGranters);    }    @Bean(name = \"tokenStore\")    public TokenStore tokenStore() {        return new CustomTokenStore();    }    @Primary    @Bean(name = \"tokenService\")    public DefaultTokenServices defaultTokenServices() {        DefaultTokenServices tokenServices = new CustomTokenServices();        tokenServices.setTokenStore(tokenStore());        tokenServices.setClientDetailsService(customClientDetailsService);        tokenServices.setSupportRefreshToken(true);        tokenServices.setReuseRefreshToken(false);        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();        provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(                customUserDetailsService));        tokenServices                .setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider>asList(provider)));        return tokenServices;    }}oauth2 认证配置:import com.example.oauth2.service.CustomUserDetailsService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.authentication.configuration.GlobalAuthenticationConfigurerAdapter;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;/** * oauth2 认证配置 */@Configurationpublic class AuthenticationConifg extends GlobalAuthenticationConfigurerAdapter {    @Autowired    private CustomUserDetailsService customUserDetailsService;    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();    @Override    public void init(AuthenticationManagerBuilder auth) throws Exception {        auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder);    }}

下面是 Oauth 2.0 自定义类,比如:实现 Oauth 2.0 相关接口来实现查询自己数据库数据进行用户验证、客户端验证等操作。

自定义 ClientDetails 类:import com.example.oauth2.entry.Client;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.oauth2.provider.ClientDetails;import java.util.*;import java.util.stream.Collectors;/** * 自定义 ClientDetails 类 */public class CustomClientDetails implements ClientDetails {    private Client client;    public CustomClientDetails(Client client) {        this.client = client;    }    @Override    public String getClientId() {        return client.getClientId();    }    @Override    public Set<String> getResourceIds() {        String resourceId[] = client.getResourceId();        return resourceId == null ? Collections.emptySet() : Arrays.stream(client.getResourceId()).collect(Collectors.toSet());    }    @Override    public boolean isSecretRequired() {        return true;    }    @Override    public String getClientSecret() {        return client.getClientSecret();    }    @Override    public boolean isScoped() {        return client.getScope() != null && !(client.getScope().length > 0);    }    @Override    public Set<String> getScope() {        String scopes[] = client.getScope();        return scopes == null ? Collections.emptySet() : Arrays.stream(client.getScope()).collect(Collectors.toSet());    }    @Override    public Set<String> getAuthorizedGrantTypes() {        String authorizedGrantTypes[] = client.getAuthorizedGrantTypes();        return authorizedGrantTypes == null ? Collections.emptySet() : Arrays.stream(client.getAuthorizedGrantTypes()).collect(Collectors.toSet());    }    @Override    public Set<String> getRegisteredRedirectUri() {        String registeredRedirectUri[] = client.getRegisteredRedirectUri();        return registeredRedirectUri == null ? Collections.emptySet() : Arrays.stream(client.getRegisteredRedirectUri()).collect(Collectors.toSet());    }    @Override    public Collection<GrantedAuthority> getAuthorities() {        String authorities[] = client.getAuthorities();        if (authorities == null) {            return Collections.emptyList();        }        return Arrays.stream(authorities).map(                a -> (GrantedAuthority) () -> a        ).collect(Collectors.toList());    }    @Override    public Integer getAccessTokenValiditySeconds() {        return client.getAccessTokenValiditySeconds();    }    @Override    public Integer getRefreshTokenValiditySeconds() {        return client.getRefreshTokenValiditySeconds();    }    @Override    public boolean isAutoApprove(String scope) {        return false;    }    @Override    public Map<String, Object> getAdditionalInformation() {        return null;    }}自定义 ClientDetailsService 类:import com.example.oauth2.entry.Client;import com.example.oauth2.repository.ClientRepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.oauth2.provider.ClientDetails;import org.springframework.security.oauth2.provider.ClientDetailsService;import org.springframework.security.oauth2.provider.ClientRegistrationException;import org.springframework.stereotype.Component;import java.util.Optional;/** * 自定义 ClientDetailsService 类 */@Componentpublic class CustomClientDetailsService implements ClientDetailsService {    @Autowired    private ClientRepository clientRepository;    @Override    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {        Optional<Client> clientOptional = Optional.ofNullable(clientRepository.findByClientId(clientId));        if (clientOptional.isPresent()) {            return new CustomClientDetails(clientOptional.get());        }        throw new ClientRegistrationException(clientId);    }}自定义 token 授权器:import org.springframework.security.authentication.*;import org.springframework.security.core.Authentication;import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;import org.springframework.security.oauth2.provider.*;import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;import java.util.LinkedHashMap;import java.util.Map;/** * 自定义 token 授权器,此处使用“密码授权模式” * 此类也可以扩展用户获取 token 的失败次数超过某个阈值后锁定用户 */public class CustomResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {    private static final String GRANT_TYPE = \"password\";    private final AuthenticationManager authenticationManager;    public CustomResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {        this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);    }    protected CustomResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {        super(tokenServices, clientDetailsService, requestFactory, grantType);        this.authenticationManager = authenticationManager;    }    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());        String username = (String) parameters.get(\"username\");        String password = (String) parameters.get(\"password\");        parameters.remove(\"password\");        Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);        try {            userAuth = this.authenticationManager.authenticate(userAuth);        } catch (AccountStatusException ase) {            throw new InvalidGrantException(ase.getMessage());        } catch (BadCredentialsException bce) {            throw new InvalidGrantException(bce.getMessage());        }        if (userAuth != null && userAuth.isAuthenticated()) {            OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);            return new OAuth2Authentication(storedOAuth2Request, userAuth);        } else {            throw new InvalidGrantException(\"Could not authenticate user: \" + username);        }    }}自定义 DefaultTokenServices 类:import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.core.Authentication;import org.springframework.security.core.AuthenticationException;import org.springframework.security.oauth2.common.*;import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;import org.springframework.security.oauth2.common.exceptions.InvalidScopeException;import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;import org.springframework.security.oauth2.provider.*;import org.springframework.security.oauth2.provider.token.DefaultTokenServices;import org.springframework.security.oauth2.provider.token.TokenEnhancer;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;import org.springframework.transaction.annotation.Transactional;import org.springframework.util.Assert;import java.util.Date;import java.util.Set;import java.util.UUID;/** * 自定义 DefaultTokenServices 类 */public class CustomTokenServices extends DefaultTokenServices {    private int refreshTokenValiditySeconds = 60 * 60 * 24 * 30; // default 30 days.    private int accessTokenValiditySeconds = 60 * 60 * 12; // default 12 hours.    private boolean supportRefreshToken = false;    private boolean reuseRefreshToken = true;    private TokenStore tokenStore;    private ClientDetailsService clientDetailsService;    private TokenEnhancer accessTokenEnhancer;    private AuthenticationManager authenticationManager;    public void afterPropertiesSet() throws Exception {        Assert.notNull(tokenStore, \"tokenStore must be set\");    }    @Transactional    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);        OAuth2RefreshToken refreshToken = null;        if (existingAccessToken != null) {            if (existingAccessToken.getRefreshToken() != null) {                refreshToken = existingAccessToken.getRefreshToken();                tokenStore.removeRefreshToken(refreshToken);            }            tokenStore.removeAccessToken(existingAccessToken);        }        if (refreshToken == null) {            refreshToken = createRefreshToken(authentication);        } else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {            refreshToken = createRefreshToken(authentication);        }        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);        tokenStore.storeAccessToken(accessToken, authentication);        refreshToken = accessToken.getRefreshToken();        if (refreshToken != null) {            tokenStore.storeRefreshToken(refreshToken, authentication);        }        return accessToken;    }    @Transactional(noRollbackFor = {InvalidTokenException.class, InvalidGrantException.class})    public OAuth2AccessToken refreshAccessToken(String refreshTokenValue, TokenRequest tokenRequest)            throws AuthenticationException {        if (!supportRefreshToken) {            throw new InvalidGrantException(\"Invalid refresh token: \" + refreshTokenValue);        }        OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(refreshTokenValue);        if (refreshToken == null) {            throw new InvalidGrantException(\"Invalid refresh token: \" + refreshTokenValue);        }        OAuth2Authentication authentication = tokenStore.readAuthenticationForRefreshToken(refreshToken);        if (this.authenticationManager != null && !authentication.isClientOnly()) {            Authentication user = new PreAuthenticatedAuthenticationToken(authentication.getUserAuthentication(), \"\", authentication.getAuthorities());            user = authenticationManager.authenticate(user);            Object details = authentication.getDetails();            authentication = new OAuth2Authentication(authentication.getOAuth2Request(), user);            authentication.setDetails(details);        }        String clientId = authentication.getOAuth2Request().getClientId();        if (clientId == null || !clientId.equals(tokenRequest.getClientId())) {            throw new InvalidGrantException(\"Wrong client for this refresh token: \" + refreshTokenValue);        }        tokenStore.removeAccessTokenUsingRefreshToken(refreshToken);        if (isExpired(refreshToken)) {            tokenStore.removeRefreshToken(refreshToken);            throw new InvalidTokenException(\"Invalid refresh token (expired): \" + refreshToken);        }        authentication = createRefreshedAuthentication(authentication, tokenRequest);        if (!reuseRefreshToken) {            tokenStore.removeRefreshToken(refreshToken);            refreshToken = createRefreshToken(authentication);        }        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);        tokenStore.storeAccessToken(accessToken, authentication);        if (!reuseRefreshToken) {            tokenStore.storeRefreshToken(accessToken.getRefreshToken(), authentication);        }        return accessToken;    }    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {        return tokenStore.getAccessToken(authentication);    }    private OAuth2Authentication createRefreshedAuthentication(OAuth2Authentication authentication, TokenRequest request) {        OAuth2Authentication narrowed = authentication;        Set<String> scope = request.getScope();        OAuth2Request clientAuth = authentication.getOAuth2Request().refresh(request);        if (scope != null && !scope.isEmpty()) {            Set<String> originalScope = clientAuth.getScope();            if (originalScope == null || !originalScope.containsAll(scope)) {                throw new InvalidScopeException(\"Unable to narrow the scope of the client authentication to \" + scope                        + \".\", originalScope);            } else {                clientAuth = clientAuth.narrowScope(scope);            }        }        narrowed = new OAuth2Authentication(clientAuth, authentication.getUserAuthentication());        return narrowed;    }    protected boolean isExpired(OAuth2RefreshToken refreshToken) {        if (refreshToken instanceof ExpiringOAuth2RefreshToken) {            ExpiringOAuth2RefreshToken expiringToken = (ExpiringOAuth2RefreshToken) refreshToken;            return expiringToken.getExpiration() == null                    || System.currentTimeMillis() > expiringToken.getExpiration().getTime();        }        return false;    }    public OAuth2AccessToken readAccessToken(String accessToken) {        return tokenStore.readAccessToken(accessToken);    }    public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,            InvalidTokenException {        OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);        if (accessToken == null) {            throw new InvalidTokenException(\"Invalid access token: \" + accessTokenValue);        } else if (accessToken.isExpired()) {            tokenStore.removeAccessToken(accessToken);            throw new InvalidTokenException(\"Access token expired: \" + accessTokenValue);        }        OAuth2Authentication result = tokenStore.readAuthentication(accessToken);        if (result == null) {            // in case of race condition            throw new InvalidTokenException(\"Invalid access token: \" + accessTokenValue);        }        if (clientDetailsService != null) {            String clientId = result.getOAuth2Request().getClientId();            try {                clientDetailsService.loadClientByClientId(clientId);            } catch (ClientRegistrationException e) {                throw new InvalidTokenException(\"Client not valid: \" + clientId, e);            }        }        return result;    }    public String getClientId(String tokenValue) {        OAuth2Authentication authentication = tokenStore.readAuthentication(tokenValue);        if (authentication == null) {            throw new InvalidTokenException(\"Invalid access token: \" + tokenValue);        }        OAuth2Request clientAuth = authentication.getOAuth2Request();        if (clientAuth == null) {            throw new InvalidTokenException(\"Invalid access token (no client id): \" + tokenValue);        }        return clientAuth.getClientId();    }    public boolean revokeToken(String tokenValue) {        OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);        if (accessToken == null) {            return false;        }        if (accessToken.getRefreshToken() != null) {            tokenStore.removeRefreshToken(accessToken.getRefreshToken());        }        tokenStore.removeAccessToken(accessToken);        return true;    }    private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {        if (!isSupportRefreshToken(authentication.getOAuth2Request())) {            return null;        }        int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());        String value = UUID.randomUUID().toString();        if (validitySeconds > 0) {            return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()                    + (validitySeconds * 1000L)));        }        return new DefaultOAuth2RefreshToken(value);    }    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());        if (validitySeconds > 0) {            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));        }        token.setRefreshToken(refreshToken);        token.setScope(authentication.getOAuth2Request().getScope());        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;    }    protected int getAccessTokenValiditySeconds(OAuth2Request clientAuth) {        if (clientDetailsService != null) {            ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());            Integer validity = client.getAccessTokenValiditySeconds();            if (validity != null) {                return validity;            }        }        return accessTokenValiditySeconds;    }    protected int getRefreshTokenValiditySeconds(OAuth2Request clientAuth) {        if (clientDetailsService != null) {            ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());            Integer validity = client.getRefreshTokenValiditySeconds();            if (validity != null) {                return validity;            }        }        return refreshTokenValiditySeconds;    }    protected boolean isSupportRefreshToken(OAuth2Request clientAuth) {        if (clientDetailsService != null) {            ClientDetails client = clientDetailsService.loadClientByClientId(clientAuth.getClientId());            return client.getAuthorizedGrantTypes().contains(\"refresh_token\");        }        return this.supportRefreshToken;    }    public void setTokenEnhancer(TokenEnhancer accessTokenEnhancer) {        this.accessTokenEnhancer = accessTokenEnhancer;    }    public void setRefreshTokenValiditySeconds(int refreshTokenValiditySeconds) {        this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;    }    public void setAccessTokenValiditySeconds(int accessTokenValiditySeconds) {        this.accessTokenValiditySeconds = accessTokenValiditySeconds;    }    public void setSupportRefreshToken(boolean supportRefreshToken) {        this.supportRefreshToken = supportRefreshToken;    }    public void setReuseRefreshToken(boolean reuseRefreshToken) {        this.reuseRefreshToken = reuseRefreshToken;    }    public void setTokenStore(TokenStore tokenStore) {        this.tokenStore = tokenStore;    }    public void setAuthenticationManager(AuthenticationManager authenticationManager) {        this.authenticationManager = authenticationManager;    }    public void setClientDetailsService(ClientDetailsService clientDetailsService) {        this.clientDetailsService = clientDetailsService;    }}自定义 TokenStore 类:import com.example.oauth2.entry.AccessToken;import com.example.oauth2.entry.RefreshToken;import com.example.oauth2.repository.AccessTokenRepository;import com.example.oauth2.repository.RefreshTokenRepository;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.oauth2.common.OAuth2AccessToken;import org.springframework.security.oauth2.common.OAuth2RefreshToken;import org.springframework.security.oauth2.common.util.SerializationUtils;import org.springframework.security.oauth2.provider.OAuth2Authentication;import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;import org.springframework.security.oauth2.provider.token.TokenStore;import org.springframework.stereotype.Component;import java.io.UnsupportedEncodingException;import java.math.BigInteger;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.*;import java.util.stream.Collectors;/** * 自定义 TokenStore 类 */@Componentpublic class CustomTokenStore implements TokenStore {    private Logger logger = LogManager.getLogger(getClass());    @Autowired    private AccessTokenRepository accessTokenRepository;    @Autowired    private RefreshTokenRepository refreshTokenRepository;    private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();    @Override    public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {        return readAuthentication(token.getValue());    }    @Override    public OAuth2Authentication readAuthentication(String token) {        OAuth2Authentication authentication = null;        Optional<AccessToken> opt = Optional.ofNullable(accessTokenRepository.findOAuthAccessTokenByTokenId(token));        if (opt.isPresent()) {            authentication = deserializeAuthentication(opt.get().getAuthentication().getBytes());        }        return authentication;    }    @Override    public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {        String refreshToken = null;        if (token.getRefreshToken() != null) {            refreshToken = token.getRefreshToken().getValue();        }        if (readAccessToken(token.getValue()) != null) {            removeAccessToken(token.getValue());        }        AccessToken accessToken = new AccessToken();        accessToken.setTokenId(token.getValue());        accessToken.setToken(new String(serializeAccessToken(token)));        accessToken.setAuthenticationId(authenticationKeyGenerator.extractKey(authentication));        accessToken.setUsername(authentication.isClientOnly() ? null : authentication.getName());        accessToken.setClientId(authentication.getOAuth2Request().getClientId());        accessToken.setAuthentication(new String(serializeAuthentication(authentication)));        accessToken.setRefreshToken(extractTokenKey(refreshToken));        accessTokenRepository.save(accessToken);    }    @Override    public OAuth2AccessToken readAccessToken(String tokenValue) {        OAuth2AccessToken accessToken = null;        Optional<AccessToken> opt = Optional.ofNullable(accessTokenRepository.findOAuthAccessTokenByTokenId(tokenValue));        if (opt.isPresent()) {            accessToken = deserializeAccessToken(opt.get().getToken().getBytes());        }        return accessToken;    }    @Override    public void removeAccessToken(OAuth2AccessToken token) {        removeAccessToken(token.getValue());    }    public void removeAccessToken(String tokenValue) {        accessTokenRepository.deleteAllByTokenId(tokenValue);    }    @Override    public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {        RefreshToken oAuthRefreshToken = new RefreshToken();        oAuthRefreshToken.setTokenId(refreshToken.getValue());        oAuthRefreshToken.setToken(new String(serializeRefreshToken(refreshToken)));        oAuthRefreshToken.setAuthentication(new String(serializeAuthentication(authentication)));        refreshTokenRepository.save(oAuthRefreshToken);    }    @Override    public OAuth2RefreshToken readRefreshToken(String tokenValue) {        OAuth2RefreshToken refreshToken = null;        Optional<RefreshToken> opt = Optional.ofNullable(refreshTokenRepository.findOAuthRefreshTokenByTokenId(tokenValue));        if (opt.isPresent()) {            refreshToken = deserializeRefreshToken(opt.get().getToken().getBytes());        }        return refreshToken;    }    @Override    public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {        return readAuthenticationForRefreshToken(token.getValue());    }    public OAuth2Authentication readAuthenticationForRefreshToken(String tokenValue) {        OAuth2Authentication authentication = null;        Optional<RefreshToken> opt = Optional.ofNullable(refreshTokenRepository.findOAuthRefreshTokenByTokenId(tokenValue));        if (opt.isPresent()) {            authentication = deserializeAuthentication(opt.get().getAuthentication().getBytes());        }        return authentication;    }    @Override    public void removeRefreshToken(OAuth2RefreshToken token) {        removeRefreshToken(token.getValue());    }    public void removeRefreshToken(String tokenValue) {        refreshTokenRepository.deleteOAuthRefreshTokenByTokenId(tokenValue);    }    @Override    public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {        removeAccessTokenUsingRefreshToken(refreshToken.getValue());    }    public void removeAccessTokenUsingRefreshToken(String tokenValue) {        accessTokenRepository.deleteAllByRefreshToken(extractTokenKey(tokenValue));    }    @Override    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {        OAuth2AccessToken accessToken = null;        String authenticationId = authenticationKeyGenerator.extractKey(authentication);        Optional<AccessToken> opt = Optional.ofNullable(accessTokenRepository.findOAuthAccessTokenByAuthenticationId(authenticationId));        if (opt.isPresent()) {            accessToken = deserializeAccessToken(opt.get().getToken().getBytes());            if (accessToken != null                    && !authenticationId.equals(authenticationKeyGenerator.extractKey(readAuthentication(accessToken.getValue())))) {                removeAccessToken(accessToken.getValue());                // Keep the store consistent (maybe the same user is represented by this authentication but the details have                // changed)                storeAccessToken(accessToken, authentication);            }        }        return accessToken;    }    @Override    public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {        List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>();        Optional<List<AccessToken>> opt = Optional.ofNullable(accessTokenRepository.findAllByClientIdAndUsername(clientId, userName));        if (opt.isPresent()) {            accessTokens = opt.get().stream().map(AccessToken::getToken).filter(t -> t != null).map(t -> deserializeAccessToken(t.getBytes())).collect(Collectors.toList());        }        return accessTokens;    }    @Override    public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {        List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>();        Optional<List<AccessToken>> opt = Optional.ofNullable(accessTokenRepository.findAllByClientId(clientId));        if (opt.isPresent()) {            accessTokens = opt.get().stream().map(AccessToken::getToken).filter(t -> t != null).map(t -> deserializeAccessToken(t.getBytes())).collect(Collectors.toList());        }        return accessTokens;    }    protected byte[] serializeAccessToken(OAuth2AccessToken token) {        return Base64.getEncoder().encode(SerializationUtils.serialize(token));    }    protected byte[] serializeRefreshToken(OAuth2RefreshToken token) {        return Base64.getEncoder().encode(SerializationUtils.serialize(token));    }    protected byte[] serializeAuthentication(OAuth2Authentication authentication) {        return Base64.getEncoder().encode(SerializationUtils.serialize(authentication));    }    protected OAuth2AccessToken deserializeAccessToken(byte[] token) {        return SerializationUtils.deserialize(Base64.getDecoder().decode(token));    }    protected OAuth2RefreshToken deserializeRefreshToken(byte[] token) {        return SerializationUtils.deserialize(Base64.getDecoder().decode(token));    }    protected OAuth2Authentication deserializeAuthentication(byte[] authentication) {        return SerializationUtils.deserialize(Base64.getDecoder().decode(authentication));    }    protected String extractTokenKey(String value) {        if (value == null) {            return null;        }        MessageDigest digest;        try {            digest = MessageDigest.getInstance(\"MD5\");        } catch (NoSuchAlgorithmException e) {            throw new IllegalStateException(\"MD5 algorithm not available.  Fatal (should be in the JDK).\");        }        try {            byte[] bytes = digest.digest(value.getBytes(\"UTF-8\"));            return String.format(\"%032x\", new BigInteger(1, bytes));        } catch (UnsupportedEncodingException e) {            throw new IllegalStateException(\"UTF-8 encoding not available.  Fatal (should be in the JDK).\");        }    }}自定义 UserDetails 类:import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Arrays;import java.util.Collection;import java.util.Collections;import java.util.stream.Collectors;/** * 自定义 UserDetails 类 */public class CustomUserDetails implements UserDetails {    private static final long serialVersionUID = -2560899610049516015L;    private String userName;    private String password;    private String roles[];    public CustomUserDetails(String userName, String password, String[] roles) {        this.userName = userName;        this.password = password;        this.roles = roles;    }    @Override    public Collection<? extends GrantedAuthority> getAuthorities() {        String roles[] = this.roles;        if (roles == null) {            return Collections.emptyList();        }        return Arrays.stream(roles).map(                r -> (GrantedAuthority) () -> r        ).collect(Collectors.toList());    }    @Override    public String getPassword() {        return password;    }    @Override    public String getUsername() {        return userName;    }    public void setUserName(String userName) {        this.userName = userName;    }    public void setPassword(String password) {        this.password = password;    }    public String[] getRoles() {        return roles;    }    public void setRoles(String[] roles) {        this.roles = roles;    }    @Override    public boolean isAccountNonExpired() {        return true;    }    @Override    public boolean isAccountNonLocked() {        return true;    }    @Override    public boolean isCredentialsNonExpired() {        return true;    }    @Override    public boolean isEnabled() {        return true;    }}自定义 UserDetailsService 类:import com.example.oauth2.entry.User;import com.example.oauth2.repository.UserRepository;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Component;import java.util.Optional;/** * 自定义 UserDetailsService 类 */@Componentpublic class CustomUserDetailsService implements UserDetailsService {    @Autowired    private UserRepository userRepository;    @Override    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {        Optional<User> opt = Optional.ofNullable(userRepository.findByUserName(userName));        if (opt.isPresent()) {            User user = opt.get();            return new CustomUserDetails(user.getUserName(), user.getPassword(), user.getRoles());        }        throw new UsernameNotFoundException(userName);    }}

Controller 类和 Service 类

下面是用户、客户端的 Controller 和 Service 类,功能包括所有用户列表、创建一个用户、登出、创建一个客户端。

用户 controller:import com.example.oauth2.util.APIResponse;import com.example.oauth2.service.UserService;import com.example.oauth2.vo.UserVO;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.oauth2.provider.token.DefaultTokenServices;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/** * 用户 controller */@RestControllerpublic class UserController {    @Autowired    private UserService userService;    @Autowired    @Qualifier(value = \"tokenService\")    private DefaultTokenServices tokenService;    /**     * 所有用户列表     * @return     */    @GetMapping(value = \"/user/list\")    public APIResponse getUserList() {        return userService.getUserList();    }    /**     * 创建一个用户     * @param response     * @param userVO     * @return     */    @PostMapping(value = \"/user/create\")    public APIResponse addUser(HttpServletResponse response, @RequestBody UserVO userVO) {        APIResponse apiResponse = new APIResponse();        apiResponse.setResponseCode(APIResponse.OK);        response.setStatus(HttpServletResponse.SC_CREATED);        return userService.addUser(userVO);    }    /**     * 登出     * @param request     */    @PostMapping(value = \"/oauth/logout\")    public void logoutUser(HttpServletRequest request) {        String authorization = request.getHeader(\"Authorization\");        if (authorization != null && authorization.contains(\"Bearer\")) {            String tokenId = authorization.substring(\"Bearer\".length() + 1);            tokenService.revokeToken(tokenId);        }    }    public static void main(String[] args) {        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();        System.out.println(passwordEncoder.encode(\"demoClientSecret\"));    }}用户 Service 类:import com.example.oauth2.entry.User;import com.example.oauth2.repository.UserRepository;import com.example.oauth2.util.APIResponse;import com.example.oauth2.vo.UserVO;import org.apache.logging.log4j.Level;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.stereotype.Component;import java.time.Instant;import java.util.Arrays;import java.util.List;import java.util.Optional;import java.util.stream.Collectors;@Componentpublic class UserService {    private Logger logger = LogManager.getLogger(getClass());    @Autowired    private UserRepository userRepository;    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();    public APIResponse getUserList() {        APIResponse apiResponse = new APIResponse();        apiResponse.setResponseCode(APIResponse.OK);        List<User> userList = userRepository.findAll();        List<User> result = userList.stream().map(u -> {            u.setPassword(\"\");            return u;        }).collect(Collectors.toList());        apiResponse.setData(result);        return apiResponse;    }    public APIResponse addUser(UserVO userVO) {        APIResponse apiResponse = new APIResponse();        apiResponse.setResponseCode(APIResponse.OK);        //此处忽略重复用户校验        try {            User user = new User();            user.setUserName(userVO.getUserName());            user.setPassword(passwordEncoder.encode(userVO.getPassword()));            user.setRoles(userVO.getRoles());            user.setLastPswdChangeTimestamp(Instant.now().toString());            userRepository.save(user);        } catch (Exception e) {            Throwable t = Optional.ofNullable(e.getCause()).orElse(e);            String errorMessage = Optional.ofNullable(t.getMessage()).orElse(\"Internal exception: \" + e);            String traceMessage = Arrays.asList(e.getStackTrace()).stream().map(trace -> trace.toString()).collect(Collectors.joining(\"\\nat \"));            logger.log(Level.ERROR, errorMessage + \"\\nat \" + traceMessage);            apiResponse.setResponseCode(APIResponse.ERROR);        }        return apiResponse;    }}客户端 controller:import com.example.oauth2.util.APIResponse;import com.example.oauth2.service.ClientService;import com.example.oauth2.vo.ClientVO;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;/** * 客户端 controller */@RestControllerpublic class ClientController {    @Autowired    private ClientService clientService;    /**     * 创建一个客户端     * @param clientVO     * @return     */    @PostMapping(value = \"/client/create\")    public APIResponse addClient(@RequestBody ClientVO clientVO) {        APIResponse apiResponse = new APIResponse();        apiResponse.setResponseCode(APIResponse.OK);        return clientService.addClient(clientVO);    }}客户端 Service 类:import com.example.oauth2.entry.Client;import com.example.oauth2.repository.ClientRepository;import com.example.oauth2.util.APIResponse;import com.example.oauth2.vo.ClientVO;import org.apache.logging.log4j.Level;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.stereotype.Component;import java.util.Arrays;import java.util.Optional;import java.util.stream.Collectors;@Componentpublic class ClientService {    private Logger logger = LogManager.getLogger(getClass());    @Autowired    private ClientRepository clientRepository;    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();    public APIResponse addClient(ClientVO clientVO) {        APIResponse apiResponse = new APIResponse();        apiResponse.setResponseCode(APIResponse.OK);        //此处忽略重复用户校验        try {            Client client = new Client();            client.setClientId(clientVO.getClientId());            client.setClientSecret(passwordEncoder.encode(clientVO.getClientSecret()));            client.setResourceId(clientVO.getResourceId());            client.setScope(clientVO.getScope());            client.setAuthorizedGrantTypes(clientVO.getAuthorizedGrantTypes());            client.setRegisteredRedirectUri(clientVO.getRegisteredRedirectUri());            client.setAccessTokenValiditySeconds(clientVO.getAccessTokenValiditySeconds());            client.setRefreshTokenValiditySeconds(clientVO.getRefreshTokenValiditySeconds());            client.setAuthorities(clientVO.getAuthorities());            client.setAdditionalInformation(clientVO.getAdditionalInformation());            clientRepository.save(client);        } catch (Exception e) {            Throwable t = Optional.ofNullable(e.getCause()).orElse(e);            String errorMessage = Optional.ofNullable(t.getMessage()).orElse(\"Internal exception: \" + e);            String traceMessage = Arrays.asList(e.getStackTrace()).stream().map(trace -> trace.toString()).collect(Collectors.joining(\"\\nat \"));            logger.log(Level.ERROR, errorMessage + \"\\nat \" + traceMessage);            apiResponse.setResponseCode(APIResponse.ERROR);        }        return apiResponse;    }}

接口测试

创建用户接口

url:

http://127.0.0.1:8080/user/create

请求头:

Authorization Bearer 106dfc9e-0a2f-400b-be96-5b71ffa8d6b6

参数:

    {        \"password\": \"b2F1dGgxMjM=\",        \"roles\": [            \"ADMIN\"        ],        \"userName\": \"oauth\"    }

注意:password 是通过 base64 编码,解码后内容是:oauth123。

创建客户端接口

url:

http://127.0.0.1:8080/client/create

请求头:

Authorization Bearer 106dfc9e-0a2f-400b-be96-5b71ffa8d6b6

参数:

    {        \"clientId\": \"demoClientId\",        \"clientSecret\": \"demoClientSecret\",        \"scope\": [            \"read\",            \"write\",            \"trust\"        ],        \"authorizedGrantTypes\": [            \"password\",            \"refresh_token\"        ],        \"accessTokenValiditySeconds\": 1200,        \"refreshTokenValiditySeconds\": 3600    }
登录(即获取 token)接口

url:

http://127.0.0.1:8080/oauth/token?username=oauth&password=b2F1dGgxMjM=&grant_type=password

请求头:

Authorization Basic ZGVtb0NsaWVudElkOmRlbW9DbGllbnRTZWNyZXQ=

返回结果:

    {        \"access_token\": \"106dfc9e-0a2f-400b-be96-5b71ffa8d6b6\",        \"token_type\": \"bearer\",        \"refresh_token\": \"9d54fa6a-615c-4a90-9daf-8506e74edeb2\",        \"expires_in\": 1199,        \"scope\": \"trust read write\"    }

注意:ZGVtb0NsaWVudElkOmRlbW9DbGllbnRTZWNyZXQ= 是通过 base64 编码,解码后内容是:demoClientId:demoClientSecret

刷新 token 接口

url:

http://127.0.0.1:8080/oauth/token?grant_type=refresh_token&refresh_token=9d54fa6a-615c-4a90-9daf-8506e74edeb2

请求头:

Authorization Basic ZGVtb0NsaWVudElkOmRlbW9DbGllbnRTZWNyZXQ=

返回结果:

    {        \"access_token\": \"新的 access_token\",        \"token_type\": \"bearer\",        \"refresh_token\": \"新的 refresh_token\",        \"expires_in\": 1199,        \"scope\": \"trust read write\"    }
获取用户列表接口

url:

http://127.0.0.1:8080/user/list

请求头:

Authorization Bearer 106dfc9e-0a2f-400b-be96-5b71ffa8d6b6

返回结果:

    {        \"responseCode\": 0,        \"reasonCode\": \"\",        \"data\": [            {                \"userName\": \"oauth\",                \"password\": \"\",                \"roles\": [                    \"ADMIN\"                ],                \"lastPswdChangeTimestamp\": \"2021-06-28T06:17:28.822Z\"            },            {                \"userName\": \"admin\",                \"password\": \"\",                \"roles\": [                    \"ADMIN\"                ],                \"lastPswdChangeTimestamp\": \"2021-02-26T08:20:00.288Z\"            }        ]    }
退出(销毁 token)接口

url:

http://127.0.0.1:8080/oauth/logout

请求头:

Bearer 106dfc9e-0a2f-400b-be96-5b71ffa8d6b6

注意事项

1. 调用创建用户、客户端接口,先把 Oauth 2.0 鉴权的配置注释,不然会报无权调用接口,修改鉴权配置代码如下:

    /**     * oauth2 资源服务器配置     */    @Configuration    @EnableResourceServer    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {        @Override        public void configure(HttpSecurity http) throws Exception {    //        http    //            .authorizeRequests()    //            .antMatchers(\"/oauth/**\").permitAll()  //配置允许访问的 url    //            .anyRequest().authenticated()  //其它所有请求都需要鉴权认证,即需校验 token    //        ;            //所有请求不做鉴权认证,注意请求头不能设置 Authorization,如果设置了也会进行鉴权认证            http.authorizeRequests().anyRequest().permitAll().and().logout().permitAll();        }    }

2. 调用获取 token、刷新 token 接口,必须在请求头配置一个参数,key 是 Authorization,value 是 Basic ZGVtb0NsaWVudElkOmRlbW9DbGllbnRTZWNyZXQ=ZGVtb0NsaWVudElkOmRlbW9DbGllbnRTZWNyZXQ= 内容是 base64 编码(demoClientId:demoClientSecret)。

3. 调用销毁 token 接口,必须在请求头配置一个参数,key 是 Authorization,value 是 Bearer c2f0fc2e-5fe9-4f05-a7ef-5756021877d9c2f0fc2e-5fe9-4f05-a7ef-5756021877d9 是 access_token。

4. 创建用户的密码加密算法必须与 Oauth 2.0 鉴权时的算法一致,不然会报 Bad credentials。

总结

通过这次的 Spring Boot 整合 Oauth 2.0 的密码模式实战,我们可以知道 Oauth 2.0 密码模式的使用场景,可以掌握如何设计表存储用户、客户端、访问令牌、刷新令牌数据,理解获取 token、刷新 token、销毁 token 的流程和原理。

小七学习网,助您升职加薪,遇问题可联系:客服微信【1601371900】 备注:来自网站

免责声明: 1、本站信息来自网络,版权争议与本站无关 2、本站所有主题由该帖子作者发表,该帖子作者与本站享有帖子相关版权 3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和本站的同意 4、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责 5、用户所发布的一切软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。 6、您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 7、请支持正版软件、得到更好的正版服务。 8、如有侵权请立即告知本站(邮箱:1099252741@qq.com,备用微信:1099252741),本站将及时予与删除 9、本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章和视频仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。