带领读者对Spring Security OAuth2框架的授权码模式有一个比较直观的概念,能使用框架搭建授权码模式授权服务器与资源服务器(分离版本)
授权码模式要求:用户登录并对第三方应用(客户端)进行授权,出示授权码交给客户端,客户端凭授权码换取access_token(访问凭证)
此模式要求授权服务器与用户直接交互,在此过程中,第三方应用是无法获取到用户输入的密码等信息的,这个模式也是OAuth 2.0中最安全的一个
这里主要关注authorization-code-authorization-server
与authorization-code-resource-server
这两个模块
本文以及后续文章的demo均放在GitHub上,欢迎大家Star & Fork,源码地址:GitHub - hellxz/spring-security-oauth2-learn: 基于Spring Security OAuth2 5.0以下版本的写法的学习代码
authorization-code-client-resttemplate-jdbc
这个项目是用来测试非OAuth2服务使用RestTemplate与JdbcTemplate对接OAuth2授权服务的,流程这里不讲,有兴趣可以debug看看,可能会让您对整个流程会有更清晰的感受
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
文中服务器均使用demo级别配置,请勿直接使用到生产环境
授权服务器结构主体如下:
启动类自不多说,先说下SecurityConfig
package com.github.hellxz.oauth2.config; |
|
import org.springframework.context.annotation.Bean; |
|
import org.springframework.context.annotation.Configuration; |
|
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.EnableWebSecurity; |
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; |
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
|
import org.springframework.security.crypto.password.PasswordEncoder; |
|
import java.util.Collections; |
|
@Configuration |
|
@EnableWebSecurity |
|
public class SecurityConfig extends WebSecurityConfigurerAdapter { |
|
@Bean |
|
public PasswordEncoder passwordEncoder(){ |
|
return new BCryptPasswordEncoder(); |
|
} |
|
@Override |
|
protected void configure(AuthenticationManagerBuilder auth) throws Exception { |
|
// @formatter: off |
|
auth.inMemoryAuthentication() |
|
.withUser("hellxz") |
|
.password(passwordEncoder().encode("xyz")) |
|
.authorities(Collections.emptyList()); |
|
// @formatter: on |
|
} |
|
@Override |
|
protected void configure(HttpSecurity http) throws Exception { |
|
http.authorizeRequests() |
|
.anyRequest().authenticated() //所有请求都需要通过认证 |
|
.and() |
|
.httpBasic() //Basic登录 |
|
.and() |
|
.csrf().disable(); //关跨域保护 |
|
} |
|
} |
|
通过@Configuration
和@EnableWebSecurity
开启Spring Security配置,继承WebSecurityConfigurerAdapter
的方法,实现个性化配置,这里我们使用内存保存一个名为hellxz
、密码为xyz
的用户,与授权服务器交互的用户就是他了
除了配置用户,我们需要对服务的资源进行保护,这里将所有的请求都要求通过认证才可以访问,用户登录需要使用httpBasic形式(就是那种网页弹个窗要求登录的那种)
Spring Security 5.x版本后,要求显示声明使用的密码器,就是PasswordEncoder
了,常用BCryptPasswordEncoder
,简单的可以认为它是使用时间戳和盐进行加密的一种算法,同一个密码被加密后也不会相同
接着看看授权服务器的配置
,画重点
package com.github.hellxz.oauth2.config; |
|
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; |
|
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; |
|
//授权服务器配置 |
|
@Configuration |
|
@EnableAuthorizationServer //开启授权服务 |
|
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter { |
|
@Autowired |
|
private PasswordEncoder passwordEncoder; |
|
@Override |
|
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { |
|
//允许表单提交 |
|
security.allowFormAuthenticationForClients() |
|
.checkTokenAccess("isAuthenticated()"); |
|
} |
|
@Override |
|
public void configure(ClientDetailsServiceConfigurer clients) throws Exception { |
|
// @formatter: off |
|
clients.inMemory() |
|
.withClient("client-a") //client端唯一标识 |
|
.secret(passwordEncoder.encode("client-a-secret")) //客户端的密码,这里的密码应该是加密后的 |
|
.authorizedGrantTypes("authorization_code") //授权模式标识 |
|
.scopes("read_user_info") //作用域 |
|
.resourceIds("resource1") //资源id |
|
.redirectUris("http://localhost:9001/callback"); //回调地址 |
|
// @formatter: on |
|
} |
|
} |
|
1.通过@Configuration
和EnableAuthorizationServer
开启授权服务器配置,通过重写AuthorizationServerConfigurerAdapter
的方法来完成自定义授权服务器
2.OAuth2授权码模式中,要求不仅仅用户需要登录,还要求客户端也需要登录,这里就需要在configure(ClientDetailsServiceConfigurer clients)
这个方法中配置客户端(第三方应用)的登录信息,
withClient
中配置的是客户端id(client_id)"authorization_code"
(授权码模式),这里是可以同时支持多种授权模式的,为了简单只写一个redirectUris校验是否同一个客户端这个,可能说的不是很准确,说下大体流程,我们在授权服务器上配置了这个回调地址,授权服务器在用户授权成功后,返回授权码的地址就是它,另外我们后续申请token时,也需要传递这个回调地址,所以我的理解是校验是否是同一客户端发来的第二次请求(换token时)
3.configure(AuthorizationServerSecurityConfigurer security)
这里配置资源客户端(第三方应用)的表单提交权限,类似Spring Security配置的permitAll()
等权限控制标识,如果不配置,客户端将无法换取token
4.application.properties
这里我只配置了server.port=8080
这样我们就配置了相当简易的授权服务器,启动测试下
获取授权码的流程,一般是由客户端使用自己的client_id与密码+response_type=code拼接url,让浏览器跳转完成的,用户的登录与授权过程都需要在浏览器中完成,启动项目后访问下列url
http://localhost:8080/oauth/authorize?client_id=client-a&client_secret=client-a-secret&response_type=code
登录用户/密码: hellxz/xyz ,选择Approve表示接受授权,Deny反之,如下动图所示
最后我们得到了回调地址http://localhost:9001/callback?code=2e6450
这里的code就是授权码,接下来我们使用授权码进行换取token
POST请求,http://localhost:8080/oauth/token,参数如图
BasicAuth:这里填的是客户端配置的client_id和client_secret的值,相当于curl --user client_id:client_secret
,配置后会在Header中添加Authorization:Basic Y2xpZW50LWE6Y2xpZW50LWEtc2VjcmV0
,Basic空格
后的是client_id:client_secret
具体值被Base64后得到的值
请求参数列表:
最后我们获得了授权服务的响应,包含token的json
{ |
|
"access_token": "99435e13-f9fe-438a-a94e-3b00d549b329", //访问token |
|
"token_type": "bearer", //token类型,使用时需要拼接在token前并在token前加空格 |
|
"expires_in": 43199, //过期时间 |
|
"scope": "read_user_info" //作用域 |
|
} |
在access_token未过期之前,同一个用户名使用同一个客户端访问都会是同一个access_token
授权服务器先放在这里,不要关服,接下来搭建资源服务器
资源服务器结构
入口类不多说,先搭建资源服务器主要配置,这里直接使用ResourceConfig
进行配置
package com.github.hellxz.oauth2.config; |
|
import org.springframework.context.annotation.Bean; |
|
import org.springframework.context.annotation.Configuration; |
|
import org.springframework.context.annotation.Primary; |
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
|
import org.springframework.security.config.http.SessionCreationPolicy; |
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
|
import org.springframework.security.crypto.password.PasswordEncoder; |
|
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; |
|
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; |
|
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; |
|
import org.springframework.security.oauth2.provider.token.RemoteTokenServices; |
|
@Configuration |
|
@EnableResourceServer |
|
public class ResourceConfig extends ResourceServerConfigurerAdapter { |
|
@Bean |
|
public PasswordEncoder passwordEncoder() { |
|
return new BCryptPasswordEncoder(); |
|
} |
|
@Primary |
|
@Bean |
|
public RemoteTokenServices remoteTokenServices() { |
|
final RemoteTokenServices tokenServices = new RemoteTokenServices(); |
|
//设置授权服务器check_token端点完整地址 |
|
tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token"); |
|
//设置客户端id与secret,注意:client_secret值不能使用passwordEncoder加密! |
|
tokenServices.setClientId("client-a"); |
|
tokenServices.setClientSecret("client-a-secret"); |
|
return tokenServices; |
|
} |
|
@Override |
|
public void configure(HttpSecurity http) throws Exception { |
|
//设置创建session策略 |
|
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); |
|
//@formatter:off |
|
//所有请求必须授权 |
|
http.authorizeRequests() |
|
.anyRequest().authenticated(); |
|
//@formatter:on |
|
} |
|
@Override |
|
public void configure(ResourceServerSecurityConfigurer resources) { |
|
resources.resourceId("resource1").stateless(true); |
|
} |
|
} |
|
1.通过@Configuration
和@EnableResourceServer
这两个注解标识服务是一个资源服务器,重写ResourceServerConfigurerAdapter
来实现自定义授权服务器
2.配置configure(HttpSecurity http)
方法,这里可以代替Spring Security同名方法配置,开启所有请求需要授权才可访问
3.配置资源相关设置configure(ResourceServerSecurityConfigurer resources)
,这里只设置resourceId
后续的使用redis校验token也在这里设置
4.校验token的配置,这里使用了远程调用授权服务器帮忙校验token的方式,只需要显示注入RemoteTokenServices remoteTokenServices()
的Bean,就可以调用授权服务器的/oauth/check_token端点,设置客户端配置的值,详见注释
这样一来我们就配置好了资源服务器,当然光有配置是不够的,我们搞一个资源接口做测试用
上边的ResourceController
与UserVO
都比较简单,传入一个名称,返回用户对象,包含用户名和邮箱信息
package com.github.hellxz.oauth2.web.controller; |
|
import com.github.hellxz.oauth2.web.vo.UserVO; |
|
import org.springframework.web.bind.annotation.GetMapping; |
|
import org.springframework.web.bind.annotation.PathVariable; |
|
import org.springframework.web.bind.annotation.RestController; |
|
@RestController |
|
public class ResourceController { |
|
@GetMapping("/user/{username}") |
|
public UserVO user(@PathVariable String username){ |
|
return new UserVO(username, username + "@foxmail.com"); |
|
} |
|
} |
|
package com.github.hellxz.oauth2.web.vo; |
|
public class UserVO { |
|
private String username; |
|
private String email; |
|
public UserVO(String username, String email) { |
|
this.username = username; |
|
this.email = email; |
|
} |
|
public String getUsername() { |
|
return username; |
|
} |
|
public void setUsername(String username) { |
|
this.username = username; |
|
} |
|
public String getEmail() { |
|
return email; |
|
} |
|
public void setEmail(String email) { |
|
this.email = email; |
|
} |
|
} |
|
application.properties中配置了与授权服务器不同的端口:8081
server.port=8081
启动资源服务测试
什么也不传,直接访问接口,提示资源需要授权
复制之前获取到的token,添加token访问接口http://localhost:8081/user/hellxz001
Bearer Token相当于在Headers中添加Authorization:Bearer空格access_token
至此我们成功的搭建并测试了授权码模式下的最简单的授权服务与资源服务分离的demo