概述
關于SSO
網上關于SSO的介紹非常多,舉個例就是,當我們使用瀏覽器登錄了淘寶https://www.taobao.com以后,我們再訪問天貓https://www.tmall.com時就不需要再次登錄。也就是淘寶和天貓之間只需要登錄其中一個業(yè)務系統,另一個就自動登錄
運用場景
一般情況下,一個公司內肯定不止一個業(yè)務系統,公司內相互可信任的系統,我們希望可以實現和淘寶天貓相同的單點登錄效果。多個業(yè)務系統使用同一個認證服務也能簡化開發(fā)
相關知識
對于此文你所要了解的相關知識:
- oauth2.0簡介:http://www.ruanyifeng.com/blog/2019/04/oauth_design.html
- oauth2.0的四種方式:http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html
- JWT:https://zhuanlan.zhihu.com/p/27370773
- Spring Security:http://m.itdecent.cn/p/e22fdeedc9a3
Spring Security Oauth+JWT單點登錄流程

圖中一個認證服務器,兩個客戶端應用,A和B。用戶請求訪問A的服務器資源時會被引導到認證服務器進行授權,經過登錄認證和同意授權后返回授權碼給A,A的服務端收到授權碼后再次向認證服務器請求token,最終返回JWT類型的token,A解析接收到的JWT進行解析獲取到用戶相關信息保存進Spring Security的SecurityContext中實現登錄;此時用戶再次請求訪問B的服務器資源時,B會引導用戶到認證服務器進行請求授權,此時不在需要再次登錄認證,只要授權確認,確認后返回授權碼到B,B再次向認證服務器發(fā)起獲取token請求,認證服務器返回JWT,B對JWT進行解析,保存用戶進Spring Security的SecurityContext中實現登錄
實現流程
- 搭建SSO第三方認證授權服務中心
- 搭建客戶端應用1
- 搭建客戶端應用2
- 測試
前言
本文主要闡述使用Spring Security Oauth+JWT實現單點登錄和做到客戶端的權限控制,不闡述其相關源碼和實現原理
相關版本
Spring boot:2.2.0.RELEASE
實現步驟
一共創(chuàng)建三個服務,一個認證服務,兩個客戶端服務,采取單獨創(chuàng)建三個工程項目的方式,不是三個服務模塊化整合在一起。三個服務都引入相同的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
搭建認證中心服務
- 配置文件
server.servlet.context-path= /server
只配置了服務的根路徑,端口用默認的8080
- 處理用戶信息
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* 這里實際情況應該是根據參數s查詢數據庫用戶數據
*/
return new User(username, bCryptPasswordEncoder.encode("123"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_LOW,ROLE_MID"));
}
}
了解Spring Security的同學應該對這個很了解這個類了(可以看我的上文http://m.itdecent.cn/p/e22fdeedc9a3了解相關),這里讓Spring Security只有輸入密碼123可以經過認證,并且這個用戶擁有ROLE_LOW和ROLE_MID的權限
- 認證中心服務配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter{
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private MyUserDetailService myUserDetailService;
// @Autowired
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("client1")
.secret(passwordEncoder.encode("123"))
.redirectUris("http://localhost:8081/client1/login")
.scopes("all")
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.autoApprove(false)
.and()
.withClient("client2")
.secret(passwordEncoder.encode("123"))
.redirectUris("http://localhost:8082/client2/login")
.scopes("all")
.authorizedGrantTypes("authorization_code", "refresh_token", "password")
.autoApprove(true);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("isAuthenticated()");
}
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("testKey");
return jwtAccessTokenConverter;
}
}
認證中心服務配置類主要做三件事
- @EnableAuthorizationServer注解把這個服務聲明為認證服務
- 配置兩個客戶端相關信息,包含client_id,client_secret,redirect_uri,scope,支持的認證方式(密碼模式,授權碼模式等),以及配置允許自動授權。這里需要注意redirect_uri必須配置否則獲取授權碼時會報至少需要注冊一個回調地址的錯誤,還有配置client_secret時必須用BCryptPasswordEncoder進行編碼一下,否則會出現client_secret錯誤的問題,因為Spring Security Oauth默認情況下會對發(fā)起請求中的client_secret參數進行編碼
- 配置生成的token類型為JWT
- 配置token的安全約束
- Spring Security配置
@Configuration
@EnableWebSecurity
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailService myUserDetailService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@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.userDetailsService(myUserDetailService);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
有學習過Spring Security的同學對這個配置應該也比較熟悉了,主要就是
- 注冊了一個BCryptPasswordEncoder實例
- 配置了Spring Security安全規(guī)則
- 給AuthenticationManagerBuilder配置上自定義的MyUserDetailService
- 注冊一個AuthenticationManager的實例。如果想實現認證服務器支持密碼模式獲取token的話必須在這注冊這個實例,并且在認證服務配置類AuthorizationServerConfig中給AuthorizationServerEndpointsConfigurer配置上此實例,如下的endpoints.authenticationManager(authenticationManager)
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
}
搭建客戶端服務1
- 配置文件
server.port=8081
server.servlet.context-path=/client1
security.oauth2.client.client-id = client1
security.oauth2.client.client-secret= 123
security.oauth2.client.user-authorization-uri = http://localhost:8080/server/oauth/authorize
security.oauth2.client.access-token-uri = http://localhost:8080/server/oauth/token
security.oauth2.resource.jwt.key-uri = http://localhost:8080/server/oauth/token_key
端口設為8081,根路徑設為/client1,請求token的所必要的參數client-id、client-secret,user-authorization-uri是請求授權碼的地址,access-token-uri是請求token的地址,jwt.key-uri是獲取JWT的signKey的地址
- 客戶端配置文件
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class SsoClient1Config extends WebSecurityConfigurerAdapter{
@Override
public void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}
主要三個注解
- @EnableOAuth2Sso讓該服務成為一個客戶端應用,當用戶訪問此服務下的資源時(接口)會引導用戶到認證服務器進行登錄認證,根據配置文件中的相關配置
- @EnableWebSecurity讓Spring Security安全配置生效
- @EnableGlobalMethodSecurity(prePostEnabled = true)讓接口方法上的權限控制生效
- 測試接口
@RestController
public class Client1Controller {
@GetMapping("/high")
@PreAuthorize("hasAuthority('ROLE_HIGH')")
public String normal( ) {
return "high permission";
}
@GetMapping("/mid")
@PreAuthorize("hasAuthority('ROLE_MID')")
public String medium() {
return "mid permission";
}
@GetMapping("/low")
@PreAuthorize("hasAuthority('ROLE_LOW')")
public String admin() {
return "low permission";
}
}
三個接口分別要求不同的用戶權限,ROLE_HIGH、ROLE_MID、ROLE_LOW
搭建客戶端服務2
兩個客戶端其實基本一樣,我就直接上代碼了
- 配置文件
server.port=8082
server.servlet.context-path=/client2
security.oauth2.client.client-id = client2
security.oauth2.client.client-secret= 123
security.oauth2.client.user-authorization-uri = http://localhost:8080/server/oauth/authorize
security.oauth2.client.access-token-uri = http://localhost:8080/server/oauth/token
security.oauth2.resource.jwt.key-uri = http://localhost:8080/server/oauth/token_key
- 配置類
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class SsoClient2Config extends WebSecurityConfigurerAdapter{
@Override
public void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/**").authorizeRequests()
.anyRequest().authenticated();
}
}
- 測試接口
@RestController
public class Client2Controller {
@GetMapping("/high")
@PreAuthorize("hasAuthority('ROLE_HIGH')")
public String normal( ) {
return "high permission";
}
@GetMapping("/mid")
@PreAuthorize("hasAuthority('ROLE_MID')")
public String medium() {
return "mid permission";
}
@GetMapping("/low")
@PreAuthorize("hasAuthority('ROLE_LOW')")
public String admin() {
return "low permission";
}
}
測試
-
單點登錄測試
三個服務全部啟動,直接訪問客戶端1的接口localhost:8081/client1/low
訪問low接口
隨后被引導到登錄頁面
登錄頁
可以發(fā)現原本訪問的接口在localhost:8081/client1下,跳轉到了localhost:8081/server下面。輸入用戶名user,密碼123登錄
請求授權
跳轉到是否同意授權的頁面,點擊Authorize
成功訪問客戶端1接口
這時候意味著用戶已經登錄客戶端1,嘗試訪問客戶端2的接口localhost:8082/client2/low
成功訪問客戶端2接口
這時候沒有讓我們再次登錄就可以直接訪問了(如果你在認證服務的配置中把客戶端2的自動授權設置為false,這里會再次向你詢問是否同意授權,但是不用登錄),這已經實現單點登錄了 -
權限控制測試
上面我們登錄的這個用戶只有ROLE_LOW和ROLE_MID的權限,登錄狀態(tài)下再次嘗試訪localhost:8081/client1/mid
訪問mid成功
可以看出訪問需要ROLE_LOW和ROLE_MID權限的接口都沒問題,再訪問http://localhost:8081/client1/high
訪問high失敗
訪問需要ROLE_HIGH權限的接口返回403,說明我們的權限控制也實現了
工程結構
最后上一下我的工程結構
-
認證中心服務
認證中心 -
客戶端1
客戶端1 -
客戶端2
客戶端2
總結
- 搭建SSO第三方認證授權服務中心
- 搭建客戶端應用1
- 搭建客戶端應用2
- 測試
Github源碼地址:https://github.com/iemi/sso
覺得有用的同學Github給顆星星謝謝啦









