1 缘起

先吐槽,
在搜索关于Oauth2.0授权码方式认证时,
遇到的问题比较多,一句话,按照其分享的步骤一步一步来,最终,无法成功,
本想,抄近路,看一些前人分享的应用案例,直接使用,
近路不通,曲线前进。
后分享,
结合前人的经验以及源码一点点调试,最终实现了授权码认证,
同时,学习到了两种参数配置方式:基于内存配置参数和基于数据库配置参数,
本文,旨在分享正确且完整免费的应用实战案例,
读者可直接拿来使用,把服务跑起来,无需耗费过多精力在调试上,
后续有时间,再耐心抠一抠源码,既要学习实践,又要学习理论,
前期应用时,可以先从打通脉络开始,后续再逐步究其原理。

版本:
SpringBoot:2.2.8.RELEASE
SpringSecurity:5.2.5
Oauth2.0:2.3.8.RELEASE

2 拦截配置

拦截配置用于拦截指定的认证接口,以及配置使用Oauth2保护哪些资源,
当调用这些接口时(资源),进入认证逻辑。

2.1 依赖

这里,集成Spring security和Oauth2用于安全认证,
而Redis是用于Token存储,POM依赖如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.8.RELEASE</version>
</dependency>

2.2 认证拦截配置

认证拦截即配置Oauth2的URI拦截,如 / o a u t h / ∗ ∗ /oauth/** /oauth/, / l o g i n / ∗ ∗ /login/** /login/, l o g o u t / ∗ ∗ logout/** logout/
当访问这类接口时,进入Oauth2的认证逻辑,
默认使用Oauth2的登录表单:formLogin,当需要登录时,直接跳转到Oauth2的登录界面,
配置样例如下:

package com.monkey.security.modules.oauth2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * URI拦截.
 *
 * @author xindaqi
 * @since 2022-10-25 17:10
 */
@EnableWebSecurity
@Configuration
//@Order(1)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf().disable().cors();
        httpSecurity.authorizeRequests()
                .antMatchers("/oauth/**", "/login/**", "logout/**")
                .permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().permitAll();
    }

}

2.3 资源拦截配置

资源拦截是配置哪些资源使用Oauth2鉴权,
当访问这些接口资源时,进入Oauth2的鉴权逻辑,
这里配置的端口为: / a p i / v 1 / ∗ ∗ /api/v1/** /api/v1/系列接口,
配置样例如下:

package com.monkey.security.modules.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;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

/**
 * 认证资源配置.
 *
 * @author xindaqi
 * @since 2022-10-25 17:02
 */
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("rid");
    }

    @Override
    public void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.requestMatchers()
                .antMatchers("/api/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**")
                .authenticated();
    }
}

3 存储配置

为什么会使用到存储
因为,Oauth2支持两种参数配置方式:内存配置和持久化配置,
天然支持MySQL,因此,直接使用MySQL的数据源。
Token的存储同样支持多种方式,这里原则Redis存储

3.1 依赖

存储相关的数据源依赖如下,使用阿里的Durid作为数据库连接池。

 <!--数据库工具-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<!--阿里工具-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>${druid.version}</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.2 MySQL和Redis配置

SpringBoot的持久化配置如下,包括MySQL和Redis。

spring:
  devtools:
    restart:
      enabled: true
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/db_monkey_run?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 10
      max-active: 100
      min-idle: 10
      max-wait: 6000
      filters: stat, wall
      stat-view-servlet:
        enabled: true
        login-username: admin
        login-password: 123456
  redis:
    database: 0
    host: 127.0.0.1
    password: 123456
    jedis:
      pool:
        max-active: 1 # 连接池:最大连接数,-1不限制
        max-idle: 8 # 连接池:最大空闲连接数量
        max-wait: 2000 # 连接池:最大阻塞等待时间,-1不限制,单位:毫秒
        min-idle: 0 # 连接池;最小空闲连接数量
      timeout: 1000 # 连接Redis服务器超时时间,单位:毫秒

server:
  port: 9125
  servlet:
    session:
      timeout: PT10S

logging:
  level:
    root: DEBUG

pagehelper:
  helperDialect: mysql
  reasonable: true
  supportMethodsArguments: true
  params: count=countSql

4 自定义用户配置

Oauth2可以自定义用户逻辑,结合认证配置,进行多租户管理,
用户认证可以通过MySQL查询数据,如果查不到,则直接提示当前用户非法,
禁止颁发认证数据,这里为了简单测试,仅使用硬编码的方式使用测试用户,
用户名使用client,密码直接使用123456,即后面使用的password,
使用MySQL存储用户进行鉴权的文章参考:https://blog.csdn.net/Xin_101/article/details/119704923
完整样例如下:

package com.monkey.security.modules.oauth2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * 自定义用户逻辑.
 *
 * @author xindaqi
 * @since 2022-10-25 19:05
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 可以定义用户验证的逻辑,如从MySQL查询用户,我在其他教程里有实现过,可参考:https://blog.csdn.net/Xin_101/article/details/119704923
        //设置用户密码
        String password = new BCryptPasswordEncoder().encode("123456");
        //创建用户以及权限
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_CLIENT"));
    }
}

5 认证服务器配置

为了方便测试,将参数配置抽象成了方法,
代码有些杂乱,见谅。
认证服务器用于配置认证参数,如授权方式、令牌过期时间、租户、授权密码等,
本文讲解密码授权和授权码授权两种。

5.1 密码模式

密码模式配置有两种方式:内存方式和持久化方式。

5.1.1 内存配置

(1) 配置核心
使用内存方式配置参数,配置样例如下图所示,
需要注意的是配置密码,使用加密的密码,不可直接使用原生字符串,
密码模式下,无需配置重定向URI,
为token配置了刷新机制:refresh_token,当生成token时,可以使用refresh_token刷新,重新获取access_token,过期时间重新计时。
在这里插入图片描述

(2)完整样例
配置完整样例如下:

package com.monkey.security.modules.oauth2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;
import java.util.Objects;

/**
 * 认证服务端配置.
 *
 * @author xindaqi
 * @since 2022-10-25 16:52
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    AuthenticationManager authenticationManager;

    @Resource
    RedisConnectionFactory redisConnectionFactory;

    @Resource
    MyUserDetailsService myUserDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Resource
    DataSource dataSource;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
        // 内存存储配置信息+密码模式
        serviceConfig(0, clientDetailsServiceConfigurer, dataSource, "password");
    }

    /**
     * 服务配置,如授权方式,token过期时间等.
     *
     * @param flag                           内存和数据库标识,0:内存;1:数据库
     * @param clientDetailsServiceConfigurer 配置器
     * @param dataSource                     数据源
     * @param grantType                      授权类型,password:密码模式;authorization_code:授权码模式
     * @throws Exception 异常
     */
    private void serviceConfig(int flag, ClientDetailsServiceConfigurer clientDetailsServiceConfigurer, DataSource dataSource, String grantType) throws Exception {
        if (flag == 1) {
            clientDetailsServiceConfigurer.jdbc(dataSource);
        } else {
            clientDetailsServiceConfigurer
                    .inMemory()
                    .withClient("oauth1")
                    .authorities("ROLE_CLIENT")
                    .authorizedGrantTypes(grantType, "refresh_token")
                    .accessTokenValiditySeconds(3600)
                    .redirectUris("http://www.bing.com")
                    .resourceIds("rid")
                    .scopes("all")
                    .secret(new BCryptPasswordEncoder().encode("123456"));
        }
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpointsConfig) throws Exception {
        // 默认情况下,授权码存储在内存中:InMemoryAuthorizationCodeServices,
        // 所以,不用配置
        endpointsConfig.tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                .userDetailsService(myUserDetailsService);
    }

    /**
     * 配置授权码存储位置.
     *
     * @param flag                                 授权码存储位置标识:0,内存;1:数据库
     * @param endpointsConfig                      端配置
     * @param randomValueAuthorizationCodeServices 授权码存储位置
     */
    private void codeStoreEndpointConfig(int flag, AuthorizationServerEndpointsConfigurer endpointsConfig, RandomValueAuthorizationCodeServices randomValueAuthorizationCodeServices) {
        if (flag == 1) {
            // 授权码存储数据库,需要配置jdbc存储
            endpointsConfig.authorizationCodeServices(randomValueAuthorizationCodeServices);
        }
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfig) throws Exception {
        serverSecurityConfig.allowFormAuthenticationForClients();
    }
}

(3)生成token
URI: / o a u t h / t o k e n /oauth/token /oauth/token请求token,
需要配置相关的入参,详细配置如下图所示,
URL格式(不包括参数): h t t p : / / i p : p o r t / o a u t h / t o k e n http://ip:port/oauth/token http://ip:port/oauth/token
方法类型:POST
参数:直接拼接到URL中,
必填参数如下表:

序号参数描述
1grant_type授权类型,密码模式:password
2password自定义的用户密码,123456
3resource_ids资源id
4client_id内存中配置的client
5client_secret内存中配置的secret

接口响应的数据包括access_token:请求资源时的认证token;refresh_token:更新access_token的token;expires_in:token过期时间。

在这里插入图片描述

Token存储使用Redis,请求生成token后,Redis生成数据,如下图所示,
由图可知,access_token和refresh_token互为键值,
以前缀区分,refresh_to_access前缀,即以refresh_token值为键,access_token为值,
用于刷新access_token使用,刷新access_token时会传入refresh_token,通过refresh_token作为键的一部分在Redis查询;
access_to_refresh前缀,以access_token值为键,refresh_token为值,用于验证资源,请求资源时会传入access_token,通过access_token作为键的一部分在Redis中查询。
在这里插入图片描述
在这里插入图片描述

(4)刷新touken
刷新token,即使用refresh_token更新当前的access_token,
更新后,access_token重新生成,并重新计时,但是,refresh_token并不会更新,
首先token的接口与上面的接口一样,详细的配置与请求如下图所示,
由图可知,不同的只是grant_type与refresh_token,使用对应的数据即可,
grant_type值为refresh_token表明刷新token;
refresh_token值为上面生成的refresh_token。
在这里插入图片描述
(5)使用token请求资源
上面生成access_token后,并配置 / a p i / v 1 / ∗ ∗ /api/v1/** /api/v1/使用Oauth2鉴权,
当访问这类资源时,需要在URL中拼接access_token,
详细配置如下图所示,成功的响应。
在这里插入图片描述
如果access_token非法, 则返回异常信息,
如下图所示,
这里没有使用统一异常拦截,所以,返回的是原生Oauth2的信息。
在这里插入图片描述

5.1.2 持久化配置

上面的配置是在内存中,其实,无论配置是在哪里都不影响已配置的信息使用,
但是,对于多租户而言,需要动态配置相关信息的话,使用持久化配置会更灵活,
如果使用内存方式,则需要修改代码,重启服务,不是长久之计,
Oauth2支持持久化配置,本文使用MySQL数据源。
(1)创建表oauth_client_details
oauth_client_details用于存储配置信息,
创建表语句如下,表中字段与内存方式属性的对应关系:
在这里插入图片描述

CREATE TABLE `db_monkey_run`.`oauth_client_details` (
  `client_id` varchar(48) NOT NULL,
  `resource_ids` varchar(256) DEFAULT NULL,
  `client_secret` varchar(256) DEFAULT NULL,
  `scope` varchar(256) DEFAULT NULL,
  `authorized_grant_types` varchar(256) DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) DEFAULT NULL,
  `authorities` varchar(256) DEFAULT NULL,
  `access_token_validity` int(11) DEFAULT NULL,
  `refresh_token_validity` int(11) DEFAULT NULL,
  `additional_information` varchar(4096) DEFAULT NULL,
  `autoapprove` varchar(256) DEFAULT NULL,
  PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

(2)初始化配置数据
从初始化数据就可以看出,持久化配置的方案与服务是解耦的,
可以随时变更数据库中的配置数据,而无需修改代码以及更新服务,
配置的测试数据client_id为oauth2,插入数据语句如下,
其中,client_secret为加密后的数据,原始密码为123456,
同时验证数据库配置生效,将token过期时间修改为4600,
authorized_grant_types有多个参数使用英文逗号隔开即可。

INSERT INTO `db_monkey_run`.`oauth_client_details`
(client_id, resource_ids, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
VALUES 
("oauth2", "rid", "$2a$10$XumFMRzG5Z0ip2fC.n7YHua3GoSRTZo0b9UfaX1pp8LpEGTHeUP66", "all", "password,refresh_token", "ROLE_CLIENT", 4600);

(3)核心配置
配置服务的存储方式,使用jdbc,其中,数据源datasource是SpringBoot配置的MySQL数据源,可以自动装配使用。
在这里插入图片描述

(4)完整测试样例

package com.monkey.security.modules.oauth2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;
import java.util.Objects;

/**
 * 认证服务端配置.
 *
 * @author xindaqi
 * @since 2022-10-25 16:52
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    AuthenticationManager authenticationManager;

    @Resource
    RedisConnectionFactory redisConnectionFactory;

    @Resource
    MyUserDetailsService myUserDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Resource
    DataSource dataSource;

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
        // 数据库存储配置信息+密码模式
        serviceConfig(1, clientDetailsServiceConfigurer, dataSource, "password");
    }

    /**
     * 服务配置,如授权方式,token过期时间等.
     *
     * @param flag                           内存和数据库标识,0:内存;1:数据库
     * @param clientDetailsServiceConfigurer 配置器
     * @param dataSource                     数据源
     * @param grantType                      授权类型,password:密码模式;authorization_code:授权码模式
     * @throws Exception 异常
     */
    private void serviceConfig(int flag, ClientDetailsServiceConfigurer clientDetailsServiceConfigurer, DataSource dataSource, String grantType) throws Exception {
        if (flag == 1) {
            clientDetailsServiceConfigurer.jdbc(dataSource);
        } else {
            clientDetailsServiceConfigurer
                    .inMemory()
                    .withClient("oauth1")
                    .authorities("ROLE_CLIENT")
                    .authorizedGrantTypes(grantType, "refresh_token")
                    .accessTokenValiditySeconds(3600)
                    .redirectUris("http://www.bing.com")
                    .resourceIds("rid")
                    .scopes("all")
                    .secret(new BCryptPasswordEncoder().encode("123456"));
        }
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpointsConfig) throws Exception {
        // 默认情况下,授权码存储在内存中:InMemoryAuthorizationCodeServices,
        // 所以,不用配置
        endpointsConfig.tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                .userDetailsService(myUserDetailsService);
    }

    /**
     * 配置授权码存储位置.
     *
     * @param flag                                 授权码存储位置标识:0,内存;1:数据库
     * @param endpointsConfig                      端配置
     * @param randomValueAuthorizationCodeServices 授权码存储位置
     */
    private void codeStoreEndpointConfig(int flag, AuthorizationServerEndpointsConfigurer endpointsConfig, RandomValueAuthorizationCodeServices randomValueAuthorizationCodeServices) {
        if (flag == 1) {
            // 授权码存储数据库,需要配置jdbc存储
            endpointsConfig.authorizationCodeServices(randomValueAuthorizationCodeServices);
        }
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfig) throws Exception {
        serverSecurityConfig.allowFormAuthenticationForClients();
    }
}

(5)生成token
与内存方式一致,唯一不同的是client_id为oauth2,为验证数据库配置是否生效,
请求样例如下图所示,
由图可知,正常获取token,其中,过期时间为4599,是4600的倒计时,数据库配置生效。
在这里插入图片描述
(6)刷新token
与内存方式相同,client_id为oauth2。
在这里插入图片描述
(7)使用token请求资源
在URL中拼接access_token参数,使用access_token请求资源,
如下图所示,成功请求数据。
在这里插入图片描述

5.2 授权码模式

与密码方式授权相比,授权码方式多了一道授权码,
使用授权码获取token,使用token获取资源,流程如下图所示,
授权码模式同样支持内存配置和持久化配置,
同时,多出来的授权码也是支持内存存储和持久化存储
在这里插入图片描述

5.2.1 内存配置

(1)核心配置
授权码内存配置如下图所示,
需要注意的是配置重定向URI用于登录成功后获取授权码,
授权类型使用authorization_code。
在这里插入图片描述
(2)完整测试样例

package com.monkey.security.modules.oauth2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;
import java.util.Objects;

/**
 * 认证服务端配置.
 *
 * @author xindaqi
 * @since 2022-10-25 16:52
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    AuthenticationManager authenticationManager;

    @Resource
    RedisConnectionFactory redisConnectionFactory;

    @Resource
    MyUserDetailsService myUserDetailsService;

    @Resource
    DataSource dataSource;

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
        // 内存存储配置信息+授权码模式
        serviceConfig(0, clientDetailsServiceConfigurer, dataSource, "authorization_code");
    }

    /**
     * 服务配置,如授权方式,token过期时间等.
     *
     * @param flag                           内存和数据库标识,0:内存;1:数据库
     * @param clientDetailsServiceConfigurer 配置器
     * @param dataSource                     数据源
     * @param grantType                      授权类型,password:密码模式;authorization_code:授权码模式
     * @throws Exception 异常
     */
    private void serviceConfig(int flag, ClientDetailsServiceConfigurer clientDetailsServiceConfigurer, DataSource dataSource, String grantType) throws Exception {
        if (flag == 1) {
            clientDetailsServiceConfigurer.jdbc(dataSource);
        } else {
            clientDetailsServiceConfigurer
                    .inMemory()
                    .withClient("oauth1")
                    .authorities("ROLE_CLIENT")
                    .authorizedGrantTypes(grantType, "refresh_token")
                    .accessTokenValiditySeconds(3600)
                    .redirectUris("http://www.bing.com")
                    .resourceIds("rid")
                    .scopes("all")
                    .secret(new BCryptPasswordEncoder().encode("123456"));
        }
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpointsConfig) throws Exception {
        // 默认情况下,授权码存储在内存中:InMemoryAuthorizationCodeServices,
        // 所以,不用配置
        endpointsConfig.tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                .userDetailsService(myUserDetailsService);
    }

    /**
     * 配置授权码存储位置.
     *
     * @param flag                                 授权码存储位置标识:0,内存;1:数据库
     * @param endpointsConfig                      端配置
     * @param randomValueAuthorizationCodeServices 授权码存储位置
     */
    private void codeStoreEndpointConfig(int flag, AuthorizationServerEndpointsConfigurer endpointsConfig, RandomValueAuthorizationCodeServices randomValueAuthorizationCodeServices) {
        if (flag == 1) {
            // 授权码存储数据库,需要配置jdbc存储
            endpointsConfig.authorizationCodeServices(randomValueAuthorizationCodeServices);
        }
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfig) throws Exception {
        serverSecurityConfig.allowFormAuthenticationForClients();
    }
}

(3)生成授权码
授权码生成的URI为: / o a u t h / a u t h o r i z e /oauth/authorize /oauth/authorize
构建的请求如下图所示,
方法:GET
URL格式(不带参): h t t p : / / i p : p o r t / o a u t h / a u t h o r i z e http://ip:port/oauth/authorize http://ip:port/oauth/authorize
参数直接在URL中拼接,完整的URL为:
http://localhost:9125/oauth/authorize?response_type=code&client_id=oauth1&scope=all
可以看到,该URL中并没有密码相关的数据,因为,该请求是在浏览器中请求,
会重定向到登录界面,输入用户名和密码。
在这里插入图片描述
(4)通过浏览器请求
http://localhost:9125/oauth/authorize?response_type=code&client_id=oauth1&scope=all
会自动进入login,这也是配置 / l o g i n / ∗ ∗ /login/** /login/授权的原因,
登录页如下图所示:
在这里插入图片描述
同意当前用户申请,并授权,
配置如下图所示。
在这里插入图片描述
授权成功后会重定向到配置的链接,并生成授权码,
如下图所示,该授权码存储在内存中,一次性使用,用完即抛。
在这里插入图片描述
(5)生成token
使用授权码生成token配置如下图所示,
方法:POST
参数:请求体,form-data格式
URI: / o a u t h / t o k e n /oauth/token /oauth/token
添加的code参数即为授权码,成功获取token。
在这里插入图片描述

(6)刷新token
授权码模式中,授权码是一次性的,用完即废弃(物理删除),
如需刷新token,需要重新生成授权码,
通过http://localhost:9125/oauth/authorize?response_type=code&client_id=oauth1&scope=all重新生成,结果如下图所示。
在这里插入图片描述
刷新token的配置如下图所示,
方法:POST
参数:请求体form-data格式,具体参数见图中描述,
其中,client_secret为配置的secret,
生成新的access_token,过期时间重新计时,refresh_token保持不变。
在这里插入图片描述

(7)请求资源
使用access_token请求资源,在URL中添加参数access_token即可,
测试样例如下图所示。
在这里插入图片描述

5.2.2 持久化配置

授权码的持久化配置,有两部分,一部分是配置参数的持久化,参见上面密码配置的持计划配置说明,
另一部分是授权码的持久化配置,即授权码存储在数据库中,但也是一次性的,用完即删,
授权码存在的意义是当还没使用时,服务重启后,该授权码仍可用,不用重新再生成,更加方便。
(1)添加配置参数
为oauth1用户添加授权码认证模式,插入语句如下,
其中,authorized_grant_types为authorization_code,refresh_token,指定授权码模式,并支持刷新token,
token过期时间配置为4600,用于验证,数据库配置是否生效。

INSERT INTO `db_monkey_run`.`oauth_client_details`
(client_id, resource_ids, client_secret, scope, authorized_grant_types, authorities, access_token_validity)
VALUES 
("oauth1", "rid", "$2a$10$XumFMRzG5Z0ip2fC.n7YHua3GoSRTZo0b9UfaX1pp8LpEGTHeUP66", "all", "authorization_code,refresh_token", "ROLE_CLIENT", 4600);

(2)新建oauth_code表
该表用于存储授权码,只需新建表,Oauth2会进行CURD。

CREATE TABLE IF NOT EXISTS `db_monkey_run`.`oauth_code` (
    `code` VARCHAR(256) NULL DEFAULT NULL,
    `authentication` BLOB NULL DEFAULT NULL
)  ENGINE=INNODB DEFAULT CHARACTER SET=UTF8;

(3)核心配置
配置使用MySQL,添加jdbc,数据源使用MySQL,已自动装配,参见完整样例。
在这里插入图片描述

授权码使用MySQL,终端配置器使用jdbc授权码服务:new JdbcAuthorizationCodeServices(dataSource)。
在这里插入图片描述

(4)完整样例

package com.monkey.security.modules.oauth2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.List;
import java.util.Objects;

/**
 * 认证服务端配置.
 *
 * @author xindaqi
 * @since 2022-10-25 16:52
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    AuthenticationManager authenticationManager;

    @Resource
    RedisConnectionFactory redisConnectionFactory;

    @Resource
    MyUserDetailsService myUserDetailsService;

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Resource
    DataSource dataSource;

    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
        // 数据库存储配置信息+授权码模式
        serviceConfig(1, clientDetailsServiceConfigurer, dataSource, "authorization_code");
    }

    /**
     * 服务配置,如授权方式,token过期时间等.
     *
     * @param flag                           内存和数据库标识,0:内存;1:数据库
     * @param clientDetailsServiceConfigurer 配置器
     * @param dataSource                     数据源
     * @param grantType                      授权类型,password:密码模式;authorization_code:授权码模式
     * @throws Exception 异常
     */
    private void serviceConfig(int flag, ClientDetailsServiceConfigurer clientDetailsServiceConfigurer, DataSource dataSource, String grantType) throws Exception {
        if (flag == 1) {
            clientDetailsServiceConfigurer.jdbc(dataSource);
        } else {
            clientDetailsServiceConfigurer
                    .inMemory()
                    .withClient("oauth1")
                    .authorities("ROLE_CLIENT")
                    .authorizedGrantTypes(grantType, "refresh_token")
                    .accessTokenValiditySeconds(3600)
                    .redirectUris("http://www.bing.com")
                    .resourceIds("rid")
                    .scopes("all")
                    .secret(new BCryptPasswordEncoder().encode("123456"));
        }
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpointsConfig) throws Exception {
        // 默认情况下,授权码存储在内存中:InMemoryAuthorizationCodeServices,
        // 所以,不用配置
        endpointsConfig.tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                .userDetailsService(myUserDetailsService);
        // 授权码存储:1:数据库,表名:oauth_code;
        codeStoreEndpointConfig(1, endpointsConfig, new JdbcAuthorizationCodeServices(dataSource));
    }

    /**
     * 配置授权码存储位置.
     *
     * @param flag                                 授权码存储位置标识:0,内存;1:数据库
     * @param endpointsConfig                      端配置
     * @param randomValueAuthorizationCodeServices 授权码存储位置
     */
    private void codeStoreEndpointConfig(int flag, AuthorizationServerEndpointsConfigurer endpointsConfig, RandomValueAuthorizationCodeServices randomValueAuthorizationCodeServices) {
        if (flag == 1) {
            // 授权码存储数据库,需要配置jdbc存储
            endpointsConfig.authorizationCodeServices(randomValueAuthorizationCodeServices);
        }
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfig) throws Exception {
        serverSecurityConfig.allowFormAuthenticationForClients();
    }
}

(5)获取授权码
构建请求,获取授权码:http://localhost:9125/oauth/authorize?response_type=code&client_id=oauth1&scope=all
登录页如下图所示,用户名:oauth1,密码:123456
在这里插入图片描述
授权码如下图所示:
在这里插入图片描述
查看数据库存储的授权码,如下图所示。
在这里插入图片描述
(6)获取token
参考如上描述,配置获取token请求,如下图所示,
获取的token过期时间为4342,说明数据库配置的过期已生效。
在这里插入图片描述
(7)请求资源
URL中配置access_token参数,请求对应的接口,
请求如下图所示,成功响应。
在这里插入图片描述

6 小结

(1)Oauth2.0认证需要配置:认证拦截、认证服务、资源服务以及用户服务,其中,认证拦截是拦截认证相关的URI,如 / o a u t h / ∗ ∗ /oauth/** /oauth/系列的URI, / l o g i n / ∗ ∗ /login/** /login/系列的URI;认证服务是认证服务器相关的配置信息,如认证方式、token有效期等;资源服务是需要使用oauth2.0进行授权访问的服务;用户服务是自定义的用户校验,可以自定义校验逻辑,如从MySQL查询账户;
(2)认证服务的授权方式有四种:密码模式、授权码模式、客户端模式和简化模式,本文实现前两种,即密码模式和授权码模式;
(3)认证服务的配置存储有两种方式:内存方式和持久化方式,其中,内存方式的配置信息随服务重启而重新加载,而持久化的方式所有配置信息均在数据库存储,与服务是解耦的,持久化的方式可以在数据库动态配置多租户,而内存方式,变更租户则要重新修改代码重新发布上线;
(4)授权码生成同样有两种方式:内存模式和持久化方式,内存方式,授权码存储在内存中,当服务重启后,授权码被消除,及时没有使用授权码获取token,持久化方式,生成的授权码存储在数据库,当服务重启后,授权码可以继续使用(如果没有使用过);
(5)授权码方式,生成的授权码都是一次性的,使用过一次,就会被删除。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐