Spring Security + OAuth2 + JWT 基本使用
前面學習了 Spring Security 入門,現在搭配 oauth2 + JWT 進行測試。
1、什么是 OAuth2
OAuth 是一個關於授權(authorization)的開放網絡標准,使得第三方應用可以使用該令牌在限定時間、限定范圍訪問指定資源。在全世界得到廣泛應用,目前的版本是2.0版。
1.1、關於 OAuth2 的幾個重要概念:
resource owner
: 擁有被訪問資源的用戶user-agent
: 一般來說就是瀏覽器client
: 第三方應用Authorization server
: 認證服務器,用來進行用戶認證並頒發tokenResource server
:資源服務器,擁有被訪問資源的服務器,需要通過token來確定是否有權限訪問
1.2、握手流程
明確概念后,就可以看 OAuth2 的協議握手流程,摘自RFC6749
(A)用戶打開客戶端以后,客戶端要求用戶給予授權。
(B)用戶同意給予客戶端授權。
(C)客戶端使用上一步獲得的授權,向認證服務器申請令牌。
(D)認證服務器對客戶端進行認證以后,確認無誤,同意發放令牌。
(E)客戶端使用令牌,向資源服務器申請獲取資源。
(F)資源服務器確認令牌無誤,同意向客戶端開放資源
以QQ登錄為例,下圖實線部分是我們用戶真正操作的流程,而虛線部分則是服務內部的流程:
1.3、授權模式
oauth2根據使用場景不同,分成了4種模式
- 授權碼模式(authorization code)
- 簡化模式(implicit)
- 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)
授權碼模式使用到了回調地址,是最為復雜的方式,通常網站中經常出現的微博,qq第三方登錄,都會采用這個形式。簡化模式不常用。
2、配置
使用oauth2保護你的應用,可以分為簡易的分為三個步驟
- 配置資源服務器
- 配置授權服務器
- 配置spring security
2.1、maven 依賴配置
這里直接引入 spring-cloud oauth2,更加方便之后的拓展。
<!--spring boot-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.13.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<!--spring cloud oauth2-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!--spring cloud security-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
<!--spring cloud-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2.2、配置授權服務器
這里需要進行訪問客戶端的配置,並配置授權類型和access_token
轉jwtToken
。
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailServiceImpl userDetailService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
/**
* 配置授權類型
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//設置Jwt內容增強
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> list = new ArrayList<>();
list.add(jwtTokenEnhancer);
list.add(jwtAccessTokenConverter);
tokenEnhancerChain.setTokenEnhancers(list);
endpoints
//密碼模式必須配置
.authenticationManager(authenticationManager)
//密碼模式必須配置
.userDetailsService(userDetailService)
//accessToken轉JwtToken
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
//jwt內容增強
.tokenEnhancer(tokenEnhancerChain);
}
/**
* 配置客戶端詳情信息
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.
//基於內存配置
inMemory()
//客戶端ID
.withClient("client")
//密鑰
.secret(bCryptPasswordEncoder.encode("112233"))
//重定向地址
.redirectUris("http://www.baidu.com")
//授權范圍
.scopes("all")
//accessToken有效時間
.accessTokenValiditySeconds(60)
//refreshToken有效時間
.refreshTokenValiditySeconds(3600)
/**
* 授權類型
* authorization_code:授權碼模式
* password:密碼模式
* refresh_token:刷新令牌
*/
.authorizedGrantTypes("authorization_code", "password", "refresh_token");
}
}
2.3、配置資源服務器
繼承 ResourceServerConfigurerAdapter
並添加 @EnableResourceServer
注解
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//攔截所有請求
.anyRequest()
.authenticated()
.and()
//spring secuity提供了requestMatchers接口,等價於http.authorizeRequests().anyRequest().access("permitAll");
//提供資源,訪問/user需要權限認證
.requestMatchers()
.antMatchers("/user/**");
}
}
2.4、JWT 配置
2.4.1、accessToken 轉 JwtToken 配置類
主要工作是創建 JwtAccessTokenConverter
並設置密鑰,並注入到 Bean 管理容器中。
/**
* accessToken轉JwtToken配置
*/
@Configuration
public class JwtTokenStoreConfig {
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//設置jwt密鑰
jwtAccessTokenConverter.setSigningKey("test_key");
return jwtAccessTokenConverter;
}
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
}
2.4.2、JwtToken內容拓展配置類
當 accessToken 轉 jwtToken時,如果想往令牌中加入自定義用戶信息,例如登錄時間點,可以配置以下類:
/**
* JwtToken內容拓展配置類
* @author Lin
*/
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> map = new HashMap<>();
map.put("enhance", "enhance info");
((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);
return oAuth2AccessToken;
}
}
2.5、配置 spring security
/**
* spring security配置類
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密碼加密
*
* @return
*/
@Bean
public BCryptPasswordEncoder getPasswordEncode() {
return new BCryptPasswordEncoder();
}
/**
* 接口請求授權
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**", "/login/**","/logout/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
}
@Override
@Bean
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
2.6、實現 UserDetailsService
實現 UserDetailService 用於登錄驗證,以及密碼模式下需要用到。
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = bCryptPasswordEncoder.encode("123456");
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("permission1"));
}
}
創建 User
實體類如下(非必須):
public class User implements UserDetails {
private String username;
private String password;
private List<GrantedAuthority> authorities;
public User(String username, String password, List<GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3、運行測試
3.1、獲取授權碼
直接訪問 /oauth/authorize? 接口可以獲得授權碼
在我的項目中訪問路徑如下:
瀏覽器訪問,跳轉到http://localhost:8080/login.html
默認登錄頁,點擊登錄,授權:
跳轉到https://www.baidu.com/?code=XKee3V
頁面,XKee3v就是獲得的授權碼。
3.2、根據授權碼模式獲得令牌
利用 postman 測試,訪問
配置 Authorization 信息,即登錄客戶端的賬號和密碼;
配置 Body 信息,grant_type
的參數值是 authorization_code
,authorization_code
即為授權碼模式,code
即為上文獲得的授權碼。
配置完后運行測試,返回 access_token
和 refresh_token
,看到 access_token
成功轉為JwtToken
。
3.3、密碼模式
密碼模式比授權碼模式簡單一點,不需要獲得授權碼,直接忽略上文獲取授權碼的操作,只需稍微改動配置信息。
Authorization 信息無需改動,修改 Body 信息, grant_type
的參數值改為 password
,代表密碼模式,填寫登錄 spring security 的賬號和密碼。
3.4、刷新令牌
在上文中我設置了 access_token
的時效性為60秒,當access_token
失效時,需要根據refresh_token
獲取新的令牌。
訪問路徑如下:
Authorization 配置信息如下:
Body 需要配置 grant_type
的參數值為 refresh_token
,代表刷新令牌,並填寫refresh_token
的參數值。訪問后即可獲得新的 access_token
。
3.5、根據 access_token
獲得資源
訪問路徑如下:
Header 請求頭添加 Authorization 參數,並設置參數值為 bearer+空格+ access_token,即可獲得接口返回值。