实战讲解Spring Oauth2.0密码模式和授权码模式(内存inMemory+持久化jdbc配置)
(1)Oauth2.0认证需要配置:认证拦截、认证服务、资源服务以及用户服务,其中,认证拦截是拦截认证相关的URI,如$/oauth/**$系列的URI,$/login/**$系列的URI;认证服务是认证服务器相关的配置信息,如认证方式、token有效期等;资源服务是需要使用oauth2.0进行授权访问的服务;用户服务是自定义的用户校验,可以自定义校验逻辑,如从MySQL查询账户;(2)认证服务的
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中,
必填参数如下表:
序号 | 参数 | 描述 |
---|---|---|
1 | grant_type | 授权类型,密码模式:password |
2 | password | 自定义的用户密码,123456 |
3 | resource_ids | 资源id |
4 | client_id | 内存中配置的client |
5 | client_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)授权码方式,生成的授权码都是一次性的,使用过一次,就会被删除。
更多推荐
所有评论(0)