spring security oauth2 入门(SpringSecurity-OAuth2万文详解)
Oauth2.0是目前流行的授权机制,用于授权第三方应用,获取数据。Oauth协议为用户资源的授权提供一个安全、开放并且简易的规范标准。和以往授权不同的是OAuth不会使第三方触及到用户的账号信息(用户和密码),也就是说第三方不需要使用用户的用户名和密码就可以获取到该用户的用户资源权限。
OAuth2设计的角色- 资源所有者(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)授权码模式(Authorization Code):功能是最完整的,流程也是最严密的,国内各大服务提供商(微信、微博、淘宝、百度)都是使用此授权模式进行授权。该授权模式可以确定是用户进行授权的,并且令牌是认证服务器放发到第三方应用服务器,而不是浏览器上。
简化模式(implicit)
简化模式(Implicit):和授权码模式不同的是,令牌发放给浏览器,OAuth2客户端运行在浏览器中,通过KS脚本去申请令牌。而不是发放该第三方应用的服务器。
密码模式(resource owner password credentials)
密码模式(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协议。
基础模块创建
- 创建spring-oauth2-base模块
- 在spring-oauth2-base模块的pom.xml中添加相关依赖
<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>
- 在spring.oauth2.base.api模块下添加IResultCode接口
/** *统一返回结果接口 */ publicinterfaceIResultCode{ /** *返回码 * *@returnint */ intgetCode(); /** *返回消息 * *@returnString */ StringgetMsg(); }
- 在spring.oauth2.base.api模块下添加ResultCode实现IResultCode
@Getter @AllArgsConstructor publicenumResultCodeimplementsIResultCode{ /** *操作成功 */ SUCCESS(200,"操作成功"), /** *业务异常 */ FAILURE(400,"业务异常"), /** *服务异常 */ ERROR(500,"服务异常"), /** *参数错误 */ GLOBAL_PARAM_ERROR(540,"参数错误"); /** *状态码 */ finalintcode; /** *消息内容 */ finalStringmsg; }
- 在spring.oauth2.base.api模块下添加Result用于统一结果处理
认证服务器模块创建
@Data @Getter publicclassResult<T>implementsSerializable{ privatestaticfinallongserialVersionUID=1L; /** *状态码 */ privateintcode; /** *状态信息 */ privateStringmsg; /** * */ privateDatetime; privateTdata; privateResult(){ this.time=newDate(); } privateResult(IResultCoderesultCode){ this(resultCode,null,resultCode.getMsg()); } privateResult(IResultCoderesultCode,Stringmsg){ this(resultCode,null,msg); } privateResult(IResultCoderesultCode,Tdata){ this(resultCode,data,resultCode.getMsg()); } privateResult(IResultCoderesultCode,Tdata,Stringmsg){ this(resultCode.getCode(),data,msg); } privateResult(intcode,Tdata,Stringmsg){ this.code=code; this.data=data; this.msg=msg; this.time=newDate(); } /** *返回状态码 * *@paramresultCode状态码 *@param<T>泛型标识 *@returnApiResult */ publicstatic<T>Result<T>success(IResultCoderesultCode){ returnnewResult<>(resultCode); } publicstatic<T>Result<T>success(Stringmsg){ returnnewResult<>(ResultCode.SUCCESS,msg); } publicstatic<T>Result<T>success(IResultCoderesultCode,Stringmsg){ returnnewResult<>(resultCode,msg); } publicstatic<T>Result<T>data(Tdata){ returndata(data,"处理成功"); } publicstatic<T>Result<T>data(Tdata,Stringmsg){ returndata(ResultCode.SUCCESS.code,data,msg); } publicstatic<T>Result<T>data(intcode,Tdata,Stringmsg){ returnnewResult<>(code,data,data==null?"承载数据为空":msg); } publicstatic<T>Result<T>fail(){ returnnewResult<>(ResultCode.FAILURE,ResultCode.FAILURE.getMsg()); } publicstatic<T>Result<T>fail(Stringmsg){ returnnewResult<>(ResultCode.FAILURE,msg); } publicstatic<T>Result<T>fail(intcode,Stringmsg){ returnnewResult<>(code,null,msg); } publicstatic<T>Result<T>fail(IResultCoderesultCode){ returnnewResult<>(resultCode); } publicstatic<T>Result<T>fail(IResultCoderesultCode,Stringmsg){ returnnewResult<>(resultCode,msg); } publicstatic<T>Result<T>condition(booleanflag){ returnflag?success("处理成功"):fail("处理失败"); } }
- 创建spring-oauth2-server模块
- 添加依赖pom.xml
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--forOAuth2.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>
- 配置application.yml
认证服务器配置-授权码模式创建配置类
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:授权类型,支持同时多种授权类型
统一管理Bean配置类
/** *认证服务器 */ @Configuration @EnableAuthorizationServer//开启认证服务器 publicclassOAuth2AuthorizationServerConfigextendsAuthorizationServerConfigurerAdapter{ //在MyOAuth2Config添加到容器了 @Autowired privatePasswordEncoderpasswordEncoder; /** *配置被允许访问此认证服务器的客户端详细信息 *1.内存管理 *2.数据库管理方式 *@paramclients *@throwsException */ @Override publicvoidconfigure(ClientDetailsServiceConfigurerclients)throwsException{ 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 publicclassMyOAuth2Config{ @Bean publicPasswordEncoderpasswordEncoder(){ returnnewBCryptPasswordEncoder(); } }
指定认证用户的用户名和密码,用户和密码是资源的所有者。这个用户名和密码和客户端id和密码是不一样的,客户端ID和密码是应用系统的标识,每个应用系统对应一个客户端id和密码。
在spring-oauth2-server模块创建安全配置类:
- 创建spring.oauth2.server.config.OAuth2SecurityConfig类继承WebSecurityConfigurerAdapter
- 在OAuth2SecurityConfig类上添加注解
- @EnableWebSecurity,包含了@Confifiguration注解
令牌访问端点
/** *安全配置类 */ @EnableWebSecurity publicclassOAuth2SecurityConfigextendsWebSecurityConfigurerAdapter{ @Autowired privatePasswordEncoderpasswordEncoder; @Autowired privateUserDetailsServicemyUserDetailsService; /** *用户类信息 *@paramauth *@throwsException */ @Override protectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException{ auth.inMemoryAuthentication() .withUser("admin") .password(passwordEncoder.encode("123456")) .authorities("admin_role") ; } }
Spring Security对OAuth2提供了默认可访问端点,即URL
获取请求授权码Code
- /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),如下图
通过授权码获取令牌token
- 获取到授权码(code) 后,就可以通过它来获取访问令牌(access_token)。
涉及:TokenEndpoint
POST方式请求:http://localhost:8899/oauth/token
- Postman中将 client_id:client_secret 通过 Base64 编码
- post方式,请求体中指定授权方式和授权码
- 每个授权码申请令牌后就会失效,需要重新发送请求获取授权码再去认证,不然就会请求认证失败
认证服务器配置-密码模式
密码模式(resource owner password credentials),用户向客户端提供自己在认证服务器上的用户和密码,然后客户端通过用户提供的用户名和密码向认证服务器获取令牌。
但是如果用户名和密码遗漏,认证服务器无法判断客户端提交的用户和密码是否是盗取的,那意味着令牌就可以随时获取,信息容易泄露。
配置密码模式
- 在安全配置类中spring.oauth2.server.config.OAuth2SecurityConfig,将AuthenticationManager注入到bean
指定密码模式
/** *password密码模式要使用此认证管理器 *@return *@throwsException */ @Bean @Override publicAuthenticationManagerauthenticationManagerBean()throwsException{ returnsuper.authenticationManagerBean(); }
在认证服务器配置类OAuth2AuthorizationServerConfig中
- 覆盖父类的configure(AuthorizationServerEndpointsConfigurer endpoints)方法,用于配置令牌访问端点,把authenticationManager注入并添加
@Autowired privateAuthenticationManagerauthenticationManager; @Override publicvoidconfigure(AuthorizationServerEndpointsConfigurerendpoints)throwsException{ //密码模式需要配置认证管理器 endpoints.authenticationManager(authenticationManager); }
- 针对test-pc客户端添加支持密码模,可以同时支持多个模式,配置如下
获取令牌token
.authorizedGrantTypes("authorization_code","password","implicit","client_credentials","refresh_token")
使用浏览器访问http://localhost:8899/oauth/token
- 重新启动浏览器
- Postman中将 client_id:client_secret 通过 Base64 编码
- post 方式,请求体中指定: 授权方式 、用户名、密码
认证服务器配置- 简化授权模式
不通过第三方应用程序,直接在浏览器中向认证服务器申请令牌,不需要先获取授权码。直接可以一次请求就可得到令牌,在 redirect_uri 指定的回调地址中传递令牌( access_token )。该模式适合直接运行在浏览器上的应用,不用后端支持(例如 Javascript 应用)
简化模式在OAuth2AuthorizationServerConfig类中的configure(ClientDetailsServiceConfigurer clients)方法中指定implicit
获取令牌token
- 打开浏览器,输入访问地址
http://localhost:8899/oauth/authorize?client_id=test-pc&response_type=token
注:此时response_typ的参数值必须是token
- 当请求到达认证服务器的 AuthorizationEndpoint 后,它会要求资源所有者做身份验证 :
- 点击登录以后,会跳转到指定的redirect_uri,回调路径会,回调路径携带着令牌 access_token 、 expires_in 、 scope 等 ,如下图:
认证服务器配置- 客户端授权模式
客户端模式(client credentials)是指客户端以自己名义,而不是用户名义,向认证服务器进行认证,严格说客户端模式并不属于OAuth2框架所解决的问题,在这种模式下,用户直接向客户端注册,客户端以自己的名义向认证服务器提供服务,实际上并不存在授权问题。
客户端向认证服务器进行身份认证,并要求一个访问令牌。
认证服务器确认无误后,向客户端提供访问令牌。
指定客户端模式在OAuth2AuthorizationServerConfig类的configure(ClientDetailsServiceConfigurer clients)方法中指定client_credentials
获取令牌token
- Postman中将 client_id:client_secret 通过 Base64 编码
2.post 方式,请求体中指定授权类型grant_type :client_credentials
注响应结果没有刷新令牌
认证服务器配置-令牌刷新策略
如果用户访问资源的时候,客户端的令牌已经过期,那么就需要更新令牌,申请一个新的访问令牌。
客户端发出更新令牌的Http请求,包含以下参数:
grant_type:表示使用授权模式,此处固定值为refresh_token
refresh_token:表示早前收到的需要更新的令牌
scope:表示申请的授权范围,不可以超出上一次申请的范围。
**注:刷新令牌只有在授权模式和密码模式中才有,对应的指定这两种模式时,在类型上加上refresh_token**。
获取新令牌报错
- Postman中将 client_id:client_secret 通过 Base64 编码
- post 方式,请求体中指定:授权类型、刷新令牌
当前报错: Internal Server Error , 对应idea控制台也发出警告: UserDetailsService is required.
原因当前需要使用内存方式存储了用户令牌,应用使用UserDetailsService才行
解决办法创建UserDetailsService实现
- 创建MyUserDetailsService动态获取用户令牌
@Component publicclassMyUserDetailsServiceimplementsUserDetailsService{ @Autowired privatePasswordEncoderpasswordEncoder; @Override publicUserDetailsloadUserByUsername(Stringusername)throwsUsernameNotFoundException{ returnnewUser("admin",passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin_role")); } }
- 在安全配置类OAuth2SecurityConfig中注入myUserDetailsService
@Autowired privateUserDetailsServicemyUserDetailsService; /** *用户类信息 *@paramauth *@throwsException */ @Override protectedvoidconfigure(AuthenticationManagerBuilderauth)throwsException{ auth.userDetailsService(myUserDetailsService); }
3.认证配置类OAuth2AuthorizationServerConfig的configure(AuthorizationServerEndpointsConfigurer endpoints)方法上加入到令牌端点上
测试获取新令牌
@Override publicvoidconfigure(AuthorizationServerEndpointsConfigurerendpoints)throwsException{ //密码模式需要配置认证管理器 endpoints.authenticationManager(authenticationManager); //刷新令牌获取新令牌时需要 endpoints.userDetailsService(myUserDetailsService); }
- 重启认证服务器
- Postman中将 client_id:client_secret 通过 Base64 编码
- post 方式,请求体中指定:授权类型 refresh_token 、刷新令牌值
认证服务器配置-令牌管理策略Redis & JDBC
默认情况下,令牌是通过randomUUID产生的32为随机数来进行填充,从而产生的令牌默认是存储在内存中。
内存存储采用的是TokenStore接口默认实现类InMemoryTokenStore,开发时方便调试,适用单机版
RedisTokenStore将令牌存储到Redis非关系型数据库,适用于高并发服务
JdbcTokenStore基于JDBC将令牌存储到关系型数据库中,可以在不同的服务器间共享令牌
JWtTokenStore将用户信息存储到令牌中,这样后端就可以不存储,前端拿到令牌后可以直接解析出用户信息。
Redis令牌管理启动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的配置
配置Redis的管理TokenStore
spring: thymeleaf: cache:false application: name:oauth2-server redis: port:6379 host:127.0.0.1 connect-timeout:50000
在MyOAuth2Config类中注入RedisTokenStore
令牌管理策略添加到端点
@Autowired privateRedisConnectionFactoryredisConnectionFactory; /** *Redis令牌管理 *步骤: *1.启动redis *2.添加redis依赖 *3.添加redis依赖后,容器就会有RedisConnectionFactory实例 *@return */ @Bean publicTokenStoreredisTokenStore(){ returnnewRedisTokenStore(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 * 查看效果如下
JDBC管理令牌创建相关数据表
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类型
修改后的表结构如下:
添加JDBC相关依赖
--usedinteststhatuseHSQL createtableoauth_client_details( client_idVARCHAR(128)PRIMARYKEY, resource_idsVARCHAR(256), client_secretVARCHAR(256), scopeVARCHAR(256), authorized_grant_typesVARCHAR(256), web_server_redirect_uriVARCHAR(256), authoritiesVARCHAR(256), access_token_validityINTEGER, refresh_token_validityINTEGER, additional_informationVARCHAR(4096), autoapproveVARCHAR(256) ); INSERTINTO`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'); createtableoauth_client_token( token_idVARCHAR(256), tokenBLOB, authentication_idVARCHAR(256)PRIMARYKEY, user_nameVARCHAR(256), client_idVARCHAR(256) ); createtableoauth_access_token( token_idVARCHAR(256), tokenBLOB, authentication_idVARCHAR(256)PRIMARYKEY, user_nameVARCHAR(256), client_idVARCHAR(256), authenticationBLOB, refresh_tokenVARCHAR(256) ); createtableoauth_refresh_token( token_idVARCHAR(256), tokenBLOB, authenticationBLOB ); createtableoauth_code( codeVARCHAR(256), authenticationBLOB ); createtableoauth_approvals( userIdVARCHAR(256), clientIdVARCHAR(256), scopeVARCHAR(256), statusVARCHAR(10), expiresAtTIMESTAMP, lastModifiedAtTIMESTAMP ); --customizedoauth_client_detailstable createtableClientDetails( appIdVARCHAR(256)PRIMARYKEY, resourceIdsVARCHAR(256), appSecretVARCHAR(256), scopeVARCHAR(256), grantTypesVARCHAR(256), redirectUrlVARCHAR(256), authoritiesVARCHAR(256), access_token_validityINTEGER, refresh_token_validityINTEGER, additionalInformationVARCHAR(4096), autoApproveScopesVARCHAR(256) );
其中有 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中添加数据源
配置JDBC的管理JdbcTokenStore
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:SELECT1FROMDUAL redis: port:6379 host:127.0.0.1 connect-timeout:50000
- 在MyOAuth2Config将DruidDataSource 数据源注入
/** *druid数据源 *@return */ @Bean @ConfigurationProperties(prefix="spring.datasource") publicDataSourcedruidDataSource(){ returnnewDruidDataSource(); }
- 在MyOAuth2Config指定 JDBC 管理 JdbcTokenStore
测试
/** *jdbc管理令牌 *步骤: *1.创建相关表 *2.添加jdbc相关依赖 *3.配置数据源信息 *@return */ @Bean publicTokenStorejdbcTokenStore(){ returnnewJdbcTokenStore(druidDataSource()); }
- 重启认证服务器
- 使用密码模式进行授权操作,然后查询 oauth_access_token 表就存储了令牌信息
认证服务器配置-JDBC管理授权码
授权码主要是操作oauth_code表,只有当grant_type是authorization_code(授权码模式)时,该表中才会有数据产生,其他模式下oauth_code表不会生成数据。
授权码切换成JDBC
- 在MyOAuth2Config类中注入AuthorizationCodeServices
/** *授权码管理策略 *@return */ @Bean publicAuthorizationCodeServicesjdbcAuthorizationCodeServices(){ //使用JDBC方式保存授权码到oauth_code中 returnnewJdbcAuthorizationCodeServices(druidDataSource()); }
- 在认证服务器配置类OAuth2AuthorizationServerConfig中的configure(AuthorizationServerEndpointsConfigurer endpoints)方法中将授权码添加到端点上
测试
- 重启项目
- 发送请求获取授权码
http://localhost:8899/oauth/authorize?client_id=test-pc&response_type=code
- 查看 oauth_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对客户端密码进行加密
- 在MyOAuth2Config类中注入ClientDetailsService
/** *使用JDBC方式管理客户端信息 *@return */ @Bean publicClientDetailsServicejdbcClientDetailsService(){ returnnewJdbcClientDetailsService(druidDataSource()); }
- 在认证服务器配置类OAuth2AuthorizationServerConfig的confifigure(ClientDetailsServiceConfifigurer) 切换成JDBC 方式管理客户端信息
测试
@Autowired privateClientDetailsServicejdbcClientDetailsService; /** *配置被允许访问此认证服务器的客户端详细信息 *1.内存管理 *2.数据库管理方式 *@paramclients *@throwsException */ @Override publicvoidconfigure(ClientDetailsServiceConfigurerclients)throwsException{ 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 publicvoidconfigure(AuthorizationServerSecurityConfigurersecurity)throwsException{ //所有人可访问/oauth/token_key后面要获取公钥,默认拒绝访问 security.tokenKeyAccess("permitAll()"); //认证后可访问/oauth/check_token,默认拒绝访问 security.checkTokenAccess("isAuthenticated()"); }
- 重启认证服务器
- 检查令牌对应的用户信息
实现资源服务器
实现资源服务器的有两种方式:
认证服务器和资源服务器定义在一个SpringBoot中
- 配置资源服务器,对任何“/api/**”接口的访问,都必须经过OAuth2认证服务器认证。
@Configuration @EnableResourceServer publicclassOAuth2ResourceServerextendsResourceServerConfigurerAdapter{ @Override publicvoidconfigure(HttpSecurityhttp)throwsException{ http.authorizeRequests() .anyRequest().authenticated() .and() .requestMatchers() .antMatchers("/api/**"); } }
- 随便写一个业务Api接口,代表该应用对外提供的服务资源:
@RestController @RequestMapping("/api") publicclassHelloController{ @RequestMapping("/hello/{name}") publicStringhello(@PathVariable("name")Stringname){ return"HelloOauth2:" name; } }
- 使用AccessToken访问资源
认证资源服务器分离
创建spring-oauth2-resourceapi模块
添加依pom.xml
创建测试API
<dependencies> <!--forOAuth2.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") publicclassHelloController{ @RequestMapping("/hello/{name}") publicStringhello(@PathVariable("name")Stringname){ return"Hellospring-oauth2-resourceapi:" name; } }
- 创建资源服务器配置类OAuth2ResourceServer类继承ResourceServerConfigurerAdapter
- 在类上添加注解
- @Configuration
- @EnableResourceServer :标识资源服务器,所有发往当前服务的请求,都会去请求头里找token,找不到或 验证不通过不允许访问
- @EnableGlobalMethodSecurity(prePostEnabled = true):开启方法级权限控制
- 重写资源服务器相关配置方法configure(ResourceServerSecurityConfigurer resources)
- 配置当前资源服务器ID
- 添加校验令牌服务
- 创建 RemoteTokenServices 远程校验令牌服务,去校验令牌有效性,因为当前认证和资源服务器不是在同一工程中,所以要通过远程调用认证服务器校验令牌是否有效
- 如果认证和资源服务器在同一工程中,可以使用 DefaultTokenServices 配置校验令牌。
测试
@Configuration @EnableResourceServer//标识为资源服务器,所有发往当前服务的请求,都会去请求头里找token,找不到或验证不通过不允许访问 @EnableGlobalMethodSecurity(prePostEnabled=true) publicclassOAuth2ResourceServerextendsResourceServerConfigurerAdapter{ @Resource privateDataSourcedataSource; @Bean publicTokenStoretokenStore(){ returnnewJdbcTokenStore(dataSource); } @Override publicvoidconfigure(HttpSecurityhttp)throwsException{ http.authorizeRequests() .anyRequest().authenticated() .and() .requestMatchers() .antMatchers("/test/**"); } @Override publicvoidconfigure(ResourceServerSecurityConfigurerresources)throwsException{ resources.resourceId("oauth2-resource") .tokenServices(tokenServices()); } /** *配置资源服务器如何校验token *1.DefaultTokenServices *如果认证服务器和资源服务器在同一个服务,则直接采用默认服务验证 *2.RemoteTokenServices *当认证服务器和资源服务器不在同一个服务,要使用此服务器去远程认证服务器验证 *@return */ @Primary @Bean publicRemoteTokenServicestokenServices(){ //资源服务器去远程认证服务器验证token是否有效 finalRemoteTokenServicestokenService=newRemoteTokenServices(); //请求认证服务器验证URL,注意:默认这个端点是拒绝访问的,要设置认证后可访问 tokenService.setCheckTokenEndpointUrl("http://localhost:8899/oauth/check_token"); //在认证服务器配置的客户端id tokenService.setClientId("test-pc"); //在认证服务器配置的客户端密码 tokenService.setClientSecret("123456"); returntokenService; } }
- 启动认证服务器和资源服务器
- 通过密码方式获取令牌
- 请求头带上令牌请求/test/hello 资源
如果您觉得本文不错,欢迎关注,点赞,收藏支持,您的关注是我坚持的动力!
原创不易,转载请注明出处,感谢支持!如果本文对您有用,欢迎转发分享!
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com