Spring Security OAuth2.0
OAuth2 介绍
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
在认证和授权的过程中涉及的三方包括: 1、服务提供方,用户使用服务提供方来存储受保护的资源,如照片,视频,联系人列表。 2、用户,存放在服务提供方的受保护的资源的拥有者。 3、客户端,要访问服务提供方资源的第三方应用,通常是网站,如提供照片打印服务的网站。在认证过程之前,客户端要向服务提供者申请客户端标识。
例如微信的第三方登陆,以京东的微信登陆为例,此时微信是服务的提供方,京东就是客户端。京东需要获取微信中用户存储的姓名与头像等身份信息。具体流程可以看下图
OAuth2 的授权模式
OAuth2.0有4种授权模式:
- 授权码 (Authorization Code Grant 又称授权码模式)
- 隐式授权 (Implicit Grant 又称简化模式)
- RO凭证授权 (Resource Owner Password Credentials Grant 又称密码模式)
- Client凭证授权 (Client Credentials Grant又称客户端模式)
授权码模式
授权代码授予类型用于获取访问令牌和刷新令牌,并针对机密客户端进行了优化。由于这是一个基于重定向的流,因此客户端必须能够与资源所有者的用户代理(通常是 Web 浏览器)进行交互,并且能够(通过重定向)从授权服务器接收传入的请求。
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
验证流程阐述:
- (A) 客户端通过将资源所有者的用户代理定向到授权终结点来启动流。 客户端包括其客户端标识符、请求的作用域、本地状态和重定向 URI,授权服务器将在授予(或拒绝)访问权限后将用户代理发回该 URI。
- (B) 授权服务器(通过用户代理)对资源所有者进行身份验证,并确定资源所有者是授予还是拒绝客户端的访问请求。
- (C) 假定资源所有者授予访问权限,授权服务器使用前面提供的重定向 URI(在请求中或在客户端注册期间)将用户代理重定向回客户端。 重定向 URI 包括授权代码和客户端之前提供的任何本地状态
- (D) 客户端通过包含上一步中收到的授权代码,从授权服务器的令牌终结点请求访问令牌。 发出请求时,客户端向授权服务器进行身份验证。 客户端包含用于获取用于获取用于验证的授权代码的重定向 URI。
- (E) 授权服务器对客户端进行身份验证,验证授权代码,并确保收到的重定向 URI 与步骤
(C) 中用于重定向客户端的 URI 匹配。 如果有效,授权服务器将使用访问令牌和刷新令牌(可选)进行响应。
简化授权模式
隐式授权类型用于获取访问令牌(它支持颁发刷新令牌),并针对已知运行特定重定向 URI 的公共客户端进行了优化。 这些客户端通常使用脚本语言(如 JavaScript)在浏览器中实现。
由于这是一个基于重定向的流,因此客户端必须能够与资源所有者的用户代理(通常是 Web 浏览器)进行交互,并且能够(通过重定向)从授权服务器接收传入的请求。
与授权代码授予类型不同,在授权代码授予类型中,客户端对授权令牌和访问令牌发出单独的请求,客户端接收访问令牌作为授权请求的结果。
隐式授权类型不包括客户端身份验证,并且依赖于资源所有者的存在和重定向 URI 的注册。 由于访问令牌已编码到重定向 URI 中,因此可能会向资源所有者和驻留在同一设备上的其他应用程序公开访问令牌。
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI --->| |
| User- | | Authorization |
| Agent -|----(B)-- User authenticates -->| Server |
| | | |
| |<---(C)--- Redirection URI ----<| |
| | with Access Token +---------------+
| | in Fragment
| | +---------------+
| |----(D)--- Redirection URI ---->| Web-Hosted |
| | without Fragment | Client |
| | | Resource |
| (F) |<---(E)------- Script ---------<| |
| | +---------------+
+-|--------+
| |
(A) (G) Access Token
| |
^ v
+---------+
| |
| Client |
| |
+---------+
验证流程阐述:
- (A) 客户端通过将资源所有者的用户代理定向到授权终结点来启动流。 客户端包括其客户端标识符、请求的作用域、本地状态和重定向 URI,授权服务器将在授予(或拒绝)访问权限后将用户代理发回该 URI。
- (B) 授权服务器(通过用户代理)对资源所有者进行身份验证,并确定资源所有者是授予还是拒绝客户端的访问请求。
- ? 假定资源所有者授予访问权限,授权服务器将使用前面提供的重定向 URI 将用户代理重定向回客户端。 重定向 URI 在 URI 片段中包含访问令牌。
- (D) 用户代理遵循重定向指令,向 Web 托管的客户机资源发出请求。 用户代理在本地保留片段信息。
- (E) Web 托管的客户机资源返回一个网页(通常是带有嵌入式脚本的 HTML 文档),该网页能够访问完整的重定向 URI,包括用户代理保留的片段,并提取片段中包含的访问令牌(和其他参数)。
- (F) 用户代理在本地执行 Web 托管的客户机资源提供的脚本,该脚本提取访问令牌。
- (G) 用户代理将访问令牌传递给客户端。
密码模式
资源所有者密码凭据授予类型适用于资源所有者与客户端(如设备操作系统或特权应用程序)建立信任关系的情况。 授权服务器在启用此授权类型时应特别小心,并且仅在其他流不可行时才允许它。
此授权类型适用于能够获取资源所有者凭据(用户名和密码,通常使用交互式表单)的客户端。 它还用于使用直接身份验证方案(如 HTTP 基本或摘要)迁移现有客户端。 通过将存储的凭据转换为访问令牌来对 OAuth 进行身份验证。
+----------+
| Resource |
| Owner |
| |
+----------+
v
| Resource Owner
(A) Password Credentials
|
v
+---------+ +---------------+
| |>--(B)---- Resource Owner ------->| |
| | Password Credentials | Authorization |
| Client | | Server |
| |<--(C)---- Access Token ---------<| |
| | (w/ Optional Refresh Token) | |
+---------+ +---------------+
验证流程阐述:
- (A) 资源所有者向客户端提供其用户名和密码。
- (B) 客户端通过包含从资源所有者处收到的凭据,从授权服务器的令牌终结点请求访问令牌。 发出请求时,客户端向授权服务器进行身份验证。
- ? 授权服务器对客户端进行身份验证并验证资源所有者凭据,如果有效,则颁发访问令牌。
客户端模式
在客户端模式下,客户端仅需要发送客户端自己的凭证 (或其他支持的验证方式) 就可以请求并获取到一个 access token (令牌)。客户端可以拿着 token 去请求在其控制下的受保护的资源,或者其他资源所有者先前安排给资源服务器的资源。
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
验证流程阐述:
- (A) 客户端使用授权服务器进行验证
- (B) 授权服务器对客户端的信息进行验证,如果是合法的则签发一个 access token
OAuth2 刷新令牌
刷新令牌是用于获取访问令牌的凭据。 刷新令牌由授权服务器颁发给客户端,用于在当前访问令牌无效或过期时获取新的访问令牌,或者获取具有相同或更窄范围的其他访问令牌(访问令牌的生存期可能比资源所有者授权的权限短,权限更少)。 颁发刷新令牌是可选的,由授权服务器自行决定。 如果授权服务器颁发刷新令牌,则在颁发访问令牌时会包含刷新令牌(即图 1 中的步骤 (D) )。
刷新令牌是一个字符串,表示资源所有者授予客户端的授权。 该字符串通常对客户端不透明。 令牌表示用于检索授权信息的标识符。 与访问令牌不同,刷新令牌仅用于授权服务器,从不发送到资源服务器。
+--------+ +---------------+
| |--(A)------- Authorization Grant --------->| |
| | | |
| |<-(B)----------- Access Token -------------| |
| | & Refresh Token | |
| | | |
| | +----------+ | |
| |--(C)---- Access Token ---->| | | |
| | | | | |
| |<-(D)- Protected Resource --| Resource | | Authorization |
| Client | | Server | | Server |
| |--(E)---- Access Token ---->| | | |
| | | | | |
| |<-(F)- Invalid Token Error -| | | |
| | +----------+ | |
| | | |
| |--(G)----------- Refresh Token ----------->| |
| | | |
| |<-(H)----------- Access Token -------------| |
+--------+ & Optional Refresh Token +---------------+
刷新令牌流程阐述:
- (A) 客户端通过向授权服务器进行身份验证并提交授权授予来请求访问令牌。
- (B) 授权服务器对客户端进行身份验证并验证授权授予,如果有效,则颁发访问令牌和刷新令牌。
- (C) 客户端通过提供访问令牌向资源服务器发出受保护的资源请求。
- (D) 资源服务器验证访问令牌,如果有效,则为请求提供服务。
- (E) 重复步骤 (C) 和 (D),直到访问令牌过期。 如果客户端知道访问令牌已过期,它将跳到步骤 (G);否则,它会发出另一个受保护的资源请求。
- (F) 由于访问令牌无效,资源服务器将返回无效令牌错误。
- (G) 客户端通过向授权服务器进行身份验证并提供刷新令牌来请求新的访问令牌。 客户端身份验证要求基于客户端类型和授权服务器策略。
- (H) 授权服务器对客户端进行身份验证并验证刷新令牌,如果有效,则颁发新的访问令牌(以及可选的新刷新令牌)。
Spring Security OAuth2 自定义授权服务器
引入 Maven 依赖
pom.xml
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
applicaton.yml
server:
port: 8088
授权码模式
配置 WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception{
http.formLogin().permitAll()
.successForwardUrl("/loginSuccess");
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers(HttpMethod.POST,"/login").permitAll()
.anyRequest().authenticated();
http.logout().permitAll();
http.csrf().disable();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public MyUserDetailsService myUserDetailsService(){
return new MyUserDetailsService();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
定义自己的 UserDetailsService
UserDetailsService
public class MyUserDetailsService implements UserDetailsService{
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = "kgtdata";
String pwdCrypt = passwordEncoder.encode(password);
List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
authorityList.add(new SimpleGrantedAuthority("ROLE_user"));
UserDetails user = new User(username,pwdCrypt,authorityList);
return user;
}
}
创建认证服务器
AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
clients.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("yourSecret"))
.redirectUris("http://www.baidu.com")
.scopes("all")
.authorizedGrantTypes("authorization_code");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer){
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
创建资源服务器
ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer
public class MyResourceServerConfigurerAdapter extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception{
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestMatchers().antMatchers("/user/**");
}
}
定义两个 Controller
UserController
@RestController
public class UserController {
@PostMapping("/user/info")
public String info(){
return "The User Info !!!";
}
}
LoginController
@RestController
public class LoginController {
@PostMapping("/loginSuccess")
public String login(){
return "Sucess !!!";
}
}
测试效果
设置验证参数
设置请求参数
Postman 调用结束后可以看见
设置验证的token
然后得到相应的返回
简化模式实现
修改认证服务器代码
AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
clients.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("yourSecret"))
.redirectUris("http://www.baidu.com")
.scopes("all")
.accessTokenValiditySeconds(3600).refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer){
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
测试效果
通过授权后可以看见网址栏中返回了 token
https://www.baidu.com/#access_token=442e830a-e488-47eb-b78e-1626d64097b9&token_type=bearer&expires_in=3458
密码模式实现
修改认证服务器
AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
clients.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("yourSecret"))
.redirectUris("http://www.baidu.com")
.scopes("all")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit","password");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer){
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
测试效果
然后可以看见返回了如下内容
{"access_token":"5a8a0e3f-b389-41a6-beec-26be5aef977e","token_type":"bearer","expires_in":3599,"scope":"all"}
客户端模式实现
修改认证服务器
AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
clients.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("yourSecret"))
.redirectUris("http://www.baidu.com")
.scopes("all")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit","password","client_credentials");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer){
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
测试效果
然后可以看见返回
{"access_token":"9ac4f34a-aebe-4ed2-9037-e5e906c76b81","token_type":"bearer","expires_in":3599,"scope":"all"}
更新 Token 实现
修改认证服务器
AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
AuthenticationManager authenticationManager;
@Autowired
MyUserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager)
.reuseRefreshTokens(false)
.userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
clients.inMemory()
.withClient("client")
.secret(passwordEncoder.encode("yourSecret"))
.redirectUris("http://www.baidu.com")
.scopes("all")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit","password","client_credentials","refresh_token");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer){
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
测试效果
- 首先用密码模式获取 access_token 和 refresh_token
在浏览器中输入 http://localhost:8088/oauth/token?grant_type=password&client_id=client&scope=all&client_secret=yourSecret&username=user&password=myPassword
然后可以看到浏览器返回
{"access_token":"32d96307-db25-4c36-b303-6b065cc863b2","token_type":"bearer","refresh_token":"b6489e37-0b33-4158-93c7-13746c74b134","expires_in":3599,"scope":"all"}
- 然后用 refresh_token 请求新的 token
浏览器中输入 http://localhost:8088/oauth/token?grant_type=refresh_token&client_id=client&client_secret=yourSecret&refresh_token=b6489e37-0b33-4158-93c7-13746c74b134
然后浏览器会返回新的 access_token 和 refresh_token
{"access_token":"6a79f640-d4ad-4f60-aba0-f3b5f1d0087b","token_type":"bearer","refresh_token":"eac5efe2-5ce3-4a21-82c3-bf67fdd626da","expires_in":3600,"scope":"all"}
引入 JWT
什么是 JWT
JWT (Json Web Token)是一种提议的 Internet 标准,用于创建具有可选签名和/或可选加密的数据,其有效负载包含声明一些声明的JSON . 令牌使用私有密钥或公钥/私钥进行签名。
JWT 的组成
JWT的token是三段由小数点分隔组成的字符串:header.payload.signature,即头部、载荷与签名。
header
Header header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
【示例】
{
'alg': "HS256",
'typ': "JWT"
}
payload
Payload JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
- Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
- Public claims : 定义新创的信息,比如用户信息和其他重要信息
- Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
JWT规定7个官方字段,供选用:
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):面向的用户
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号,唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
Signature
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
Signature 由 Header 指定的算法 HS256 加密产生。该算法有两个参数,第一个参数是经过 Base64 分别编码的 Header 及 Payload 通过 . 连接组成的字符串,第二个参数是生成的密钥,由服务器保存。
$Signature = HS256(Base64(Header) + "." + Base64(Payload), secretKey)
JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature
整合 JWT
Maven 引入
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
创建 JWT 写相关配置类
@Configuration
public class JWTTokenStoreConfig {
@Bean
JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("yourKey");
return jwtAccessTokenConverter;
}
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
}
修改 OAuth2 授权服务器配置
AuthorizationServerConfigurerAdapter
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
@Qualifier("JwtTokenStore")
private TokenStore tokenStore;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager)
.reuseRefreshTokens(false)
.userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET)
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter);
}
测试效果
这里直接使用密码模式进行 token 请求 浏览器直接访问 http://localhost:8088/oauth/token?grant_type=password&client_id=client&scope=all&client_secret=yourSecret&username=user&password=password
最后我们可以看见效果
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTIxNjQwNDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX3VzZXIiXSwianRpIjoiNjY3MTIxZWYtNDU0OS00NmUyLTk2MmQtNTg3OTI5ZmJkNzk4IiwiY2xpZW50X2lkIjoiY2xpZW50Iiwic2NvcGUiOlsiYWxsIl19.S5qOsDu2OhQhuZxM14QwkR1Vn29KxU7rmFKiCaphidw","token_type":"bearer","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjY2NzEyMWVmLTQ1NDktNDZlMi05NjJkLTU4NzkyOWZiZDc5OCIsImV4cCI6MTY1MjIzODQ0OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV91c2VyIl0sImp0aSI6IjM3NTk5ODhlLTJmNGEtNDBjNC1hNTA1LWZhMDViY2QzYTA2MyIsImNsaWVudF9pZCI6ImNsaWVudCJ9.piNiGQ_E7wH1zprndJYsUsdz-nZ2SdP0V-QWH1igqNY","expires_in":3599,"scope":"all","jti":"667121ef-4549-46e2-962d-587929fbd798"}
扩展 JWT 中的内容
创建一个 JWT 增强类
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String,Object> info = new HashMap<>();
info.put("enhance","enhance info");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
}
}
在刚才自定义的 JWT 配置类中将 JwtTokenEnhancer 注册成 Bean
@Bean
public JwtTokenEnhancer tokenEnhancer(){ return new JwtTokenEnhancer();}
修改 OAuth2 授权服务器配置
AuthorizationServerConfigurerAdapter
@Autowired
JwtTokenEnhancer tokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager)
.reuseRefreshTokens(false)
.userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
endpoints.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter);
TokenEnhancerChain chain = new TokenEnhancerChain();
List<TokenEnhancer> enhancerList = new ArrayList<>();
enhancerList.add(tokenEnhancer);
enhancerList.add(jwtAccessTokenConverter);
chain.setTokenEnhancers(enhancerList);
endpoints.tokenEnhancer(chain);
}
Spring Security OAuth2 客户端
Spring Security OAuth2 客户端是用于代理我们对所谓的 OAuth2 授权服务器进行访问的工具。我们可以用其获得相应的 token ,且可以对资源进行进一步的请求。
常用类介绍
- ClientRegistration :
ClientRegistration 是一个 OAuth2.0 或 OpenId Connect 1.0 的 Provider 注册的一个门面或代表对象。它包含了诸如 client-id,client-secret, authorization-grant-type,redirect-uri 等信息 - ClientRegistrationRepository :
ClientRegistrationRepository 用于存储和提供 ClientRegistration - OAuth2AuthorizedClient :
OAuth2AuthorizedClient 是授权客户端的表示形式。当终端用户(资源所有者)已向客户端授予访问其受保护资源的授权时,将客户端视为已授权。OAuth2AuthorizedClient 用于将 OAuth2AccessToken (和可选的 OAuth2RefreshToken )关联到 ClientRegistration (客户端)和资源所有者,后者是授予授权的主要终端用户。 - OAuth2AuthorizedClientRepository/
OAuth2AuthorizedClientService :OAuth2AuthorizedClientRepository 负责在 Web 请求之间持久保存 OAuth2AuthorizedClient 。。然而,OAuth2AuthorizedClientService 的主要角色是在应用程序级别管理 OAuth2AuthorizedClient 。从开发人员的角度来看,OAuth2AuthorizedClientRepository 或 OAuth2AuthorizedClientService 提供了查找与客户端关联的 OAuth2AccessToken 的功能,以便使用它来启动受保护的资源请求。 - OAuth2AuthorizedClientManager / OAuth2AuthorizedClientProvider :
OAuth2AuthorizedClientManager 负责 OAuth2AuthorizedClient 的整体管理。Auth2AuthorizedClientProvider 实现了授权(或重新授权)OAuth 2.0 客户端的策略。实现通常会实现授权授予类型,例如。授权码、客户端凭据等。一般默认使用的 Auth2AuthorizedClientProvider 为 DelegatingOAuth2AuthorizedClientProvider 。DelegatingOAuth2AuthorizedClientProvider 中包含了 List<OAuth2AuthorizedClientProvider> authorizedClientProviders
下图展示了 OAuth2AuthorizedClientProvider 的相关实现类
客户端请求 Token 的流程介绍
一言以蔽之就是一个 OAuth2AuthorizationRequest 通过 OAuth2AuthorizedClientManager 的验证,然后返回一个 OAuth2AuthorizedClient 的过程。其中 Token 存储在 OAuth2AuthorizedClient 中
在这里我们以 DefaultOAuth2AuthorizedClientManager 为示例进行介绍。
我们将其中的复杂部分去掉,其主要的流程如下图。
其中大多数操作均集中在转换阶段,下图是转换过程的流程图。
采用密码模式进行验证
密码模式的 provicer 的验证流程可如下图
配置 yaml
application.yml
server:
port: 8088
auth_server: http://localhost:${server.port}/
spring:
security:
oauth2:
client:
registration:
test:
clientId: client
clientSecret: yourSecret
authorizationGrantType: password
scope: all
provider:
test:
tokenUri: ${auth_server}/oauth/token
编写使用客户端的配置类
@Configuration
public class OAuth2Config {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() {
return authorizeRequest -> {
Map<String, Object> contextAttributes = Collections.emptyMap();
HttpServletRequest servletRequest = authorizeRequest.getAttribute(HttpServletRequest.class.getName());
String username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME);
String password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD);
if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
contextAttributes = new HashMap<>();
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
}
修改 Adapter
WebSecurityConfigurerAdapter
@Override
public void configure(HttpSecurity http) throws Exception{
http.formLogin();
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.antMatchers(HttpMethod.POST,"/login").permitAll()
.antMatchers(HttpMethod.POST,"/test/**").permitAll()
.anyRequest().authenticated();
http.logout().permitAll();
http.csrf().disable();
http.oauth2Client(Customizer.withDefaults());
}
添加用于测试的 Controller
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager;
@PostMapping("/test/1")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
if(authentication == null)
authentication = new UsernamePasswordAuthenticationToken("user","kgtdata");
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
System.out.println(accessToken.getTokenValue());
return "index";
}
测试效果
- 使用 Postman 直接调用
然后可以看到控制台打印出的 Token
采用授权码模式获取 Token
授权码获取 Token 的过程可如下图
变写 yaml 配置文件
server:
port: 8088
auth_server: http://localhost:${server.port}/
spring:
security:
oauth2:
client:
registration:
test:
clientId: client
clientSecret: yourSecret
redirectUri: ${auth_server}/test/2
authorizationGrantType: authorization_code
scope: all
provider:
test:
authorizationUri: ${auth_server}/oauth/authorize
tokenUri: ${auth_server}/oauth/token
编写重定向后获取 Token 的Controller
@Autowired
ClientRegistrationRepository clientRegistrationRepository;
@GetMapping("test/2")
public String authCode(@PathParam("code") String code){
if(code != null && code != ""){
OAuth2AuthorizationRequest oAuth2AuthorizeRequest = OAuth2AuthorizationRequest.authorizationCode()
.authorizationUri("http://localhost:8088/oauth/authorize")
.clientId("client")
.build();
OAuth2AuthorizationResponse response = OAuth2AuthorizationResponse.success(code).
redirectUri("http://localhost:8088/test/2").build();
OAuth2AuthorizationExchange exchange = new OAuth2AuthorizationExchange(oAuth2AuthorizeRequest,response);
OAuth2AuthorizationCodeGrantRequest request = new OAuth2AuthorizationCodeGrantRequest(
clientRegistrationRepository.findByRegistrationId("test"),exchange);
OAuth2AccessTokenResponseClient codeClient = new DefaultAuthorizationCodeTokenResponseClient();
OAuth2AccessTokenResponse oauth2response = codeClient.getTokenResponse(request);
OAuth2AccessToken s = oauth2response.getAccessToken();
System.out.println(s.getTokenValue());
return "test Success";
}
return "test Failure";
}
测试效果
然后可以看到浏览器中 url 发生跳转,然后看到控制台打印出了 token
OAuth2 资源服务器
OAuth2 资源服务器的验证流程
- Filter来自读取承载令牌的身份验证将
BearerTokenAuthenticationToken 传递给 AuthenticationManager 由实现的 ProviderManager 。 - 配置
ProviderManager 为 AuthenticationProvider 类型 JwtAuthenticationProvider 。 JwtAuthenticationProvider 使用 JwtDecoder 对 Jwt 解码、校验和验证有消性。- 然后
JwtAuthenticationProvider 使用 JwtAuthenticationConverter 将 Jwt 转换为授予权限的 Collection 。 - 身份验证成功后,
Authentication 返回的是 JwtAuthenticationToken 类型的对象,并且 Authentication 的 principal 是由 JwtDecoder 返回的 Jwt 对象。最终,返回的 JwtAuthenticationToken 将由 SecurityContextHolderauthentication 中验证的 Filter 进行进一步的处理。
JwtAuthenticationProvider 的结构可简化成如下
JwtAuthenticationProvider 的具体验证流程如下
验证服务器端进行修改
Tips: 非对称加密 非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
验证服务器单主要使用私钥对 Jwt 进行加密,然后使用公钥对数据进行解密。因此私钥在验证服务器端,而公钥则在客户端。首先我们先在验证服务器上引入私钥进行使用。
首先使用 Keytool 创建私钥
keytool -genkey \
-alias jwt \ # 设置别名
-keyalg RSA \ # 设置算法
-keysize 1024 \
-keystore jwt.jks \ # 设置秘钥的存储名称
-validity 365 \ # 设置私钥的有效日期
-keypass 1234556 -storepass 123456 # 设置密码
在验证服务器上使用私钥
将秘钥文件放在 resources 文件夹下
修改 JwtAccessTokenConverter
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"),"123456".toCharArray());
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
return accessTokenConverter;
}
使用 OAuth2 资源服务器
通过私钥生成公钥
keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey
将获得的公钥存储在 public.cert 文件中
在服务器上使用私钥
将 public.cert 文件放在
修改 yml 配置文件
application.yml
spring:
security:
oauth2:
client:
registration:
test:
clientId: client
clientSecret: yourSecret
redirectUri: http://localhost:${server.port}/test/2
authorizationGrantType: password
scope: all
provider:
test:
authorizationUri: ${auth_server}/oauth/authorize
tokenUri: ${auth_server}/oauth/token
resourceserver:
jwt:
public-key-location: classpath:public.cert
修改 WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter
@Override
public void configure(HttpSecurity http) throws Exception{
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/test/**").permitAll()
.antMatchers("/user/**").permitAll()
.anyRequest().authenticated();
http.oauth2Client(Customizer.withDefaults());
http.oauth2ResourceServer().jwt();
http.formLogin();
}
对 Controller 进行修改
@Autowired
JwtDecoder jwtDecoder;
@PostMapping("/test/1")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
if(authentication == null)
authentication = new UsernamePasswordAuthenticationToken("user","password");
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
System.out.println(accessToken.getTokenValue());
Jwt jwt = jwtDecoder.decode(accessToken.getTokenValue());
System.out.println(jwt.getHeaders());
System.out.println(jwt.getId());
return "index";
}
|