目录
OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方应用访问存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth在全世界得到广泛应用,目前的版本是2.0版。
OAuth协议:https://tools.ietf.org/html/rfc6749
OAuth 基本概念
OAuth的作用就是让"客户端"安全可控地获取"用户"的授权,与"服务商提供商"进行交互。
以微信开放平台为例:准备工作 | 微信开放文档
基本设计思想:OAuth 引入了一个授权层,用来分离两种不同的角色:客户端和资源所有者。资源所有者同意以后,资源服务器可以向客户端颁发令牌。客户端通过令牌,去请求数据。
授权流程图示:

步骤明细:
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0 对于如何颁发令牌有四种方式:
不论哪一种授权方式,第三方应用申请令牌之前,都必须先到授权系统备案,注册自己的身份,然后会获取两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。
授权码(authorization code)方式,指第三方应用向授权服务方先申请授权码,然后用授权码获取令牌。授权码模式是最常用的授权流程,安全性高,适用于有后端的 Web 应用。授权码虽然通过前端传送,但令牌储存在后端,并且所有与资源服务器的通信都由后端完成,对前端屏蔽,从而可以避免令牌泄漏。
步骤总结:
- // 1-获取授权码
- http://localhost:8080/oauth/authorize?response_type=code&client_id=clientA&redirect_uri=http://www.baidu.com&scope=all
-
- // 2-用户登陆后,返回授权码
- https://www.baidu.com/?code=ru2Ye2
-
- // 3-通过授权码获取token
请求服务方的提供授权码——>用户授权——>服务方返回授权码——>通过授权码获取 access_token ——> 通过 access_token 访问资源
首先需要引入security 和 oauth2 的相关依赖,spring boot 依赖引入示例:
- <!-- Spring Security 配置 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-security</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.security.oauth</groupId>
- <artifactId>spring-security-oauth2</artifactId>
- <version>2.3.4.RELEASE</version>
- </dependency>
如果是微服务,对应的 spring cloud 依赖引入示例(security 也要引入):
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-oauth2</artifactId>
- </dependency>
WebSecurityConfigurerAdapter 是自定义的 spring security 配置文件,在 spring security 基本功能实现的基础上,配置请求拦截规则,要求对所有的 /oauth/** 接口放行
- import com.swadian.userdemo.service.MyUserDetailsService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.Lazy;
- import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
- import org.springframework.security.config.annotation.web.builders.HttpSecurity;
- import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
-
- @Configuration // 标记为注解类
- public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
- @Autowired
- @Lazy // 解决循环依赖
- private MyUserDetailsService userService;
-
- @Bean // 编码
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
-
- @Override // 注入用户信息
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- //设置UserDetailsService的实现类
- auth.userDetailsService(userService);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.formLogin().permitAll() // 表单登陆
- .and()
- .authorizeRequests()
- .antMatchers("/oauth/**").permitAll() // 授权oauth下的所有接口
- .anyRequest().authenticated()
- .and().logout().permitAll() // 登出
- .and().csrf().disable();
- }
- }
配置用户信息是权限系统的基本要求,需要根据用户信息进行身份验证,授权校验
- 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.password.PasswordEncoder;
- import org.springframework.stereotype.Service;
-
- @Service
- public class MyUserDetailsService implements UserDetailsService {
-
- @Autowired
- private PasswordEncoder passwordEncoder;
-
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- String password = passwordEncoder.encode("123456"); // 密码
- return new User("admin",
- password,
- AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
- }
- }
模拟需要授权的接口,一旦接口请求成功,会返回一串字符串,示例如下:
- @RestController
- @RequestMapping("/admin")
- public class AdminController {
- @GetMapping("/test")
- public String test() {
- return "Spring Security Test";
- }
- }
授权服务器具有以下核心端点

添加授权服务器配置,@EnableAuthorizationServer 开启授权服务器,注册可用的客户端,并在授权模式中添加 authorization_code ,表示授权码模式
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Configuration;
- 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;
-
- @Configuration
- @EnableAuthorizationServer
- public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
- @Autowired
- private PasswordEncoder passwordEncoder;
-
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- clients.inMemory()
- .withClient("clientA") //配置客户端id client_id
- .secret(passwordEncoder.encode("123456")) //配置client-secret
- .accessTokenValiditySeconds(3600) //配置访问token的有效期
- .refreshTokenValiditySeconds(864000) //配置刷新token的有效期
- .redirectUris("http://www.baidu.com") //配置redirect_uri,授权成功后跳转
- .scopes("all") //配置申请的权限范围
- .authorizedGrantTypes("authorization_code");//配置grant_type,表示授权类型
- }
- }
配置资源服务器,明确进行授权的资源
- 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;
-
- @Configuration
- @EnableResourceServer
- public class ResourceServiceConfig extends ResourceServerConfigurerAdapter {
- @Override
- public void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests()
- .anyRequest().authenticated()
- .and()
- .requestMatchers().antMatchers("/admin/**"); // 需要验权资源
- }
- }
测试流程
启动 SpringBoot 项目,在浏览器端请求授权服务器进行授权,输入以下访问链接
http://localhost:8080/oauth/authorize?response_type=code&client_id=clientA&redirect_uri=http://www.baidu.com&scope=all
授权服务器需要进行登陆,输入用户名和密码进行登陆

用户登陆成功后,跳转到授权页面

点击 Approve 进行授权确认,确认后会携带一个code,跳转到重定向地址

接下来使用code(授权码),获取登陆令牌,使用 postman 进行模拟请求,授权服务器获取令牌的接口为
http://127.0.0.1:8080/oauth/token

参数 grant_type:authorization_code,redirect_uri:百度(随便填的,必填),code:上步骤返回的code

请求成功后,会返回 json 格式的数据,其中就有我们需要的令牌

- {
- "access_token": "3c26e355-a011-4b0f-a6f6-91a724fd0153",
- "token_type": "bearer",
- "expires_in": 3599,
- "scope": "all"
- }
如果认证失败,服务端会返回 401 Unauthorized
接下来,使用令牌(access_token)去资源服务器访问授权资源,资源能够正常访问

或者,直接在资源后携带 access_token

或者,在请求头中,添加 Authorization 授权信息,注意要带上 token 类型 bearer

至此,spring security OAuth2 的授权码模式完成
简化(隐式)模式,允许直接向前端颁发令牌,这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。该模式直接在浏览器中向授权服务器申请令牌,没有"授权码"步骤,所有步骤都在浏览器中完成,同时令牌对访问者也是可见的。
简化模式直接把令牌传给前端,该方式很不安全。因此,只能用于一些安全性要求不高的场景,并且令牌设置的有效期必须非常短,通常只是会话期间(session)有效,一旦浏览器关掉,令牌就就失效了。
使用场景:纯前端应用
步骤总结:
请求服务提供方的授权 ——>直接获取 access_token ——> 携带 access_token 访问资源
代码示例:
在上文代码的基础上,修改授权服务器的配置,在 authorizedGrantTypes 属性中添加 "implicit", //简化模式,完整的代码如下:
- // 完整的代码如下
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- clients.inMemory()
- .withClient("clientA") //配置client_id
- .secret(passwordEncoder.encode("123456")) //配置client-secret
- .accessTokenValiditySeconds(3600) //配置访问token的有效期
- .refreshTokenValiditySeconds(864000) //配置刷新token的有效期
- .redirectUris("http://www.baidu.com") //配置redirect_uri,用于授权成功后跳转
- .scopes("all") //配置申请的权限范围
- .authorizedGrantTypes(
- //"authorization_code", //授权码模式,配置grant_type,表示授权类型
- "implicit", //简化模式
- //"password", //密码模式
- //"client_credentials",//客户端模式
- "refresh_token"); //更新令牌
- }
启动服务,访问以下链接
http://localhost:8080/oauth/authorize?response_type=token&client_id=clientA&redirect_uri=http://www.baidu.com&scope=all
将会直接跳转到重定向页面,同时,access_token 会在跳转的地址栏中返回,中间不需要经过任何环节。

如果对某个应用高度信任,用户直接使用用户名和密码申请令牌,这种方式称为"密码式"(password)。
在这种模式中,用户必须把密码给客户端,但是客户端不得储存密码。通常在用户对客户端高度信任的场景下使用,比如客户端是操作系统的一部分。一般来说授权服务器只有在其他授权模式无法执行的情况下,才考虑使用这种模式。
适用场景:公司自己搭建的授权服务器
代码示例:
在上文代码的基础上,修改 spring security 配置,在配置中增加 authenticationManagerBean
- import com.swadian.userdemo.service.MyUserDetailsService;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.Lazy;
- import org.springframework.security.authentication.AuthenticationManager;
- import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
- import org.springframework.security.config.annotation.web.builders.HttpSecurity;
- import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.security.crypto.password.PasswordEncoder;
-
- @Configuration // 标记为注解类
- public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
-
- @Autowired
- @Lazy // 解决循环依赖
- private MyUserDetailsService userService;
-
- @Bean
- public PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
-
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
- //设置UserDetailsService的实现类
- auth.userDetailsService(userService);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.formLogin().permitAll()
- .and()
- .authorizeRequests().antMatchers("/oauth/**").permitAll()
- .anyRequest().authenticated()
- .and().logout().permitAll()
- .and().csrf().disable();
- }
-
- @Bean
- @Override
- public AuthenticationManager authenticationManagerBean() throws Exception {
- return super.authenticationManagerBean();
- }
- }
然后修改授权服务器的配置,在 endpoints 配之中,添加允许使用 GET 和 POST 请求,并且允许进行表单验证,完整代码如下:
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.http.HttpMethod;
- import org.springframework.security.authentication.AuthenticationManager;
- 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;
-
- @Configuration
- @EnableAuthorizationServer
- public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
- @Autowired
- private PasswordEncoder passwordEncoder;
-
- @Autowired
- private AuthenticationManager authenticationManagerBean;
-
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
- .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //支持 GET,POST请求
- }
-
- @Override
- public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
- //允许表单认证
- security.allowFormAuthenticationForClients();
- }
-
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- clients.inMemory()
- .withClient("clientA") //配置client_id
- .secret(passwordEncoder.encode("123456")) //配置client-secret
- .accessTokenValiditySeconds(3600) //配置访问token的有效期
- .refreshTokenValiditySeconds(864000) //配置刷新token的有效期
- .redirectUris("http://www.baidu.com") //配置redirect_uri,用于授权成功后跳转
- .scopes("all") //配置申请的权限范围
- .authorizedGrantTypes(//"authorization_code", //授权码模式 //配置grant_type,表示授权类型
- "password", //密码模式
- //"client_credentials",//客户端模式
- "refresh_token"); //更新令牌
- }
- }
接下来获取令牌
测试方法一:通过浏览器访问以下地址 // 需要配置支持get请求和表单验证
http://localhost:8080/oauth/token?username=admin&password=123456&grant_type=password&client_id=clientA&client_secret=123456&scope=all
访问结果如下:// 直接获取到 access_token

测试方法二:通过 postman 进行测试


尝试访问授权资源

客户端模式(Client Credentials Grant)指客户端以服务器身份,向服务提供方申请授权。适用于没有前端的命令行应用,一般提供给受信任的服务器端使用。
在上文代码的基础上,配置 grant_type 为 client_credentials,然后直接在浏览器中输入以下链接就进行验证
http://localhost:8080/oauth/token?grant_type=client_credentials&client_id=clientA&scope=all&client_secret=123456
验证结果如下:

使用 oauth2 时,如果令牌失效了,可以通过 refresh_token 的授权模式再次获取access_token,从而刷新令牌,刷新令牌可避免繁琐的重复认证。
在上边代码的基础上修改授权服务器配置,在 authorizedGrantTypes 中添加 refresh_token 类型,同时可以在 endpoints 配置中,配置 refresh_token 是否可重复使用
- @Configuration
- @EnableAuthorizationServer
- public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
- @Autowired
- private PasswordEncoder passwordEncoder;
-
- @Autowired
- private MyUserDetailsService userService;
-
- @Autowired
- private AuthenticationManager authenticationManagerBean;
-
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
- .reuseRefreshTokens(false) //refresh_token是否重复使用
- .userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
- .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //支持 GET,POST请求
- }
-
- @Override
- public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
- //允许表单认证
- security.allowFormAuthenticationForClients();
- }
-
- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- clients.inMemory()
- .withClient("clientA") //配置client_id
- .secret(passwordEncoder.encode("123456")) //配置client-secret
- .accessTokenValiditySeconds(3600) //配置访问token的有效期
- .refreshTokenValiditySeconds(864000) //配置刷新token的有效期
- .redirectUris("http://www.baidu.com") //配置redirect_uri,用于授权成功后跳转
- .scopes("all") //配置申请的权限范围
- .authorizedGrantTypes(//"authorization_code", //授权码模式 //配置grant_type,表示授权类型
- "password", //密码模式
- //"client_credentials",//客户端模式
- "refresh_token"); //更新令牌
- }
- }
通过密码模式进行测试,在浏览器端访问以下链接
- http://localhost:8080/oauth/token
- ?username=admin
- &password=123456
- &grant_type=password
- &client_id=clientA
- &client_secret=123456
- &scope=all

在返回报文中,有一个 refresh_token 的字段,如果需要刷新令牌,可以使用 refresh_token 访问如下链接:
http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=clientA&client_secret=123456&refresh_token=73ca27c6-69b9-4788-ab8a-edee28d0ec93

需要引入 redis 相关的依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
修改配置文件 application.yaml ,添加 redis 配置
- spring:
- redis:
- host: localhost
- port: 6379
- database: 0
编写 redis 配置类,返回 RedisTokenStore 的存储实例
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.security.oauth2.provider.token.TokenStore;
- import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
-
- @Configuration
- public class RedisConfig {
-
- @Autowired
- private RedisConnectionFactory redisConnectionFactory;
-
- @Bean
- public TokenStore tokenStore() {
- return new RedisTokenStore(redisConnectionFactory);
- }
- }
在授权服务器配置中指定令牌的存储策略为 Redis
- @Configuration
- @EnableAuthorizationServer
- public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
- @Autowired
- private PasswordEncoder passwordEncoder;
-
- @Autowired
- private TokenStore tokenStore;
-
- @Autowired
- private MyUserDetailsService userService;
-
- @Autowired
- private AuthenticationManager authenticationManagerBean;
-
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
- .tokenStore(tokenStore) // 指定token存储到redis
- .reuseRefreshTokens(false) //refresh_token是否重复使用
- .userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
- .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //支持 GET,POST请求
- }
- }
接下来,使用密码模式进行测试,访问如下链接

查看下 redis 是否存储了 token

至此,redis 存储 Token 验证成功