Oauth2.0是目前流行的授权机制,用于授权第三方应用,获取数据。Oauth协议为用户资源的授权提供一个安全、开放并且简易的规范标准。和以往授权不同的是Oauth不会使第三方触及到用户的账号信息(用户和密码),也就是说第三方不需要使用用户的用户名和密码就可以获取到该用户的用户资源权限。
资源所有者(Resource Owner):通常是用户(User),如昵称、头像这些资源的拥有者(用户只是将这些资源放到服务提供商的资源服务器中)。
第三方应用:或者称为第三方客户端(Clinet),希望使用资源服务器提供的资源
认证服务器(Authorization Server):专门用于对资源所有者的身份进行认证,对要访问的资源进行授权、产生令牌的服务器。访问资源,需要通过认证服务器由资源所有者授权才可以访问。
资源服务器(Resource Server):存储用户的资源,验证令牌有效性。比如:微信资源服务器存储了微信用户信息,淘宝资源服务器存储了淘宝的用户信息。
服务提供商(Service Provider):认证服务和资源服务归属于一个机构,该机构就是服务提供商。
OAuth2认证流程
OAuth在第三方应用和服务提供商之间,设置一个授权层(authorization layer)。第三方应用不能直接登录"服务提供商",只可以通过授权层将"第三方应用"和用户区分开来。"第三方应用"通过授权层获取令牌(accesstoken),获取令牌后拿令牌去访问服务提供商。令牌和用户密码不同,可以指定授权层令牌的权限范围和有效期,"服务提供商"根据令牌的权限范围和有效期,向"第三方应用"开放用户对应的资源。第三方客户端登录主要步骤如下:
第三方应用,向认证服务器请求授权。
用户告知认证服务器同意授权(通常是通过用户扫码或输入“服务提供商”的用户名密码的方式)
认证服务器向第三方应用告知授权码(code)
第三方应用使用授权码(code)申请Access Token
认证服务器验证授权码,颁发Access Token
OAuth2四种授权方式
OAuth2有四种授权方式分别如下
授权码模式(Authorization Code):功能是最完整的,流程也是最严密的,国内各大服务提供商(微信、微博、淘宝、百度)都是使用此授权模式进行授权。该授权模式可以确定是用户进行授权的,并且令牌是认证服务器放发到第三方应用服务器,而不是浏览器上。
简化模式(Implicit):和授权码模式不同的是,令牌发放给浏览器,OAuth2客户端运行在浏览器中,通过KS脚本去申请令牌。而不是发放该第三方应用的服务器。
密码模式(resource owner password credentials):将用户和密码传过去,直接获取accesstokne,用户同意授权动作是在第三方应用上完成,而不是在认证服务器。第三方应用申请令牌时,直接带用户名和密码去向认证服务器申请令牌。这种方式认证服务器无法断定用户是否真的授权,用户和密码可能是第三方应用盗取过来的。
流程如下:
用户向客户端直接提供认证服务器想要的用户名和密码。
客户端将用户名和密码发给认证服务器,向认证服务器请求令牌
认证服务器确认后,向客户端提供访问令牌
客户端模式(client credentials):使用较少,当一个第三方应用自己本身需要获取资源(而不是以用户的名义),而不是获取用户资源时,客户端模式十分有用。
具体流程如下:
客户端向认证服务器进行身份认证,并要求一个访问令牌
认证服务器确认后,向客户端提供访问令牌
Spring Security OAuth2认证服务器
Spring Security登录信息存储在Session中,每次访问服务的时候,都会查看浏览器中Cookie中是不是存在JSESSIONID,如果不存在JSESSIONID会新建一个Session,将新建的SessionID保存到Cookie中。每一次发送请求都会通过浏览器的SessionID查找到对应的Session对象。从而获取用户信息。
前后端分离后,前端部署在单独的Web服务器,后端部署在另外的应用服务器上,浏览器先访问Web服务器,Web服务器访问请求到应用服务器,这样使用Cookie存储就不合适具体原因如下:
开发复杂
安全性差
客户体验差
有些前端技术不支持Cookie,比如:小程序
解决方式:
使用令牌方式进行认证解决上面说的问题,可以使用OAuth2协议。
org.springframework.boot
spring-boot-starter
org.projectlombok
lombok
true
cn.hutool
hutool-all
5.5.9
org.springframework.boot
spring-boot-starter-test
test
/**
* 统一返回结果接口
*/
public interface IResultCode {
/**
* 返回码
*
* @return int
*/
int getCode();
/**
* 返回消息
*
* @return String
*/
String getMsg();
}
@Getter
@AllArgsConstructor
public enum ResultCode implements IResultCode{
/**
* 操作成功
*/
SUCCESS(200, "操作成功"),
/**
* 业务异常
*/
FAILURE(400, "业务异常"),
/**
* 服务异常
*/
ERROR(500, "服务异常"),
/**
* 参数错误
*/
GLOBAL_PARAM_ERROR(540, "参数错误");
/**
* 状态码
*/
final int code;
/**
* 消息内容
*/
final String msg;
}
@Data
@Getter
public class Result implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 状态码
*/
private int code;
/**
* 状态信息
*/
private String msg;
/**
*
*/
private Date time;
private T data;
private Result() {
this.time = new Date();
}
private Result(IResultCode resultCode) {
this(resultCode, null, resultCode.getMsg());
}
private Result(IResultCode resultCode, String msg) {
this(resultCode, null, msg);
}
private Result(IResultCode resultCode, T data) {
this(resultCode, data, resultCode.getMsg());
}
private Result(IResultCode resultCode, T data, String msg) {
this(resultCode.getCode(), data, msg);
}
private Result(int code, T data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
this.time = new Date();
}
/**
* 返回状态码
*
* @param resultCode 状态码
* @param 泛型标识
* @return ApiResult
*/
public static Result success(IResultCode resultCode) {
return new Result<>(resultCode);
}
public static Result success(String msg) {
return new Result<>(ResultCode.SUCCESS, msg);
}
public static Result success(IResultCode resultCode, String msg) {
return new Result<>(resultCode, msg);
}
public static Result data(T data) {
return data(data, "处理成功");
}
public static Result data(T data, String msg) {
return data(ResultCode.SUCCESS.code, data, msg);
}
public static Result data(int code, T data, String msg) {
return new Result<>(code, data, data == null ? "承载数据为空" : msg);
}
public static Result fail() {
return new Result<>(ResultCode.FAILURE, ResultCode.FAILURE.getMsg());
}
public static Result fail(String msg) {
return new Result<>(ResultCode.FAILURE, msg);
}
public static Result fail(int code, String msg) {
return new Result<>(code, null, msg);
}
public static Result fail(IResultCode resultCode) {
return new Result<>(resultCode);
}
public static Result fail(IResultCode resultCode, String msg) {
return new Result<>(resultCode, msg);
}
public static Result condition(boolean flag) {
return flag ? success("处理成功") : fail("处理失败");
}
}
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth
spring-security-oauth2
2.3.6.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
2.3.12.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
com.alibaba
druid
1.1.12
com.baomidou
mybatis-plus-boot-starter
3.5.1
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
server:
port: 8899
spring:
thymeleaf:
cache: false
application:
name: oauth2-server
创建作用:
配置允许访问此认证服务器的客户端信息,没有再次配置的客户端信息不允许访问。
管理令牌
配置令牌管理策略(JDBC/Redis/JWT)
配置令牌生成策略
配置令牌端点
令牌端点的安全配置
在spring-oauth2-server
模块创建认证配置类:
创建 spring.oauth2.server.config.OAuth2AuthorizationServerConfig
类继承AuthorizationServerConfigurerAdapter
在OAuth2AuthorizationServerConfig
类上添加注解:
@Configuration
@EnableAuthorizationServer
认证服务器
配置说明:
可以配置:“authorization_code”, “password”, “implicit”,“client_credentials”,“refresh_token”
scopes
:授权范围标识,比如指定微服务名称,则只可以访问指定的微服务
autoApprove
: false跳转到授权页面手动点击授权,true不需要手动授权,直接响应授权码
redirectUris
:当获取授权码后,认证服务器会重定向到指定的这个URL
,并且带着一个授权码code
响应。
withClient
:允许访问此认证服务器的客户端ID
secret
:客户端密码,加密存储
authorizedGrantTypes
:授权类型,支持同时多种授权类型
/**
* 认证服务器
*/
@Configuration
@EnableAuthorizationServer //开启认证服务器
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
//在 MyOAuth2Config 添加到容器了
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 配置被允许访问此认证服务器的客户端详细信息
* 1.内存管理
* 2.数据库管理方式
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//客户端名称
.withClient("test-pc")
//客户端密码
.secret(passwordEncoder.encode("123456"))
//资源id,商品资源
.resourceIds("oauth2-server")
//授权类型, 可同时支持多种授权类型
.authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
//授权范围标识,哪部分资源可访问(all是标识,不是代表所有)
.scopes("all")
// false 跳转到授权页面手动点击授权,true 不用手动授权,直接响应授权码
.autoApprove(false)
.redirectUris("http://www.baidu.com/")//客户端回调地址
;
}
}
创建spring.oauth2.server.config.MyOAuth2Config
类,向容器中添加加密方式 BCrypt
@Configuration
public class MyOAuth2Config {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder() ;
}
}
指定认证用户的用户名和密码,用户和密码是资源的所有者。这个用户名和密码和客户端id和密码是不一样的,客户端ID和密码是应用系统的标识,每个应用系统对应一个客户端id和密码。
在spring-oauth2-server
模块创建安全配置类:
创建spring.oauth2.server.config.OAuth2SecurityConfig
类继承WebSecurityConfigurerAdapter
在OAuth2SecurityConfig
类上添加注解
@EnableWebSecurity
,包含了@Confifiguration
注解
/**
* 安全配置类
*/
@EnableWebSecurity
public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailsService myUserDetailsService;
/**
* 用户类信息
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder.encode("123456"))
.authorities("admin_role")
;
}
}
Spring Security对OAuth2提供了默认可访问端点,即URL
/oauth/authorize
:申请授权码code,涉及类AuthorizationEndpoint
/oauth/token
:获取令牌token,涉及类TokenEndpoint
/oauth/check_token
:用于资源服务器请求端点来检查令牌是否有效,涉及类CheckTokenEndpoint
/oauth/confirm_access
:用于确认授权提交,涉及类WhitelabelApprovalEndpoint
/oauth/error
:授权错误信息,涉及WhitelabelErrorEndpoint
/oauth/token_key
:提供公有密匙的端点,使用JWT令牌时会使用,涉及类TokenKeyEndpoint
涉及类org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
http://localhost:8899/oauth/authorize?client_id=test-pc&response_type=code
AuthorizationEndpoint
后,授权中心会要求资源所有者进行身份验证注:
1. 此处输入的用户名、密码是在认证服务器输入的(看端口8899),而不是在客户端上输入的,这样更加安全,因为客户端不知道用户名和密码
2. 密码模式中,输入的用户名、密码不是在认证服务器上输入,而是在客户端输入的,这样客户端就不太安全。
redirect_uri
,回调路径会携带一个授权码(code=8OsDS8
),如下图涉及:TokenEndpoint
POST
方式请求:http://localhost:8899/oauth/token
post
方式,请求体中指定授权方式和授权码认证服务器配置-密码模式
密码模式(resource owner password credentials),用户向客户端提供自己在认证服务器上的用户和密码,然后客户端通过用户提供的用户名和密码向认证服务器获取令牌。
但是如果用户名和密码遗漏,认证服务器无法判断客户端提交的用户和密码是否是盗取的,那意味着令牌就可以随时获取,信息容易泄露。
spring.oauth2.server.config.OAuth2SecurityConfig
,将AuthenticationManager
注入到bean /**
* password 密码模式要使用此认证管理器
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
在认证服务器配置类OAuth2AuthorizationServerConfig
中
configure(AuthorizationServerEndpointsConfigurer endpoints)
方法,用于配置令牌访问端点,把authenticationManager
注入并添加 @Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//密码模式需要配置认证管理器
endpoints.authenticationManager(authenticationManager);
}
test-pc
客户端添加支持密码模,可以同时支持多个模式,配置如下 .authorizedGrantTypes("authorization_code", "password", "implicit","client_credentials","refresh_token")
使用浏览器访问http://localhost:8899/oauth/token
重新启动浏览器
Postman中将 client_id:client_secret 通过 Base64 编码
认证服务器配置- 简化授权模式
不通过第三方应用程序,直接在浏览器中向认证服务器申请令牌,不需要先获取授权码。直接可以一次请求就可得到令牌,在 redirect_uri 指定的回调地址中传递令牌( access_token )。该模式适合直接运行在浏览器上的应用,不用后端支持(例如 Javascript 应用)
在OAuth2AuthorizationServerConfig
类中的configure(ClientDetailsServiceConfigurer clients)
方法中指定implicit
http://localhost:8899/oauth/authorize?client_id=test-pc&response_type=token
注:此时response_typ
的参数值必须是token
redirect_uri
,回调路径会,回调路径携带着令牌 access_token 、 expires_in 、 scope 等 ,如下图:认证服务器配置- 客户端授权模式
客户端模式(client credentials)是指客户端以自己名义,而不是用户名义,向认证服务器进行认证,严格说客户端模式并不属于
OAuth2
框架所解决的问题,在这种模式下,用户直接向客户端注册,客户端以自己的名义向认证服务器提供服务,实际上并不存在授权问题。
客户端向认证服务器进行身份认证,并要求一个访问令牌。
认证服务器确认无误后,向客户端提供访问令牌。
在OAuth2AuthorizationServerConfig
类的configure(ClientDetailsServiceConfigurer clients)
方法中指定client_credentials
2.post 方式,请求体中指定授权类型grant_type :client_credentials
注响应结果没有刷新令牌
认证服务器配置-令牌刷新策略
如果用户访问资源的时候,客户端的令牌已经过期,那么就需要更新令牌,申请一个新的访问令牌。
客户端发出更新令牌的Http请求,包含以下参数:
grant_type
:表示使用授权模式,此处固定值为refresh_token
refresh_token
:表示早前收到的需要更新的令牌
scope
:表示申请的授权范围,不可以超出上一次申请的范围。
**注:刷新令牌只有在授权模式和密码模式中才有,对应的指定这两种模式时,在类型上加上refresh_token
**。
当前报错: Internal Server Error , 对应idea控制台也发出警告: UserDetailsService is required.
原因当前需要使用内存方式存储了用户令牌,应用使用UserDetailsService才行
MyUserDetailsService
动态获取用户令牌@Component
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User("admin", passwordEncoder.encode("123456"),
AuthorityUtils.commaSeparatedStringToAuthorityList("admin_role"));
}
}
OAuth2SecurityConfig
中注入myUserDetailsService
@Autowired
private UserDetailsService myUserDetailsService;
/**
* 用户类信息
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService) ;
}
3.认证配置类OAuth2AuthorizationServerConfig
的configure(AuthorizationServerEndpointsConfigurer endpoints)
方法上加入到令牌端点上
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//密码模式需要配置认证管理器
endpoints.authenticationManager(authenticationManager);
//刷新令牌获取新令牌时需要
endpoints.userDetailsService(myUserDetailsService);
}
重启认证服务器
Postman中将 client_id:client_secret 通过 Base64 编码
认证服务器配置-令牌管理策略Redis & JDBC
默认情况下,令牌是通过randomUUID产生的32为随机数来进行填充,从而产生的令牌默认是存储在内存中。
内存存储采用的是
TokenStore
接口默认实现类InMemoryTokenStore
,开发时方便调试,适用单机版
RedisTokenStore
将令牌存储到Redis非关系型数据库,适用于高并发服务
JdbcTokenStore
基于JDBC将令牌存储到关系型数据库中,可以在不同的服务器间共享令牌
JWtTokenStore
将用户信息存储到令牌中,这样后端就可以不存储,前端拿到令牌后可以直接解析出用户信息。
spring-oauth2-server
模块中添加Redis
相关依赖
org.springframework.boot
spring-boot-starter-data-redis
2.3.12.RELEASE
application.yml
中添加redis
的配置spring:
thymeleaf:
cache: false
application:
name: oauth2-server
redis:
port: 6379
host: 127.0.0.1
connect-timeout: 50000
在MyOAuth2Config类中注入RedisTokenStore
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* Redis令牌管理
* 步骤:
* 1.启动redis
* 2.添加redis依赖
* 3.添加redis 依赖后, 容器就会有 RedisConnectionFactory 实例
* @return
*/
@Bean
public TokenStore redisTokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
将上面令牌管理策略作用到认证服务器端点上,这样策略就可以生效
/**
* 令牌管理策略
*/
@Autowired
private TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//密码模式需要配置认证管理器
endpoints.authenticationManager(authenticationManager);
//刷新令牌获取新令牌时需要
endpoints.userDetailsService(myUserDetailsService);
//令牌管理策略
endpoints.tokenStore(tokenStore);
//授权码管理策略,针对授权码模式有效,会将授权码放到 auth_code 表,授权后就会删除它
endpoints.authorizationCodeServices(jdbcAuthorizationCodeS
ervices);
}
注入TokenStore
在OAuth2AuthorizationServerConfig
类的configure(AuthorizationServerEndpointsConfigurer endpoints)
方法中将tokenStore添加到端点上
重启认证服务器
使用 flflushall 命令清除所有数据,方便后面查看
使用密码模式获取令牌
keys * 查看效果如下
Spring官方提供了存储OAuth2相关信息的数据库表结构
https://github.com/spring-projects/spring-security-oauth/blob/main/spring-security-oauth2/src/test/resources/schema.sql
当前使用Mysql数据库,需要修改以下数据类型:
官方提供的表结构主键类型VARCHAR(256)
,超过了Mysql的限制长度128,需要修改为VARCHAR(128)
将LONGVARBINARY
类型修改为BLOB
类型
修改后的表结构如下:
-- used in tests that use HSQL
create table oauth_client_details (
client_id VARCHAR(128) PRIMARY KEY,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
scope VARCHAR(256),
authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
autoapprove VARCHAR(256)
);
INSERT INTO `oauth_client_details` VALUES ('test-pc', 'oauth2-server,oauth2-resource', '$2a$10$Q2Dv45wFHgxQkFRaVNAzeOJorpTH2DwHb975VeHET30QsqwuoQOAe', 'all,Base_API', 'authorization_code,password,implicit,client_credentials,refresh_token', 'http://www.baidu.com/', NULL, 50000, NULL, NULL, 'false');
create table oauth_client_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
create table oauth_access_token (
token_id VARCHAR(256),
token BLOB,
authentication_id VARCHAR(256) PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
authentication BLOB,
refresh_token VARCHAR(256)
);
create table oauth_refresh_token (
token_id VARCHAR(256),
token BLOB,
authentication BLOB
);
create table oauth_code (
code VARCHAR(256),
authentication BLOB
);
create table oauth_approvals (
userId VARCHAR(256),
clientId VARCHAR(256),
scope VARCHAR(256),
status VARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
-- customized oauth_client_details table
create table ClientDetails (
appId VARCHAR(256) PRIMARY KEY,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
scope VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
authorities VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
其中有 mybatis-plus 因为后面要用,所以一起添加进来
mysql
mysql-connector-java
runtime
com.alibaba
druid
1.1.12
com.baomidou
mybatis-plus-boot-starter
3.5.1
在spring-oauth2-server
模块的application.yml
中添加数据源
server:
port: 8899
spring:
thymeleaf:
cache: false
application:
name: oauth2-server
datasource:
url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&serverTimezone=Asia/Shanghai&characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# 数据源其他配置, 在 DruidConfig配置类中手动绑定 initialSize: 8
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
redis:
port: 6379
host: 127.0.0.1
connect-timeout: 50000
MyOAuth2Config
将DruidDataSource 数据源注入 /**
* druid数据源
* @return
*/
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
MyOAuth2Config
指定 JDBC 管理 JdbcTokenStore /**
* jdbc管理令牌
*步骤:
* 1.创建相关表
* 2.添加jdbc相关依赖
* 3.配置数据源信息
* @return
*/
@Bean
public TokenStore jdbcTokenStore(){
return new JdbcTokenStore(druidDataSource());
}
重启认证服务器
使用密码模式进行授权操作,然后查询 oauth_access_token 表就存储了令牌信息
认证服务器配置-JDBC管理授权码
授权码主要是操作
oauth_code
表,只有当grant_type是authorization_code(授权码模式)
时,该表中才会有数据产生,其他模式下oauth_code
表不会生成数据。
MyOAuth2Config
类中注入AuthorizationCodeServices
/**
* 授权码管理策略
* @return
*/
@Bean
public AuthorizationCodeServices jdbcAuthorizationCodeServices(){
//使用JDBC方式保存授权码到 oauth_code中
return new JdbcAuthorizationCodeServices(druidDataSource());
}
OAuth2AuthorizationServerConfig
中的configure(AuthorizationServerEndpointsConfigurer endpoints)
方法中将授权码添加到端点上重启项目
发送请求获取授权码
http://localhost:8899/oauth/authorize?client_id=test-pc&response_type=code
认证服务器配置-JDBC存储客户端信息
查看客户端表oauth_client_details
中的字段信息详解:
client_id
:表示客户端ID
resource_ids
:可以访问资源服务器的ID,不写则不需要校验
client_secret
:客户端密码,此处不能是明文,需要加密
scope
:客户端授权范围,指定默认不需要校验
authorized_grant_types
:客户端授权类型,支持多个使用逗号分隔
authorization_code,password,implicit,client_credentials,refresh_token
web_server_redirect_uri
:服务器的回调地址
autoapprove
:false表示需要手动授权,true表示不需要自动授权
注:需要使用BCryptPasswordEncoder为client_secret对客户端密码进行加密
/**
* 使用 JDBC 方式管理客户端信息
* @return
*/
@Bean
public ClientDetailsService jdbcClientDetailsService(){
return new JdbcClientDetailsService(druidDataSource());
}
OAuth2AuthorizationServerConfig
的confifigure(ClientDetailsServiceConfifigurer)
切换成JDBC 方式管理客户端信息 @Autowired
private ClientDetailsService jdbcClientDetailsService;
/**
* 配置被允许访问此认证服务器的客户端详细信息
* 1.内存管理
* 2.数据库管理方式
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService);
;
}
重启认证服务器
使用 admin 用户获取令牌看是否正常, 可以把数据库中scope值更改下,看是不是响应修改后的
认证服务器配置-令牌端点的安全策略
端点403不允许访问
Spring Security对OAuth2提供了默认可访问端点,即URL
/oauth/authorize
:申请授权码code,涉及类AuthorizationEndpoint
/oauth/token
:获取令牌token,涉及类TokenEndpoint
/oauth/check_token
:用于资源服务器请求端点来检查令牌是否有效,涉及类CheckTokenEndpoint
/oauth/confirm_access
:用于确认授权提交,涉及类WhitelabelApprovalEndpoint
/oauth/error
:授权错误信息,涉及WhitelabelErrorEndpoint
/oauth/token_key
:提供公有密匙的端点,使用JWT令牌时会使用,涉及类TokenKeyEndpoint
默认情况下/oauth/check_token和/oauth/token_key端点默认是 denyAll() 拒绝访问的权限,如果这两个端点需要访问,要对他们进行认证和授权,才可以访问
请求头还是需要设置 client_id:client_secret
Base64编码
指定isAuthenticated()
认证后可以访问 /oauth/check_token
端点,指定 permitAll()
所有人可访问/oauth/token_key
端点,后面要获取公钥。在 OAuth2AuthorizationServerConfig
类中覆盖configure(AuthorizationServerSecurityConfigurer security)
方法如下:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//所有人可访问 /oauth/token_key 后面要获取公钥, 默认拒绝访问
security.tokenKeyAccess("permitAll()");
// 认证后可访问 /oauth/check_token , 默认拒绝访问
security.checkTokenAccess("isAuthenticated()");
}
重启认证服务器
检查令牌对应的用户信息
实现资源服务器
实现资源服务器的有两种方式:
@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestMatchers()
.antMatchers("/api/**");
}
}
@RestController
@RequestMapping("/api")
public class HelloController {
@RequestMapping("/hello/{name}")
public String hello(@PathVariable("name") String name) {
return "Hello Oauth2 :" +name;
}
}
创建spring-oauth2-resourceapi
模块
org.springframework.security.oauth
spring-security-oauth2
2.3.6.RELEASE
org.springframework.boot
spring-boot-starter-data-redis
2.3.12.RELEASE
org.springframework.boot
spring-boot-starter-thymeleaf
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
com.alibaba
druid
1.1.12
com.baomidou
mybatis-plus-boot-starter
3.5.1
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
org.springframework.security
spring-security-test
test
@RestController
@RequestMapping("/test")
public class HelloController {
@RequestMapping("/hello/{name}")
public String hello(@PathVariable("name") String name) {
return "Hello spring-oauth2-resourceapi :" +name;
}
}
创建资源服务器配置类OAuth2ResourceServer
类继承ResourceServerConfigurerAdapter
在类上添加注解
@Configuration
@EnableResourceServer
:标识资源服务器,所有发往当前服务的请求,都会去请求头里找token,找不到或 验证不通过不允许访问
@EnableGlobalMethodSecurity(prePostEnabled = true)
:开启方法级权限控制
configure(ResourceServerSecurityConfigurer resources)
配置当前资源服务器ID
添加校验令牌服务
创建 RemoteTokenServices 远程校验令牌服务,去校验令牌有效性,因为当前认证和资源服务器不是在同一工程中,所以要通过远程调用认证服务器校验令牌是否有效
如果认证和资源服务器在同一工程中,可以使用 DefaultTokenServices 配置校验令牌。
@Configuration
@EnableResourceServer // 标识为资源服务器, 所有发往当前服务的请求,都会去请求头里找token,找不到或 验证不通过不允许访问
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
@Resource
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestMatchers()
.antMatchers("/test/**");
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("oauth2-resource")
.tokenServices(tokenServices());
}
/**
* 配置资源服务器如何校验token
* 1. DefaultTokenServices
* 如果认证服务器和资源服务器在同一个服务,则直接采用默认服务验证
* 2.RemoteTokenServices
* 当认证服务器和资源服务器不在同一个服务,要使用此服务器去远程认证服务器验证
* @return
*/
@Primary
@Bean
public RemoteTokenServices tokenServices() {
//资源服务器去远程认证服务器验证 token 是否有效
final RemoteTokenServices tokenService = new RemoteTokenServices();
//请求认证服务器验证URL,注意:默认这个端点是拒绝访问的,要设置认证后可访问
tokenService.setCheckTokenEndpointUrl("http://localhost:8899/oauth/check_token");
//在认证服务器配置的客户端id
tokenService.setClientId("test-pc");
//在认证服务器配置的客户端密码
tokenService.setClientSecret("123456");
return tokenService;
}
}
启动认证服务器和资源服务器
通过密码方式获取令牌
请求头带上令牌请求/test/hello 资源
如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!