'基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐'

JSON 設計 勞累的p7前端程序員 2019-08-21
"
"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動跳轉到授權中心統一登錄頁面

輸入用戶名 codesheep,密碼 123456,即可登錄認證,並進入授權頁面:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動跳轉到授權中心統一登錄頁面

輸入用戶名 codesheep,密碼 123456,即可登錄認證,並進入授權頁面:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

授權頁面

同意授權後,會自動返回之前客戶端的測試接口:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動跳轉到授權中心統一登錄頁面

輸入用戶名 codesheep,密碼 123456,即可登錄認證,並進入授權頁面:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

授權頁面

同意授權後,會自動返回之前客戶端的測試接口:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動返回客戶端接口並調用成功

此時我們再繼續訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/medium,發現已經直接可以調用而無需認證了:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動跳轉到授權中心統一登錄頁面

輸入用戶名 codesheep,密碼 123456,即可登錄認證,並進入授權頁面:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

授權頁面

同意授權後,會自動返回之前客戶端的測試接口:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動返回客戶端接口並調用成功

此時我們再繼續訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/medium,發現已經直接可以調用而無需認證了:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

直接訪問

由於 localhost:8086/normal 和 localhost:8086/medium要求的接口權限,用戶codesheep均具備,所以能順利訪問,接下來再訪問一下更高權限的接口:localhost:8086/admin:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動跳轉到授權中心統一登錄頁面

輸入用戶名 codesheep,密碼 123456,即可登錄認證,並進入授權頁面:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

授權頁面

同意授權後,會自動返回之前客戶端的測試接口:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動返回客戶端接口並調用成功

此時我們再繼續訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/medium,發現已經直接可以調用而無需認證了:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

直接訪問

由於 localhost:8086/normal 和 localhost:8086/medium要求的接口權限,用戶codesheep均具備,所以能順利訪問,接下來再訪問一下更高權限的接口:localhost:8086/admin:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

無權限訪問

好了,訪問客戶端1 (codesheep-client1) 的測試接口到此為止,接下來訪問外掛的客戶端2 (codesheep-client2) 的測試接口:localhost:8087/normal,會發現此時會自動跳到授權頁:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動跳轉到授權中心統一登錄頁面

輸入用戶名 codesheep,密碼 123456,即可登錄認證,並進入授權頁面:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

授權頁面

同意授權後,會自動返回之前客戶端的測試接口:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動返回客戶端接口並調用成功

此時我們再繼續訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/medium,發現已經直接可以調用而無需認證了:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

直接訪問

由於 localhost:8086/normal 和 localhost:8086/medium要求的接口權限,用戶codesheep均具備,所以能順利訪問,接下來再訪問一下更高權限的接口:localhost:8086/admin:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

無權限訪問

好了,訪問客戶端1 (codesheep-client1) 的測試接口到此為止,接下來訪問外掛的客戶端2 (codesheep-client2) 的測試接口:localhost:8087/normal,會發現此時會自動跳到授權頁:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

由於用戶已通過客戶端1登錄過_因此再訪問客戶端2即無需登錄_而是直接跳到授權頁

授權完成之後就可以順利訪問客戶端2 (codesheep-client2) 的接口:

"
基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

理論知識

在此之前需要學習和了解一些前置知識包括:

  • Spring Security:基於 Spring實現的 Web系統的認證和權限模塊
  • OAuth2:一個關於授權(authorization)的開放網絡標準
  • 單點登錄 (SSO):在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統
  • JWT:在網絡應用間傳遞信息的一種基於 JSON的開放標準((RFC 7519),用於作為JSON對象在不同系統之間進行安全地信息傳輸。主要使用場景一般是用來在 身份提供者和服務提供者間傳遞被認證的用戶身份信息

要完成的目標

  • 目標1:設計並實現一個第三方授權中心服務(Server),用於完成用戶登錄,認證和權限處理
  • 目標2:可以在授權中心下掛載任意多個客戶端應用(Client)
  • 目標3:當用戶訪問客戶端應用的安全頁面時,會重定向到授權中心進行身份驗證,認證完成後方可訪問客戶端應用的服務,且多個客戶端應用只需要登錄一次即可(謂之 “單點登錄 SSO”)

基於此目標驅動,本文設計三個獨立服務,分別是:

  • 一個授權服務中心(codesheep-server)
  • 客戶端應用1(codesheep-client1)
  • 客戶端應用2(codesheep-client2)

多模塊(Multi-Module)項目搭建

三個應用通過一個多模塊的 Maven項目進行組織,其中項目父 pom中需要加入相關依賴如下:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.0.8.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.spring.platform</groupId>
<artifactId>platform-bom</artifactId>
<version>Cairo-RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

項目結構如下:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

項目結構


授權認證中心搭建

授權認證中心本質就是一個 Spring Boot應用,因此需要完成幾個大步驟:

  • pom中添加依賴
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
  • 項目 yml配置文件:
server:
port: 8085
servlet:
context-path: /uac

即讓授權中心服務啟動在本地的 8085端口之上

  • 創建一個帶指定權限的模擬用戶
@Component
public class SheepUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
if( !"codesheep".equals(s) )
throw new UsernameNotFoundException("用戶" + s + "不存在" );
return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM"));
}
}

這裡創建了一個用戶名為codesheep,密碼 123456的模擬用戶,並且賦予了 普通權限(ROLE_NORMAL)和 中等權限(ROLE_MEDIUM)

  • 認證服務器配置 AuthorizationServerConfig
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 定義了兩個客戶端應用的通行證
clients.inMemory()
.withClient("sheep1")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false)
.and()
.withClient("sheep2")
.secret(new BCryptPasswordEncoder().encode("123456"))
.authorizedGrantTypes("authorization_code", "refresh_token")
.scopes("all")
.autoApprove(false);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(jwtTokenStore()).accessTokenConverter(jwtAccessTokenConverter());
DefaultTokenServices tokenServices = (DefaultTokenServices) endpoints.getDefaultAuthorizationServerTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // 一天有效期
endpoints.tokenServices(tokenServices);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("testKey");
return converter;
}
}

這裡做的最重要的兩件事:一是 定義了兩個客戶端應用的通行證(sheep1和sheep2);二是 配置 token的具體實現方式為 JWT Token。

  • Spring Security安全配置 SpringSecurityConfig
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.formLogin().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider());
}
}

客戶端應用創建和配置

本文創建兩個客戶端應用:codesheep-client1 和codesheep-client2,由於兩者類似,因此只以其一為例進行講解

  • SSO客戶端應用配置類 ClientWebsecurityConfigurer
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class ClientWebsecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}

複雜的東西都交給註解了!

  • application.yml配置
auth-server: http://localhost:8085/uac
server:
port: 8086
security:
oauth2:
client:
client-id: sheep1
client-secret: 123456
user-authorization-uri: ${auth-server}/oauth/authorize
access-token-uri: ${auth-server}/oauth/token
resource:
jwt:
key-uri: ${auth-server}/oauth/token_key

這裡幾項配置都非常重要,都是需要和前面搭建的授權中心進行通信的

  • 創建測試控制器 TestController
@RestController
public class TestController {
@GetMapping("/normal")
@PreAuthorize("hasAuthority('ROLE_NORMAL')")
public String normal( ) {
return "normal permission test success !!!";
}
@GetMapping("/medium")
@PreAuthorize("hasAuthority('ROLE_MEDIUM')")
public String medium() {
return "medium permission test success !!!";
}
@GetMapping("/admin")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
public String admin() {
return "admin permission test success !!!";
}
}

此測試控制器包含三個接口,分別需要三種權限(ROLE_NORMAL、ROLE_MEDIUM、ROLE_ADMIN),待會後文會一一測試看效果


實驗驗證

  • 啟動授權認證中心 codesheep-server(啟動於本地8085端口)
  • 啟動客戶端應用 codesheep-client1 (啟動於本地8086端口)
  • 啟動客戶端應用 codesheep-client2 (啟動於本地8087端口)

首先用瀏覽器訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/normal,由於此時並沒有過用戶登錄認證,因此會自動跳轉到授權中心的登錄認證頁面:http://localhost:8085/uac/login:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動跳轉到授權中心統一登錄頁面

輸入用戶名 codesheep,密碼 123456,即可登錄認證,並進入授權頁面:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

授權頁面

同意授權後,會自動返回之前客戶端的測試接口:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

自動返回客戶端接口並調用成功

此時我們再繼續訪問客戶端1 (codesheep-client1) 的測試接口:localhost:8086/medium,發現已經直接可以調用而無需認證了:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

直接訪問

由於 localhost:8086/normal 和 localhost:8086/medium要求的接口權限,用戶codesheep均具備,所以能順利訪問,接下來再訪問一下更高權限的接口:localhost:8086/admin:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

無權限訪問

好了,訪問客戶端1 (codesheep-client1) 的測試接口到此為止,接下來訪問外掛的客戶端2 (codesheep-client2) 的測試接口:localhost:8087/normal,會發現此時會自動跳到授權頁:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

由於用戶已通過客戶端1登錄過_因此再訪問客戶端2即無需登錄_而是直接跳到授權頁

授權完成之後就可以順利訪問客戶端2 (codesheep-client2) 的接口:

基於Spring Security Oauth2的SSO單點登錄+JWT權限控制實踐

順利訪問客戶端2的接口

這就驗證了單點登錄SSO的功能了!

"

相關推薦

推薦中...