赞
踩
Oauth2.0是目前流行的授权机制,用于授权第三方应用,获取数据。Oauth协议为用户资源的授权提供一个安全、开放并且简易的 规范标准 。和以往授权不同的是Oauth不会使第三方触及到用户的账号信息(用户和密码),也就是说第三方不需要使用用户的用户名和密码就可以获取到该用户的用户资源权限。
OAuth在第三方应用和服务提供商之间,设置一个授权层(authorization layer)。第三方应用不能直接登录"服务提供商",只可以通过授权层将"第三方应用"和用户区分开来。"第三方应用"通过授权层获取令牌(accesstoken),获取令牌后拿令牌去访问服务提供商。令牌和用户密码不同,可以指定授权层令牌的权限范围和有效期,"服务提供商"根据令牌的权限范围和有效期,向"第三方应用"开放用户对应的资源。第三方客户端登录主要步骤如下:
OAuth2有四种授权方式分别如下
授权码模式(Authorization Code):功能是最完整的,流程也是最严密的,国内各大服务提供商(微信、微博、淘宝、百度)都是使用此授权模式进行授权。该授权模式可以确定是用户进行授权的, 并且令牌是认证服务器放发到第三方应用服务器,而不是浏览器上 。
简化模式(Implicit):和授权码模式不同的是,令牌发放给浏览器,OAuth2客户端运行在浏览器中,通过KS脚本去申请令牌。而不是发放该第三方应用的服务器。
密码模式(resource owner password credentials):将用户和密码传过去,直接获取 accesstokne ,用户同意授权动作是在 第三方应用上完成 ,而不是在认证服务器。第三方应用申请令牌时,直接带用户名和密码去向认证服务器申请令牌。 这种方式认证服务器无法断定用户是否真的授权,用户和密码可能是第三方应用盗取过来的 。
流程如下:
客户端模式(client credentials):使用较少,当一个第三方应用自己本身需要获取资源(而不是以用户的名义),而不是获取用户资源时,客户端模式十分有用。
具体流程如下:
Spring Security登录信息存储在Session中,每次访问服务的时候,都会查看浏览器中Cookie中是不是存在JSESSIONID,如果不存在JSESSIONID会新建一个Session,将新建的SessionID保存到Cookie中。每一次发送请求都会通过浏览器的SessionID查找到对应的Session对象。从而获取用户信息。
前后端分离后,前端部署在单独的Web服务器,后端部署在另外的应用服务器上,浏览器先访问Web服务器,Web服务器访问请求到应用服务器,这样使用Cookie存储就不合适具体原因如下:
解决方式:
使用令牌方式进行认证解决上面说的问题,可以使用OAuth2协议。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.5.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
/** * 统一返回结果接口 */ 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<T> 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 <T> 泛型标识 * @return ApiResult */ public static <T> Result<T> success(IResultCode resultCode) { return new Result<>(resultCode); } public static <T> Result<T> success(String msg) { return new Result<>(ResultCode.SUCCESS, msg); } public static <T> Result<T> success(IResultCode resultCode, String msg) { return new Result<>(resultCode, msg); } public static <T> Result<T> data(T data) { return data(data, "处理成功"); } public static <T> Result<T> data(T data, String msg) { return data(ResultCode.SUCCESS.code, data, msg); } public static <T> Result<T> data(int code, T data, String msg) { return new Result<>(code, data, data == null ? "承载数据为空" : msg); } public static <T> Result<T> fail() { return new Result<>(ResultCode.FAILURE, ResultCode.FAILURE.getMsg()); } public static <T> Result<T> fail(String msg) { return new Result<>(ResultCode.FAILURE, msg); } public static <T> Result<T> fail(int code, String msg) { return new Result<>(code, null, msg); } public static <T> Result<T> fail(IResultCode resultCode) { return new Result<>(resultCode); } public static <T> Result<T> fail(IResultCode resultCode, String msg) { return new Result<>(resultCode, msg); } public static <T> Result<T> condition(boolean flag) { return flag ? success("处理成功") : fail("处理失败"); } }
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- for OAuth 2.0 --> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.6.RELEASE</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.12.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
server: port: 8899 spring: thymeleaf: cache: false application: name: oauth2-server
创建作用:
在 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
:允许访问此认证服务器的客户端IDsecret
:客户端密码, 加密存储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/")//客户端回调地址 ; } }
统一管理Bean配置类
创建 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
获取请求授权码Code
涉及类 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
),如下图通过授权码获取令牌token
涉及: 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
不通过第三方应用程序,直接在浏览器中向认证服务器申请令牌,不需要先获取授权码。直接可以一次请求就可得到令牌,在 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才行
创建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); }
测试获取新令牌
默认情况下,令牌是通过randomUUID产生的32为随机数来进行填充,从而产生的令牌默认是存储在内存中。
TokenStore
接口默认实现类 InMemoryTokenStore
,开发时方便调试,适用单机版RedisTokenStore
将令牌存储到Redis非关系型数据库,适用于高并发服务JdbcTokenStore
基于JDBC将令牌存储到关系型数据库中,可以在不同的服务器间共享令牌JWtTokenStore
将用户信息存储到令牌中,这样后端就可以不存储,前端拿到令牌后可以直接解析出用户信息。启动Redis的服务器端和客户端
添加Redis的依赖
spring-oauth2-server
模块中添加 Redis
相关依赖<!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.12.RELEASE</version> </dependency>
application.yml
中添加 redis
的配置spring: thymeleaf: cache: false application: name: oauth2-server redis: port: 6379 host: 127.0.0.1 connect-timeout: 50000
配置Redis的管理TokenStore
在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); }
OAuth2AuthorizationServerConfig
类的 configure(AuthorizationServerEndpointsConfigurer endpoints)
方法中将tokenStore添加到端点上测试
创建相关数据表
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) );
添加JDBC相关依赖
其中有 mybatis-plus 因为后面要用,所以一起添加进来
<!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency>
配置数据源信息
在 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
配置JDBC的管理JdbcTokenStore
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_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
查看客户端表 oauth_client_details
中的字段信息详解:
client_id
:表示客户端IDresource_ids
:可以访问资源服务器的ID,不写则不需要校验client_secret
:客户端密码, 此处不能是明文,需要加密scope
:客户端授权范围,指定默认不需要校验authorized_grant_types
:客户端授权类型,支持多个使用逗号分隔authorization_code,password,implicit,client_credentials,refresh_token
web_server_redirect_uri autoapprove
注:需要使用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); ; }
端点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
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
模块
<dependencies> <!-- for OAuth 2.0 --> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.6.RELEASE</version> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.3.12.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!--druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.12</version> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
@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 @EnableGlobalMethodSecurity(prePostEnabled = true)
重写资源服务器相关配置方法 configure(ResourceServerSecurityConfigurer resources)
@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; } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。