首先咱們先來弄清楚這裏的先後端分離指的是什麼?咱們上篇文章已經指出oauth2有四種角色分別是(客戶端、受權服務端和資源服務端和資源全部者),資源服務端和資源全部者是指用戶數據和用戶本身,因此這裏的先後端要麼是客戶端應用要麼是受權服務端那麼究竟是哪一個呢?由於受權服務端已經實現了登陸和受權相關頁面所以咱們只須要改造一下便可,因此這裏的先後端指的就是客戶端應用了。javascript
說到客戶端應用通常有兩種實現方式,一種是傳統的先後臺一體的單體架構項目如:jsp、asp.net等等,另外一種是使用分佈式架構的先後端分離技術,如React、Vue這樣的前端框架作到先後臺相互隔離。css
還有一點咱們這裏實現的單點登陸是使用受權碼(authorization-code)模式不要搞暈最後所有實現了都不知道使用的是OAuth2的哪一種模式就有點搞笑了。html
不論是單體架構仍是分佈式架構,受權服務器都是同樣的,因此咱們先來構建受權服務器及相關代碼實現來開始本章。前端
單體架構上面已經提到了就是先後臺一體的應用,後面會介紹分佈式架構先後端分離應用實現單點登陸,咱們先從單體架構來實現單點登陸來比較他們二者的區別,也請你們自行思考二者的優缺點並在實際項目中作出選擇。java
這個單體架構的單點登陸系統包括下面幾個模塊:mysql
awbeci-sso: 父模塊
awbeci-sso-server: 認證和受權服務端(端口:8900)
awbeci-sso-client1: 單點登陸客戶端示例(端口:8901)
awbeci-sso-client2: 單點登陸客戶端示例(端口:8902) jquery
首先,我想讓初學者瞭解受權服務端的做用以及相關概念,受權服務端主要作這樣幾件事:
1. 受權服務端接受客戶端的訪問(廢話)
2. 客戶端向受權服務端發起請求令牌後,受權服務端首先會驗證是哪一個應用(client_id和client_secret),接着會驗證是哪一個用戶(username和password)並要求用戶受權。注意:這些參數和憑據都是客戶端和用戶給的
3. 受權服務端驗證都經過了,就會根據客戶端傳的redicrect_uri並帶着code跳轉到該連接返回給客戶端
4. 客戶端帶着原先傳遞的參數加上受權服務端給的code再次請求受權服務端,受權服務端接收並再次驗證是哪一個應用,哪一個用戶,哪一個code經過一系列的驗證經過以後就正式返回token給客戶端。 git
如今知道了受權服務端到底作了哪些事,這樣接下來咱們要作的就是去實現這樣一整套流程,若是後面有什麼仍是不清楚能夠回頭再來看看。web
首先咱們先來構建一個maven項目,名稱爲awbeci-sso-server並在pom.xml中添加相關依賴包,以下所示:spring
<modelVersion>4.0.0</modelVersion> <packaging>jar</packaging> <artifactId>awbeci-sso-server</artifactId> <groupId>org.awbeci</groupId> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency><!--熱部署依賴--> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
接着新建相關Package並在下面添加SsoServerApplication類,以下所示:
@SpringBootApplication public class SsoServerApplication { @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } public static void main(String[] args) { SpringApplication.run(SsoServerApplication.class, args); } }
首先添加添加SsoAuthorizationServerConfig類它是繼承自AuthorizationServerConfigurerAdapter類,以下所示:
@Configuration @EnableAuthorizationServer public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private BCryptPasswordEncoder passwordEncoder; @Override public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置客戶端的client_id和client_secret並保存到內存中 clients.inMemory() .withClient("client1") .secret(passwordEncoder.encode("123456")) .redirectUris("http://127.0.0.1:8901/client1/login") .authorizedGrantTypes("authorization_code") .scopes("all") .and() .withClient("client2") .secret(passwordEncoder.encode("123456")) .redirectUris("http://127.0.0.1:8902/client2/login") .authorizedGrantTypes("authorization_code") .scopes("all"); } }
注意:必須設置回調地址redirectUris
,而且格式是http://客戶端IP:端口/login
的格式,不然會報OAuth Error error=」invalid_request」, error_description=」At least one redirect_uri must be registered with the client.」
Spring Security 安全配置。在安全配置類裏咱們配置了:
@Configuration @EnableWebSecurity public class SsoWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; // 用來處理用戶驗證 // 被注入OAuth2Config類中的 endpoints方法中 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(bCryptPasswordEncoder); } @Override protected void configure(HttpSecurity http) throws Exception { http.formLogin() .loginPage("/login") .and() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest() .authenticated() .and().csrf().disable().cors(); } }
接下來配置UserDetailsService,代碼以下所示:
public class SsoUserDetailsService implements UserDetailsService { @Autowired private BCryptPasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 模擬從數據庫獲取用戶信息,實際項目中要從數據庫中獲取用戶信息 return User.withUsername(username) .password(passwordEncoder.encode("123456")) .authorities("ROLE_ADMIN") .build(); } }
這樣就所有配置好了認證和受權的服務端了,下面咱們就來配置客戶端。
客戶端咱們來新建兩個maven項目,固然你也能夠新建2個以上的,下面是新建的client1和client2的pom.xml依賴包,以下:
<modelVersion>4.0.0</modelVersion> <packaging>jar</packaging> <artifactId>awbeci-sso-client2</artifactId> <groupId>org.awbeci</groupId> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency><!--熱部署依賴--> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
接着新建相關Package並在下面添加SsoServerApplication類,以下所示:
@SpringBootApplication // 這裏開啓了單點登陸訪問 @EnableOAuth2Sso public class SsoClient1Application { public static void main(String[] args) { SpringApplication.run(SsoClient1Application.class, args); } }
如今咱們來配置application.yml並設置好客戶端應用以及受權地址和獲取token的地址
server: port: 8901 servlet: context-path: /client1 spring: application: name: client1service thymeleaf: cache: false mode: HTML5 encoding: UTF-8 servlet: content-type: text/html prefix: classpath:/templates/ suffix: .html resources: chain: strategy: content: enabled: true paths: /** security: oauth2: client: client-id: client1 client-secret: 123456 user-authorization-uri: http://127.0.0.1:8900/sso/oauth/authorize access-token-uri: http://127.0.0.1:8900/sso/oauth/token resource: token-info-uri: http://127.0.0.1:8900/sso/oauth/check_token
注意:上面配置中security做用就是當客戶端發現沒有身份認證的時候會自動跳轉到http://127.0.0.1:8900/sso/oauth/authorize去認證用戶,認證成功以後會返回到客戶端,客戶端就能夠經過http://127.0.0.1:8900/sso/oauth/token去自動獲取token,就不須要手動去跳轉和獲取了,這兩個自動操做的過程當中已經作了code的獲取和返回,可是你是感受不到的,這也是單體架構的優點了,而咱們後面作的先後臺分離項目就要手動去操做了。
咱們會在client1和client2項目上新建首頁頁面,這個首頁頁面就是一個簡單的html頁面咱們使用的是thymeleaf來製做這樣的頁面,咱們會在客戶端1(client1)和客戶端2(client2)上面製做相同的這樣的頁面,目的:就是當無論訪問哪一個客戶端頁面當你已經認證了用戶那麼你相互跳轉並訪問就不須要再跑去驗證用戶了,可是若是你沒經過驗證那麼無論你訪問哪一個頁面就要去驗證用戶。就是模擬一個多應用下的驗證和受權操做,若是大家實在想像不出來,能夠想一想淘寶和天貓登陸的操做,咱們就是模擬這樣的一個操做過程。下面是客戶端1和客戶端2的首頁代碼:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <title>client1</title> <meta charset="utf-8"/> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width"/> <link type="text/css" rel="stylesheet" href="https://cdn.bootcss.com/twitter-bootstrap/4.4.1/css/bootstrap.min.css"> <body> <div> 訪問<a href="http://127.0.0.1:8902/client2">客戶端2</a> </div> <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script type="text/javascript" src="https://cdn.bootcss.com/twitter-bootstrap/4.4.1/js/bootstrap.min.js"></script> </body> </html>
記得加上Controller
@RestController public class TestController { @GetMapping("/") public ModelAndView index(){ ModelAndView mv = new ModelAndView(); mv.setViewName("index"); return mv; } }
客戶端Client2的設置同上,這裏就不詳細講了,你們能夠看個人源碼,下面咱們把三個項目運行起來試試。
當我訪問客戶端1client1的時候會直接跳轉到localhost:8900/sso/login,緣由上面已經說過了,下面咱們輸入用戶名和密碼點擊登陸試試。
上面提示你要不要給應用client1受權訪問,選擇Approve(容許)再點擊Authoriza(受權)便可,這樣就會跳轉到你返回過來的redirect_uri連接了。
如今就能夠訪問客戶端1的頁面了,中間已經作了code和token的驗證了(上面已經說過了),用戶是感覺不到的。如今你點擊客戶端2,它會跳轉到客戶端2,由於咱們已經驗證了用戶,因此跳轉到客戶端2是不用再驗證用戶了,咱們點擊試試。
雖然不用再驗證用戶了,可是這裏仍是要你受權,這裏像客戶端同樣選擇便可。
這樣客戶端2也能訪問了,這樣就完成了單體架構的單點登陸了。不過上面的過程有幾個問題須要咱們去解決,我列舉了一下:
一、自動受權
二、使用數據庫保存客戶端和token信息
三、使用jwt生成token
下面咱們就來一個個改造,首先咱們來改造下自動受權,不用每次去點擊了。
改造也很簡單,咱們改下SsoAuthorizationServerConfig的configure(ClientDetailsServiceConfigurer clients)方法,以下所示:
clients.inMemory() .withClient("client1") .secret(passwordEncoder.encode("123456")) .redirectUris("http://127.0.0.1:8901/client1/login") .authorizedGrantTypes("authorization_code") + .autoApprove(true) .scopes("all") .and() .withClient("client2") .secret(passwordEncoder.encode("123456")) .redirectUris("http://127.0.0.1:8902/client2/login") .authorizedGrantTypes("authorization_code") + .autoApprove(true) .scopes("all");
如今你再輸入用戶名和密碼以後就不用點擊受權了,就直接跳轉到客戶端了,你們能夠試試。
下面咱們來改造下代碼使用數據庫保存客戶端信息。
首先咱們要建幾個跟OAuth2相關的表來保存client和token相關信息,以下所示:
CREATE TABLE `oauth_access_token` ( `token_id` varchar(255) DEFAULT NULL, `token` longblob, `authentication_id` varchar(255) DEFAULT NULL, `user_name` varchar(255) DEFAULT NULL, `client_id` varchar(255) DEFAULT NULL, `authentication` longblob, `refresh_token` varchar(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_approvals` ( `userId` varchar(255) DEFAULT NULL, `clientId` varchar(255) DEFAULT NULL, `scope` varchar(255) DEFAULT NULL, `status` varchar(10) DEFAULT NULL, `expiresAt` datetime DEFAULT NULL, `lastModifiedAt` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_client_details` ( `client_id` varchar(255) NOT NULL, `resource_ids` varchar(255) DEFAULT NULL, `client_secret` varchar(255) DEFAULT NULL, `scope` varchar(255) DEFAULT NULL, `authorized_grant_types` varchar(255) DEFAULT NULL, `web_server_redirect_uri` varchar(255) DEFAULT NULL, `authorities` varchar(255) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(255) DEFAULT NULL, `autoapprove` varchar(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_client_token` ( `token_id` varchar(255) DEFAULT NULL, `token` longblob, `authentication_id` varchar(255) DEFAULT NULL, `user_name` varchar(255) DEFAULT NULL, `client_id` varchar(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_code` ( `code` varchar(255) DEFAULT NULL, `authentication` varbinary(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(255) DEFAULT NULL, `token` longblob, `authentication` longblob ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
再添加client1和client2的客戶端信息,如圖所示:
上面的client的密碼你們可使用以下代碼生成:
System.out.println(new BCryptPasswordEncoder().encode("123456"));
接着添加下jdbc和mysql的依賴包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
接着咱們在application.yml裏面添加對mysql的配置
spring: datasource: url: jdbc:mysql://your-mysql-domain/your-database?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false username: your-username password: your-password driver-class-name: com.mysql.jdbc.Driver jpa: show-sql: true
接着改造下SsoAuthorizationServerConfig類代碼
@Configuration @EnableAuthorizationServer public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { + @Autowired + private DataSource dataSource; + @Autowired + private UserDetailsService userDetailsService; @Autowired private BCryptPasswordEncoder passwordEncoder; @Override public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("isAuthenticated()") .checkTokenAccess("isAuthenticated()"); } + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + endpoints + .userDetailsService(userDetailsService) + .tokenStore(tokenStore()); + } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 配置客戶端的client_id和client_secret並保存到內存中 - clients.inMemory() - .withClient("client1") - .secret(passwordEncoder.encode("123456")) - .redirectUris("http://127.0.0.1:8901/client1/login") - .authorizedGrantTypes("authorization_code") - .autoApprove(true) - .scopes("all") - .and() - .withClient("client2") - .secret(passwordEncoder.encode("123456")) - .redirectUris("http://127.0.0.1:8902/client2/login") - .authorizedGrantTypes("authorization_code") - .autoApprove(true) - .scopes("all"); + clients.jdbc(dataSource); } }
這樣就完成了改造,如今咱們再從新訪問客戶端1或者2,輸入用戶名和密碼,再看看數據庫oauth_access_token表裏面的數據,以下所示:
首先咱們添加JWTTokenStoreConfig配置類:
@Configuration public class JWTTokenStoreConfig { // 設置TokenStore爲JwtTokenStore @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } // 在jwt和oauth2服務器之間充當翻譯 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); // 定義將用於簽署令牌的簽名密鑰(自定義 存儲在git上authentication.yml文件) // jwt是不保密的,因此要另外加簽名驗證jwt token // todo:最好不要寫死,複雜點更好 converter.setSigningKey("awbeci"); return converter; } // 設置TokenEnhancer加強器中使用JWTTokenEnhancer加強器 @Bean public TokenEnhancer jwtTokenEnhancer() { return new JWTTokenEnhancer(); } // @Primary做用:若是有多個特定類型bean那麼就使用被@Primary標註的bean類型進行自動注入 // @Bean // @Primary // public DefaultTokenServices tokenServices() { // // 用於從出示給服務的令牌中讀取數據 // DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); // defaultTokenServices.setTokenStore(tokenStore()); // defaultTokenServices.setSupportRefreshToken(true); // return defaultTokenServices; // } }
接着,咱們添加jwt的加強器JWTTokenEnhancer類,做用:擴展jwt內容信息。
// jwt token 擴展器,加進本身的數據內容 public class JWTTokenEnhancer implements TokenEnhancer { // 要進行加強須要覆蓋enhance方法 @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { Map<String, Object> additionalInfo = new HashMap<>(); additionalInfo.put("enhancer_content", "there is enhancer content"); // 全部附加的屬性都放到HashMap中,並設置在傳入該方法的accessToken變量上 ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo); return oAuth2AccessToken; } }
接着,咱們修改SsoAuthorizationServerConfig類來支持jwt
@Configuration @EnableAuthorizationServer public class SsoAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private DataSource dataSource; - @Autowired - private BCryptPasswordEncoder passwordEncoder; @Autowired private UserDetailsService userDetailsService; - @Bean - public TokenStore tokenStore() { - return new JdbcTokenStore(dataSource); - } + @Autowired + private AuthenticationManager authenticationManager; + @Autowired + private TokenStore tokenStore; + // 將JWTTokenStore類中的JwtAccessTokenConverter關聯到OAUTH2 + @Autowired + private JwtAccessTokenConverter jwtAccessTokenConverter; + // 自動將JWTTokenEnhancer裝配到TokenEnhancer類中 + // token加強類,須要添加額外信息內容的就用這個類 + @Autowired + private TokenEnhancer jwtTokenEnhancer; @Override public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess("isAuthenticated()") .checkTokenAccess("isAuthenticated()"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + // 設置jwt簽名和jwt加強器到TokenEnhancerChain + TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); + tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, +jwtAccessTokenConverter)); endpoints.tokenStore(tokenStore) + // 在jwt和oauth2服務器之間充當翻譯(簽名) + .accessTokenConverter(jwtAccessTokenConverter) + // 令牌加強器類:擴展jwt token + .tokenEnhancer(tokenEnhancerChain) //JWT .userDetailsService(userDetailsService) + .authenticationManager(authenticationManager) } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } }
接着,咱們再來改造下客戶端1和2下的application.yml
security: oauth2: client: client-id: client2 client-secret: 123456 user-authorization-uri: http://127.0.0.1:8900/sso/oauth/authorize access-token-uri: http://127.0.0.1:8900/sso/oauth/token + resource: + jwt: + key-uri: http://127.0.0.1:8900/sso/oauth/token_key - resource: - token-info-uri: http://127.0.0.1:8900/sso/oauth/check_token
添加獲取用戶信息Controller
@SpringBootApplication @EnableOAuth2Sso public class SsoClient2Application { + @GetMapping("/user") + public Authentication user(Authentication user) { + return user; + } public static void main(String[] args) { SpringApplication.run(SsoClient2Application.class, args); } }
如今咱們再從新登陸試試,並試着訪問/user。
至此,單體架構的單點登陸就完成了,下節咱們講解分佈式架構下先後端分離項目的單點登陸。
Oauth2受權模式訪問之受權碼模式(authorization_code)訪問
SpringCloud OAuth2實現單點登陸以及OAuth2源碼原理解析
Spring Security OAUTH2 獲取用戶信息
SpringBoot配置屬性之Security
Spring Security OAuth2 配置注意點