一、前言 OAuth 是一种用来规范令牌(Token)发放的授权机制,主要包含了四种授权模式:授权码模式、简化模式、密码模式和客户端模式。Spring Security OAuth2对这四种授权模式进行了实现。这节主要记录下什么是OAuth2以及Spring Security OAuth2的基本使用。
二、四种授权模式 在了解这四种授权模式之前,我们需要先学习一些和OAuth相关的名词。举个社交登录的例子吧,比如在浏览器上使用QQ账号登录虎牙直播,这个过程可以提取出以下几个名词:Third-party application 第三方应用程序,比如这里的虎牙直播;HTTP service HTTP服务提供商,比如这里的QQ(腾讯);Resource Owner 资源所有者,就是QQ的所有人,你;User Agent 用户代理,这里指浏览器;Authorization server 认证服务器,这里指QQ提供的第三方登录服务;Resource server 资源服务器,这里指虎牙直播提供的服务,比如高清直播,弹幕发送等(需要认证后才能使用)。
认证服务器和资源服务器可以在同一台服务器上,比如前后端分离的服务后台,它即供认证服务(认证服务器,提供令牌),客户端通过令牌来从后台获取服务(资源服务器);它们也可以不在同一台服务器上,比如上面第三方登录的例子。
大致了解了这几个名词后,我们开始了解四种授权模式。
4.1 授权码模式 授权码模式是最能体现OAuth2协议,最严格,流程最完整的授权模式,流程如下所示:
A. 客户端将用户导向认证服务器; B. 用户决定是否给客户端授权; C. 同意授权后,认证服务器将用户导向客户端提供的URL,并附上授权码; D. 客户端通过重定向URL和授权码到认证服务器换取令牌; E. 校验无误后发放令牌。
其中A步骤,客户端申请认证的URI,包含以下参数:
response_type:表示授权类型,必选项,此处的值固定为”code”,标识授权码模式 client_id:表示客户端的ID,必选项 redirect_uri:表示重定向URI,可选项 scope:表示申请的权限范围,可选项 state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。 D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
grant_type:表示使用的授权模式,必选项,此处的值固定为”authorization_code”。 code:表示上一步获得的授权码,必选项。 redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。 client_id:表示客户端ID,必选项。 4.2 密码模式 在密码模式中,用户像客户端提供用户名和密码,客户端通过用户名和密码到认证服务器获取令牌。流程如下所示:
A. 用户向客户端提供用户名和密码; B. 客户端向认证服务器换取令牌; C. 发放令牌。
B步骤中,客户端发出的HTTP请求,包含以下参数:
grant_type:表示授权类型,此处的值固定为”password”,必选项。 username:表示用户名,必选项。 password:表示用户的密码,必选项。 scope:表示权限范围,可选项。 剩下两种授权模式可以参考下面的参考链接,这里就不介绍了。
三、Spring Security OAuth2 Spring框架对OAuth2协议进行了实现,下面学习下上面两种模式在Spring Security OAuth2相关框架的使用。
Spring Security OAuth2主要包含认证服务器和资源服务器这两大块的实现:
认证服务器主要包含了四种授权模式的实现和Token的生成与存储,我们也可以在认证服务器中自定义获取Token的方式(后面会介绍到);资源服务器主要是在Spring Security的过滤器链上加了OAuth2AuthenticationProcessingFilter过滤器,即使用OAuth2协议发放令牌认证的方式来保护我们的资源。
3.1 配置认证服务器 新建一个Spring Boot项目,版本为2.1.6.RELEASE,并引入相关依赖,pom如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.wno704</groupId> <artifactId>security</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Security-OAuth2-Guide</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR8</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
3.2 定义User对象 在创建认证服务器前,我们先定义一个MyUser对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Getter @Setter public class MyUser implements Serializable { private static final long serialVersionUID = 3497935890426858541L; private String userName; private String password; private boolean accountNonExpired = true; private boolean accountNonLocked= true; private boolean credentialsNonExpired= true; private boolean enabled= true; }
3.3 实现UserDetailService 接着定义UserDetailService实现org.springframework.security.core.userdetails.UserDetailsService接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Service public class UserDetailService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { MyUser user = new MyUser(); user.setUserName(username); user.setPassword(this.passwordEncoder.encode("123456")); return new User(username, user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin")); } }
这里的逻辑是用什么账号登录都可以,但是密码必须为123456,并且拥有”admin”权限(这些都在前面的Security教程里说过了,就不再详细说明了)。
接下来开始创建一个认证服务器,并且在里面定义UserDetailService需要用到的PasswordEncoder。
3.4 创建认证服务器 创建认证服务器很简单,只需要在Spring Security的配置类上使用@EnableAuthorizationServer注解标注即可。创建AuthorizationServerConfig,代码如下所示:
1 2 3 4 5 6 7 8 9 10 @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
这时候启动项目,会发现控制台打印出了随机分配的client-id和client-secret:
为了方便后面的测试,我们可以手动指定这两个值。在Spring Boot配置文件application.yml中添加如下配置:
1 2 3 4 5 security: oauth2: client: client-id: test client-secret: test123
重启项目,发现控制台输出:
说明替换成功。
3.5 授权码模式获取令牌 接下来开始往认证服务器请求授权码。打开浏览器,访问 http://localhost:8080/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://wno704.com:10008&scope=all&state=hello
URL中的几个参数在上面的授权码模式的A步骤里都有详细说明。这里response_type必须为code,表示授权码模式,client_id就是刚刚在配置文件中手动指定的test,redirect_uri这里随便指定一个地址即可,主要是用来重定向获取授权码的,scope指定为all,表示所有权限。
访问这个链接后,页面如下所示:
需要登录认证,根据我们前面定义的UserDetailService逻辑,这里用户名随便输,密码为123456即可。输入后,页面跳转如下所示:
原因是上面指定的redirect_uri必须同时在配置文件中指定,我们往application.yml添加配置:
1 2 3 4 5 6 security: oauth2: client: client-id: test client-secret: test123 registered-redirect-uri: http://wno704.com:10008
重启项目,重新执行上面的步骤,登录成功后页面成功跳转到了授权页面:
选择同意Approve,然后点击Authorize按钮后,页面跳转到了我们指定的redirect_uri,并且带上了授权码信息:
到这里我们就可以用这个授权码从认证服务器获取令牌Token了。