OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
在认证和授权的过程中涉及的三方包括: 1、服务提供方,用户使用服务提供方来存储受保护的资源,如照片,视频,联系人列表。 2、用户,存放在服务提供方的受保护的资源的拥有者。 3、客户端,要访问服务提供方资源的第三方应用,通常是网站,如提供照片打印服务的网站。在认证过程之前,客户端要向服务提供者申请客户端标识。
例如微信的第三方登陆,以京东的微信登陆为例,此时微信是服务的提供方,京东就是客户端。京东需要获取微信中用户存储的姓名与头像等身份信息。具体流程可以看下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F0kcAhAv-1655371871830)(https://app.yinxiang.com/FileSharing.action?hash=1/76baa0bfbb881035cc649d6540fd7167-17793)]
OAuth2.0有4种授权模式:
授权代码授予类型用于获取访问令牌和刷新令牌,并针对机密客户端进行了优化。由于这是一个基于重定向的流,因此客户端必须能够与资源所有者的用户代理(通常是 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)
验证流程阐述:
(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 |
| |
+---------+
验证流程阐述:
资源所有者密码凭据授予类型适用于资源所有者与客户端(如设备操作系统或特权应用程序)建立信任关系的情况。 授权服务器在启用此授权类型时应特别小心,并且仅在其他流不可行时才允许它。
此授权类型适用于能够获取资源所有者凭据(用户名和密码,通常使用交互式表单)的客户端。 它还用于使用直接身份验证方案(如 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) | |
+---------+ +---------------+
验证流程阐述:
在客户端模式下,客户端仅需要发送客户端自己的凭证 (或其他支持的验证方式) 就可以请求并获取到一个 access token (令牌)。客户端可以拿着 token 去请求在其控制下的受保护的资源,或者其他资源所有者先前安排给资源服务器的资源。
+---------+ +---------------+
| | | |
| |>--(A)- Client Authentication --->| Authorization |
| Client | | Server |
| |<--(B)---- Access Token ---------<| |
| | | |
+---------+ +---------------+
验证流程阐述:
刷新令牌是用于获取访问令牌的凭据。 刷新令牌由授权服务器颁发给客户端,用于在当前访问令牌无效或过期时获取新的访问令牌,或者获取具有相同或更窄范围的其他访问令牌(访问令牌的生存期可能比资源所有者授权的权限短,权限更少)。 颁发刷新令牌是可选的,由授权服务器自行决定。 如果授权服务器颁发刷新令牌,则在颁发访问令牌时会包含刷新令牌(即图 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 +---------------+
刷新令牌流程阐述:
pom.xml
<!-- 引入 OAuth2 核心包 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-core</artifactId>
</dependency>
<!-- 引入 OAuth2 客户端依赖 -->
<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
@Configuration
@EnableWebSecurity
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
// Step2: 重写 configure 方法
@Override
public void configure(HttpSecurity http) throws Exception{
http.formLogin().permitAll()
.successForwardUrl("/loginSuccess"); // 登陆成功时跳转的url
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll() // 通过所有 OAuth2 请求
.antMatchers(HttpMethod.POST,"/login").permitAll() // 通过 login 请求
.anyRequest().authenticated();
http.logout().permitAll();
http.csrf().disable();
}
// 注入密码加密器
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
// 注入创建的 UserService
@Bean
public MyUserDetailsService myUserDetailsService(){
return new MyUserDetailsService();
}
// 重写 Configure 方法,使配置生效
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService()) // 使用自定义的 UserDetailService
.passwordEncoder(passwordEncoder()); // 指定校验时使用的密码加密器
}
// 将代理 AuthenticationManager 注册成 Bean,供直接使用
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
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") // 设置 client_Id
.secret(passwordEncoder.encode("yourSecret"))// 配置 client_secret
.redirectUris("http://www.baidu.com") // 设置重定向的 uri
.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/**");
}
}
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 !!!";
}
}
设置验证参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p5zycLci-1655371871833)(https://app.yinxiang.com/FileSharing.action?hash=1/e6bede6e8ac6fff9e9686476393c9d91-26273)]
设置请求参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VWs7Xttq-1655371871834)(https://app.yinxiang.com/FileSharing.action?hash=1/a2be1a148f4418f9eb878f25d943bf97-19097)]
Postman 调用结束后可以看见
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yUTJt4xq-1655371871834)(https://app.yinxiang.com/FileSharing.action?hash=1/d0fe804c241350ec3348e1dd87758836-4394)]
设置验证的token
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rtu0pMTt-1655371871834)(https://app.yinxiang.com/FileSharing.action?hash=1/a055b44d9a9f179434cd8096f9356c19-28313)]
然后得到相应的返回 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSZ5kjz1-1655371871835)(https://app.yinxiang.com/FileSharing.action?hash=1/74f0fbd2ee5baba03a6d9dec64f6e984-7425)]
AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;// 为设置密钥添加加密器
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{// 配置授权服务器有关客户端的配置
clients.inMemory()
.withClient("client") // 设置 client_Id
.secret(passwordEncoder.encode("yourSecret"))// 配置 client_secret
.redirectUris("http://www.baidu.com") // 设置重定向的 uri
.scopes("all")// 设置授权的作用域
.accessTokenValiditySeconds(3600).refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit"); // <- 这里添加一个 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; // 注入一个 authenticationManager
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager) // 使用密码模式时,需要为 Endpoint 添加authenticationManager
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{// 配置授权服务器相关配置
clients.inMemory()
.withClient("client") // 配置 client_id
.secret(passwordEncoder.encode("yourSecret"))// 配置 client_secret
.redirectUris("http://www.baidu.com") // 设置重定向的 uri
.scopes("all")// 设置授权的作用域
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit","password"); // <- 添加密码授权模式 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; // 注入一个 authenticationManager
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager) // 使用密码模式时,需要为 Endpoint 添加authenticationManager
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{// 配置授权服务器相关配置
clients.inMemory()
.withClient("client") // 配置 client_id
.secret(passwordEncoder.encode("yourSecret"))// 配置 client_secret
.redirectUris("http://www.baidu.com") // 设置重定向的 uri
.scopes("all")// 设置授权的作用域
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit","password","client_credentials"); // <- 添加客户端授权模式 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"}
AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class MyAuthorizationServerConfigurerAdapter extends AuthorizationServerConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;// 为设置密钥添加加密器
@Autowired
AuthenticationManager authenticationManager; // 注入一个 authenticationManager
@Autowired
MyUserDetailsService userDetailsService; // 注入一个 UserDetailsService
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager) // 使用密码模式时,需要为 Endpoint 添加authenticationManager
.reuseRefreshTokens(false) // 设置 refresh token 是否重复使用
.userDetailsService(userDetailsService) // 设置 refresh token 对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception{// 配置授权服务器相关配置
clients.inMemory()
.withClient("client") // 配置 client_id
.secret(passwordEncoder.encode("yourSecret"))// 配置 client_secret
.redirectUris("http://www.baidu.com") // 设置重定向的 uri
.scopes("all")// 设置授权的作用域
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(78000)
.authorizedGrantTypes("authorization_code","implicit","password","client_credentials","refresh_token"); // <- 添加 refresh_token 授权类型
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer){// 配置验证服务器相关设置
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
然后可以看到浏览器返回
{"access_token":"32d96307-db25-4c36-b303-6b065cc863b2","token_type":"bearer","refresh_token":"b6489e37-0b33-4158-93c7-13746c74b134","expires_in":3599,"scope":"all"}
然后浏览器会返回新的 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 (Json Web Token)是一种提议的 Internet 标准,用于创建具有可选签名和/或可选加密的数据,其有效负载包含声明一些声明的JSON . 令牌使用私有密钥或公钥/私钥进行签名。
JWT的token是三段由小数点分隔组成的字符串:header.payload.signature,即头部、载荷与签名。
Header header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。
【示例】
{
'alg': "HS256",
'typ': "JWT"
}
Payload JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
JWT规定7个官方字段,供选用:
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
Signature 由 Header 指定的算法 HS256 加密产生。该算法有两个参数,第一个参数是经过 Base64 分别编码的 Header 及 Payload 通过 . 连接组成的字符串,第二个参数是生成的密钥,由服务器保存。
$Signature = HS256(Base64(Header) + "." + Base64(Payload), secretKey)
JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature
<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>
@Configuration
public class JWTTokenStoreConfig {
@Bean
JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("yourKey"); // 配置 jwt 秘钥
return jwtAccessTokenConverter;
}
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter()); // 注册一个 JWTTokenStore 对象
}
}
AuthorizationServerConfigurerAdapter
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter; // 注入 TokenConverter
@Autowired
@Qualifier("JwtTokenStore")
private TokenStore tokenStore; // 注入 TokenStore
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager) // 使用密码模式时,需要为 Endpoint 添加authenticationManager
.reuseRefreshTokens(false) // 设置 refresh token 是否重复使用
.userDetailsService(userDetailsService) // 设置 refresh token 对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET)
.tokenStore(tokenStore) // 指定 tokenStore;
.accessTokenConverter(jwtAccessTokenConverter); // 指定 tokenConverter
}
这里直接使用密码模式进行 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"}
public class JwtTokenEnhancer implements TokenEnhancer { // 创建一个 Token 增强器,并实现接口
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String,Object> info = new HashMap<>(); // 创建一个 Map ,用于存储额外添加到 Token 中的信息
info.put("enhance","enhance info");
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
return oAuth2AccessToken;
}
}
@Bean
public JwtTokenEnhancer tokenEnhancer(){ return new JwtTokenEnhancer();}
AuthorizationServerConfigurerAdapter
@Autowired
JwtTokenEnhancer tokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception{
endpoints.authenticationManager(authenticationManager) // 使用密码模式时,需要为 Endpoint 添加authenticationManager
.reuseRefreshTokens(false) // 设置 refresh token 是否重复使用
.userDetailsService(userDetailsService) // 设置 refresh token 对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
endpoints.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter);
TokenEnhancerChain chain = new TokenEnhancerChain(); // 创建一个 TokenEnhancerChain 对象用于存放 TokenEnhancer 列表
List<TokenEnhancer> enhancerList = new ArrayList<>(); // 创建一个 TokenEnhancer 列表
enhancerList.add(tokenEnhancer); // 将自定的 TokenEnhancer 放入到列表中
enhancerList.add(jwtAccessTokenConverter);
chain.setTokenEnhancers(enhancerList); // 将列表放入到 TokenEnhancerChain 中
endpoints.tokenEnhancer(chain); // 端点配置,使 TokenEnhancerChain 生效
}
Spring Security OAuth2 客户端是用于代理我们对所谓的 OAuth2 授权服务器进行访问的工具。我们可以用其获得相应的 token ,且可以对资源进行进一步的请求。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sX6xt9O6-1655371871835)(https://app.yinxiang.com/FileSharing.action?hash=1/667e4c68dc65bdbb9a8757bb27f5d579-7625)]
ClientRegistration
是一个 OAuth2.0 或 OpenId Connect 1.0 的 Provider 注册的一个门面或代表对象。它包含了诸如 client-id,client-secret, authorization-grant-type,redirect-uri 等信息ClientRegistrationRepository
用于存储和提供 ClientRegistration
OAuth2AuthorizedClient
是授权客户端的表示形式。当终端用户(资源所有者)已向客户端授予访问其受保护资源的授权时,将客户端视为已授权。OAuth2AuthorizedClient
用于将 OAuth2AccessToken
(和可选的 OAuth2RefreshToken
)关联到 ClientRegistration
(客户端)和资源所有者,后者是授予授权的主要终端用户。OAuth2AuthorizedClientRepository
负责在 Web 请求之间持久保存 OAuth2AuthorizedClient
。。然而,OAuth2AuthorizedClientService
的主要角色是在应用程序级别管理 OAuth2AuthorizedClient
。从开发人员的角度来看,OAuth2AuthorizedClientRepository
或 OAuth2AuthorizedClientService
提供了查找与客户端关联的 OAuth2AccessToken
的功能,以便使用它来启动受保护的资源请求。OAuth2AuthorizedClientManager
负责 OAuth2AuthorizedClient
的整体管理。Auth2AuthorizedClientProvider
实现了授权(或重新授权)OAuth 2.0 客户端的策略。实现通常会实现授权授予类型,例如。授权码、客户端凭据等。一般默认使用的 Auth2AuthorizedClientProvider
为 DelegatingOAuth2AuthorizedClientProvider
。DelegatingOAuth2AuthorizedClientProvider
中包含了 List<OAuth2AuthorizedClientProvider> authorizedClientProviders
下图展示了 OAuth2AuthorizedClientProvider 的相关实现类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-abE73Obr-1655371871835)(https://app.yinxiang.com/FileSharing.action?hash=1/c5e956d927d3905b56d1d25b576c5bd1-14767)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HFweuA7H-1655371871836)(https://app.yinxiang.com/FileSharing.action?hash=1/b0d172f9eee887cf16f80d6f31b7b85c-18234)]
一言以蔽之就是一个 OAuth2AuthorizationRequest
通过
OAuth2AuthorizedClientManager
的验证,然后返回一个
OAuth2AuthorizedClient
的过程。其中 Token 存储在 OAuth2AuthorizedClient
中
在这里我们以 DefaultOAuth2AuthorizedClientManager
为示例进行介绍。
我们将其中的复杂部分去掉,其主要的流程如下图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-byPO0gqZ-1655371871836)(https://app.yinxiang.com/FileSharing.action?hash=1/6b6cab770b76804a995cfbfc028cddaf-4836)]
其中大多数操作均集中在转换阶段,下图是转换过程的流程图。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0jXHyGoH-1655371871836)(https://app.yinxiang.com/FileSharing.action?hash=1/2450dfaf3d29c70400b9b38a0f37f8bf-44290)]
密码模式的 provicer 的验证流程可如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TxgORQ6C-1655371871837)(https://app.yinxiang.com/FileSharing.action?hash=1/c064e95fb7555876ea6fd93e38b1bccf-25712)]
application.yml
server:
port: 8088
auth_server: http://localhost:${server.port}/ # 指定授权服务器地址
spring:
security:
oauth2:
client:
registration:
test: # registrationId
clientId: client # clientId
clientSecret: yourSecret # clientSecret
# redirectUri: ${auth_server}/oauth/token
authorizationGrantType: password # 授权类型
scope: all # 授权范围
provider:
test: # providerId
# authorizationUri: ${auth_server}/oauth/authorize # 验证授权的uri
tokenUri: ${auth_server}/oauth/token # 获取 token 的 uri
# resourceserver:
# jwt:
# jwk-set-uri: ${auth_server}/oauth/token_key
@Configuration
public class OAuth2Config {
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider = // 创建带有密码模式的 Provider
OAuth2AuthorizedClientProviderBuilder.builder()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = // 创建 OAuth2AuthorizedClientManager 并曝露出去以供使用
new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); // Manager 设置供验证使用的 Provider
// 设定的 contextAttributesMapper
// 用于将 OAuth2AuthorizationRequest 转换为 OAuth2AuthorizationContext 中的 attributes
authorizedClientManager.setContextAttributesMapper(contextAttributesMapper());
return authorizedClientManager;
}
private Function<OAuth2AuthorizeRequest, Map<String, Object>> contextAttributesMapper() { // 设定的 contextAttributesMapper
return authorizeRequest -> { // OAuth2AuthorizationRequest
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<>();
// 设定用于 Oauth2 的用户名和密码
contextAttributes.put(OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME, username);
contextAttributes.put(OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME, password);
}
return contextAttributes;
};
}
}
WebSecurityConfigurerAdapter
// Step2: 重写 configure 方法
@Override
public void configure(HttpSecurity http) throws Exception{
http.formLogin();
http.authorizeRequests()
.antMatchers("/oauth/**").permitAll() // 通过所有 OAuth2 请求
.antMatchers(HttpMethod.POST,"/login").permitAll() // 通过 login 请求
.antMatchers(HttpMethod.POST,"/test/**").permitAll() // 使用于测试的 url 进行用过
.anyRequest().authenticated();
http.logout().permitAll();
http.csrf().disable();
http.oauth2Client(Customizer.withDefaults()); // 启用 oauth2Client
}
@Autowired
private OAuth2AuthorizedClientManager authorizedClientManager; // 注入 OAuth2AuthorizedClientManager
@PostMapping("/test/1")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
if(authentication == null)
authentication = new UsernamePasswordAuthenticationToken("user","kgtdata");
// 根据参数构建 OAuth2 请求对象
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
// 通过 Manager 验证然后返回相关对象
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
// 获取返回的 Token
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
// 打印 Token
System.out.println(accessToken.getTokenValue());
return "index";
}
然后可以看到控制台打印出的 Token
授权码获取 Token 的过程可如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yTfx5KBL-1655371871837)(https://app.yinxiang.com/FileSharing.action?hash=1/696e92609c23eff66c2ce32fecaacdd4-16110)]
server:
port: 8088
auth_server: http://localhost:${server.port}/ # 指定授权服务器地址
spring:
security:
oauth2:
client:
registration:
test: # registrationId
clientId: client # clientId
clientSecret: yourSecret # clientSecret
redirectUri: ${auth_server}/test/2
authorizationGrantType: authorization_code #password # 授权类型
scope: all # 授权范围
provider:
test: # providerId
authorizationUri: ${auth_server}/oauth/authorize # 验证授权的uri
tokenUri: ${auth_server}/oauth/token # 获取 token 的 uri
# resourceserver:
# jwt:
# jwk-set-uri: ${auth_server}/oauth/token_key
@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 资源服务器的验证流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tHhMSY66-1655371871838)(https://docs.spring.io/spring-security/reference/_images/servlet/oauth2/jwtauthenticationprovider.png)]
BearerTokenAuthenticationToken
传递给 AuthenticationManager
由实现的 ProviderManager
。ProviderManager
为 AuthenticationProvider
类型 JwtAuthenticationProvider
。JwtAuthenticationProvider
使用 JwtDecoder
对 Jwt 解码、校验和验证有消性。JwtAuthenticationProvider
使用 JwtAuthenticationConverter
将 Jwt 转换为授予权限的 Collection
。Authentication
返回的是 JwtAuthenticationToken
类型的对象,并且 Authentication
的
principal 是由 JwtDecoder
返回的 Jwt 对象。最终,返回的 JwtAuthenticationToken
将由 SecurityContextHolderauthentication
中验证的 Filter
进行进一步的处理。JwtAuthenticationProvider 的结构可简化成如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uUa6VQrc-1655371871838)(https://app.yinxiang.com/FileSharing.action?hash=1/beafe9412c2ca286b7276d5f6859b787-5292)]
JwtAuthenticationProvider 的具体验证流程如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-btfCg5k1-1655371871838)(https://app.yinxiang.com/FileSharing.action?hash=1/48032c45b2ea409c1791e11bca690507-23228)]
Tips: 非对称加密 非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。
验证服务器单主要使用私钥对 Jwt 进行加密,然后使用公钥对数据进行解密。因此私钥在验证服务器端,而公钥则在客户端。首先我们先在验证服务器上引入私钥进行使用。
keytool -genkey \
-alias jwt \ # 设置别名
-keyalg RSA \ # 设置算法
-keysize 1024 \
-keystore jwt.jks \ # 设置秘钥的存储名称
-validity 365 \ # 设置私钥的有效日期
-keypass 1234556 -storepass 123456 # 设置密码
将秘钥文件放在 resources 文件夹下 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ytKNcBI-1655371871839)(https://app.yinxiang.com/FileSharing.action?hash=1/320b5eec2a740978701fd483d89c2c94-2359)]
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"),"123456".toCharArray()); // 123456 为生成秘钥时的密码
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt"));
return accessTokenConverter;
}
keytool -list -rfc --keystore jwt.jks | openssl x509 -inform pem -pubkey
将获得的公钥存储在 public.cert 文件中
将 public.cert 文件放在
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F59YoSOy-1655371871839)(https://app.yinxiang.com/FileSharing.action?hash=1/66ceadce7950bc9909c2142481d8f275-4364)]
application.yml
spring:
security:
oauth2:
client:
registration:
test: # registrationId
clientId: client # clientId
clientSecret: yourSecret # clientSecret
redirectUri: http://localhost:${server.port}/test/2
authorizationGrantType: password # authorization_code # 授权类型
scope: all # 授权范围
provider:
test: # providerId
authorizationUri: ${auth_server}/oauth/authorize # 验证授权的uri
tokenUri: ${auth_server}/oauth/token # 获取 token 的 uri
resourceserver:
jwt:
public-key-location: classpath:public.cert # 指定公钥位置
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(); // 启用 oauth2 resource server
http.formLogin();
}
@Autowired
JwtDecoder jwtDecoder;
@PostMapping("/test/1")
public String index(Authentication authentication,
HttpServletRequest servletRequest,
HttpServletResponse servletResponse) {
if(authentication == null)
authentication = new UsernamePasswordAuthenticationToken("user","password");
// 根据参数构建 OAuth2 请求对象
OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("test")
.principal(authentication)
.attributes(attrs -> {
attrs.put(HttpServletRequest.class.getName(), servletRequest);
attrs.put(HttpServletResponse.class.getName(), servletResponse);
})
.build();
// 通过 Manager 验证然后返回相关对象
OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest);
// 获取返回的 Token
OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
// 打印 Token
System.out.println(accessToken.getTokenValue());
Jwt jwt = jwtDecoder.decode(accessToken.getTokenValue()); // 使用 JwtDecoder 进行解析
System.out.println(jwt.getHeaders());
System.out.println(jwt.getId());
return "index";
}