Spring Security功能多,組件抽象程度高,配置方式多樣,致使了Spring Security強大且複雜的特性。Spring Security的學習成本幾乎是Spring家族中最高的,Spring Security的精良設計值得咱們學習,可是結合實際複雜的業務場景,咱們不但須要理解Spring Security的擴展方式還須要去理解一些組件的工做原理和流程(不然怎麼去繼承並改寫須要改寫的地方呢?),這又帶來了更高的門檻,所以,在決定使用Spring Security搭建整套安全體系(受權、認證、權限、審計)以前仍是須要考慮一下未來咱們的業務會多複雜,咱們徒手寫一套安全體系來的划算仍是使用Spring Security更好。css
短短的一篇文章不可能覆蓋Spring Security的方方面面,在最近的工做中會比較多接觸OAuth2,所以本文以這個維度來簡單闡述一下若是使用Spring Security搭建一套OAuth2受權&SSO架構。html
OAuth2.0是一套受權體系的開放標準,定義了四大角色:java
其中後三項均可以是獨立的程序,在本文的例子中咱們會爲這三者創建獨立的項目。OAuth2.0標準同時定義了四種受權模式,這裏介紹最經常使用的三種,也是後面會演示的三種(在以後的介紹中令牌=Token,碼=Code,可能會混合表達):mysql
下面,咱們來搭建程序實際體會一下這幾種模式。git
首先來建立一個父POM,內含三個模塊:github
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>me.josephzhu</groupId> <artifactId>springsecurity101</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath/> </parent> <modules> <module>springsecurity101-cloud-oauth2-client</module> <module>springsecurity101-cloud-oauth2-server</module> <module>springsecurity101-cloud-oauth2-userservice</module> </modules> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/libs-milestone</url> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> </project>
而後咱們建立第一個模塊,資源服務器:web
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springsecurity101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springsecurity101-cloud-oauth2-server</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project>
這邊咱們除了使用了Spring Cloud的OAuth2啓動器以外還使用數據訪問、Web等依賴,由於咱們的資源服務器須要使用數據庫來保存客戶端的信息、用戶信息等數據,咱們同時也會使用thymeleaf來稍稍美化一下登陸頁面。
如今咱們來建立一個配置文件application.yml:ajax
server: port: 8080 spring: application: name: oauth2-server datasource: url: jdbc:mysql://localhost:3306/oauth?useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver
能夠看到,咱們會使用oauth數據庫,受權服務器的端口是8080。
數據庫中咱們須要初始化一些表:spring
DDL以下:sql
-- ---------------------------- -- Table structure for authorities -- ---------------------------- DROP TABLE IF EXISTS `authorities`; CREATE TABLE `authorities` ( `username` varchar(50) NOT NULL, `authority` varchar(50) NOT NULL, UNIQUE KEY `ix_auth_username` (`username`,`authority`), CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for oauth_approvals -- ---------------------------- DROP TABLE IF EXISTS `oauth_approvals`; CREATE TABLE `oauth_approvals` ( `userId` varchar(256) DEFAULT NULL, `clientId` varchar(256) DEFAULT NULL, `partnerKey` varchar(32) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `status` varchar(10) DEFAULT NULL, `expiresAt` datetime DEFAULT NULL, `lastModifiedAt` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for oauth_client_details -- ---------------------------- DROP TABLE IF EXISTS `oauth_client_details`; 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(4096) DEFAULT NULL, `autoapprove` varchar(255) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for users -- ---------------------------- DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `enabled` tinyint(1) NOT NULL, PRIMARY KEY (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for oauth_code -- ---------------------------- DROP TABLE IF EXISTS `oauth_code`; CREATE TABLE `oauth_code` ( `code` varchar(255) DEFAULT NULL, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
在以後演示的時候會看到這些表中的數據。這裏能夠看到咱們並無在數據庫中建立相應的表來存放訪問令牌、刷新令牌,這是由於咱們以後的實現會把令牌信息使用JWT來傳輸,不會存放到數據庫中。基本上全部的這些表都是能夠本身擴展的,只須要繼承實現Spring的一些既有類便可,這裏不作展開。
下面,咱們建立一個最核心的類用於配置受權服務器:
package me.josephzhu.springsecurity101.cloud.oauth2.server; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore; import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.sql.DataSource; import java.util.Arrays; @Configuration @EnableAuthorizationServer public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private AuthenticationManager authenticationManager; /** * 代碼1 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } /** * 代碼2 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.checkTokenAccess("permitAll()") .allowFormAuthenticationForClients().passwordEncoder(NoOpPasswordEncoder.getInstance()); } /** * 代碼3 * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers( Arrays.asList(tokenEnhancer(), jwtTokenEnhancer())); endpoints.approvalStore(approvalStore()) .authorizationCodeServices(authorizationCodeServices()) .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); } @Bean public AuthorizationCodeServices authorizationCodeServices() { return new JdbcAuthorizationCodeServices(dataSource); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtTokenEnhancer()); } @Bean public JdbcApprovalStore approvalStore() { return new JdbcApprovalStore(dataSource); } @Bean public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); } @Bean protected JwtAccessTokenConverter jwtTokenEnhancer() { KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray()); JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt")); return converter; } /** * 代碼4 */ @Configuration static class MvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("login").setViewName("login"); } } }
分析下這個類:
針對剛纔的代碼,咱們須要補充一些東西到資源目錄下,首先須要在資源目錄下建立一個templates文件夾而後建立一個login.html登陸模板:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" class="uk-height-1-1"> <head> <meta charset="UTF-8"/> <title>OAuth2 Demo</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/2.26.3/css/uikit.gradient.min.css"/> </head> <body class="uk-height-1-1"> <div class="uk-vertical-align uk-text-center uk-height-1-1"> <div class="uk-vertical-align-middle" style="width: 250px;"> <h1>Login Form</h1> <p class="uk-text-danger" th:if="${param.error}"> 用戶名或密碼錯誤... </p> <form class="uk-panel uk-panel-box uk-form" method="post" th:action="@{/login}"> <div class="uk-form-row"> <input class="uk-width-1-1 uk-form-large" type="text" placeholder="Username" name="username" value="reader"/> </div> <div class="uk-form-row"> <input class="uk-width-1-1 uk-form-large" type="password" placeholder="Password" name="password" value="reader"/> </div> <div class="uk-form-row"> <button class="uk-width-1-1 uk-button uk-button-primary uk-button-large">Login</button> </div> </form> </div> </div> </body> </html>
而後,咱們須要使用keytool工具生成密鑰,把密鑰文件jks保存到目錄下,而後還要導出一個公鑰留做之後使用。剛纔在代碼中咱們還用到了一個自定義的Token加強器,實現以下:
package me.josephzhu.springsecurity101.cloud.oauth2.server; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import java.util.HashMap; import java.util.Map; public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Authentication userAuthentication = authentication.getUserAuthentication(); if (userAuthentication != null) { Object principal = authentication.getUserAuthentication().getPrincipal(); Map<String, Object> additionalInfo = new HashMap<>(); additionalInfo.put("userDetails", principal); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); } return accessToken; } }
這段代碼很是簡單,就是把用戶信息以userDetails這個Key存放到Token中去(若是受權模式是客戶端模式這段代碼無效,由於和用戶不要緊)。這是一個常見需求,默認狀況下Token中只會有用戶名這樣的基本信息,咱們每每須要把有關用戶的更多信息返回給客戶端(在實際應用中你可能會從數據庫或外部服務查詢更多的用戶信息加入到JWT Token中去),這個時候就能夠自定義加強器來豐富Token的內容。
到此受權服務器的核心配置已經完成,如今咱們再來實現一下安全方面的配置:
package me.josephzhu.springsecurity101.cloud.oauth2.server; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import javax.sql.DataSource; @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .passwordEncoder(new BCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login", "/oauth/authorize") .permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage("/login"); } }
這裏咱們主要作了兩個事情:
最後配置一個主程序:
package me.josephzhu.springsecurity101.cloud.oauth2.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class OAuth2ServerApplication { public static void main(String[] args) { SpringApplication.run(OAuth2ServerApplication.class, args); } }
至此,受權服務器的配置完成。
先來建立項目:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springsecurity101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>springsecurity101-cloud-oauth2-userservice</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> </dependencies> </project>
配置及其簡單,聲明資源服務端口8081
server: port: 8081
還記得在資源文件夾下放咱們以前經過密鑰導出的公鑰文件,相似:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR84LFHwnK5GXErnwkmD mPOJl4CSTtYXCqmCtlbF+5qVOosu0YsM2DsrC9O2gun6wVFKkWYiMoBSjsNMSI3Z w5JYgh+ldHvA+MIex2QXfOZx920M1fPUiuUPgmnTFS+Z3lmK3/T6jJnmciUPY1pe h4MXL6YzeI0q4W9xNBBeKT6FDGpduc0FC3OlXHfLbVOThKmAUpAWFDwf9/uUA//l 3PLchmV6VwTcUaaHp5W8Af/GU4lPGZbTAqOxzB9ukisPFuO1DikacPhrOQgdxtqk LciRTa884uQnkFwSguOEUYf3ni8GNRJauIuW0rVXhMOs78pKvCKmo53M0tqeC6ul +QIDAQAB -----END PUBLIC KEY-----
先來建立一個能夠匿名訪問的接口GET /hello:
package me.josephzhu.springsecurity101.cloud.oauth2.userservice; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("hello") public String hello() { return "Hello"; } }
再來建立一個須要登陸+受權才能訪問到的一些接口:
package me.josephzhu.springsecurity101.cloud.oauth2.userservice; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("user") public class UserController { @Autowired private TokenStore tokenStore; @PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')") @GetMapping("name") public String name(OAuth2Authentication authentication) { return authentication.getName(); } @PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')") @GetMapping public OAuth2Authentication read(OAuth2Authentication authentication) { return authentication; } @PreAuthorize("hasAuthority('WRITE')") @PostMapping public Object write(OAuth2Authentication authentication) { OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails(); OAuth2AccessToken accessToken = tokenStore.readAccessToken(details.getTokenValue()); return accessToken.getAdditionalInformation().getOrDefault("userDetails", null); } }
這裏咱們配置了三個接口,而且經過@PreAuthorize在方法執行前進行權限控制:
下面咱們來建立核心的資源服務器配置類:
package me.josephzhu.springsecurity101.cloud.oauth2.userservice; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.util.FileCopyUtils; import java.io.IOException; @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { /** * 代碼1 * @param resources * @throws Exception */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("foo").tokenStore(tokenStore()); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean protected JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); Resource resource = new ClassPathResource("public.cert"); String publicKey = null; try { publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream())); } catch (IOException e) { e.printStackTrace(); } converter.setVerifierKey(publicKey); return converter; } /** * 代碼2 * @param http * @throws Exception */ @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/user/**").authenticated() .anyRequest().permitAll(); } }
這裏咱們幹了四件事情:
咱們想一下,若是受權服務器產生Token的話,資源服務器必須是要有一種辦法來驗證Token的,若是是非JWT的方式,咱們能夠這麼辦:
如今咱們使用的是不落地的JWT方式+非對稱加密,須要經過本地公鑰進行驗證,所以在這裏咱們配置了公鑰的路徑。
最後建立一個啓動類:
package me.josephzhu.springsecurity101.cloud.oauth2.userservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } }
至此,資源服務器配置完成,咱們還在資源服務器中分別建了兩個控制器,用於測試匿名訪問和收到資源服務器權限保護的資源。
如今咱們來看一下如何配置數據庫實現:
首先是oauth_client_details表:
INSERT INTO `oauth_client_details` VALUES ('userservice1', 'foo', '1234', 'FOO', 'password,refresh_token', '', 'READ,WRITE', 7200, NULL, NULL, 'true'); INSERT INTO `oauth_client_details` VALUES ('userservice2', 'foo', '1234', 'FOO', 'client_credentials,refresh_token', '', 'READ,WRITE', 7200, NULL, NULL, 'true'); INSERT INTO `oauth_client_details` VALUES ('userservice3', 'foo', '1234', 'FOO', 'authorization_code,refresh_token', 'https://baidu.com', 'READ,WRITE', 7200, NULL, NULL, 'false');
如以前所說,這裏配置了三條記錄:
而後是authorities表,其中咱們配置了兩條記錄,配置reader用戶具備讀權限,writer用戶具備寫權限:
INSERT INTO `authorities` VALUES ('reader', 'READ'); INSERT INTO `authorities` VALUES ('writer', 'READ,WRITE');
最後是users表配置了兩個用戶的帳戶名和密碼:
INSERT INTO `users` VALUES ('reader', '$2a$04$C6pPJvC1v6.enW6ZZxX.luTdpSI/1gcgTVN7LhvQV6l/AfmzNU/3i', 1); INSERT INTO `users` VALUES ('writer', '$2a$04$M9t2oVs3/VIreBMocOujqOaB/oziWL0SnlWdt8hV4YnlhQrORA0fS', 1);
還記得嗎,密碼咱們使用的是BCryptPasswordEncoder加密(準確說是哈希),可使用一些在線工具進行哈希
POST請求地址:
http://localhost:8080/oauth/token?grant_type=client_credentials&client_id=userservice2&client_secret=1234
以下圖所示,直接能夠拿到Token:
這裏注意到並無提供刷新令牌,刷新令牌用於避免訪問令牌失效後還須要用戶登陸,客戶端模式沒有用戶概念,沒有刷新令牌。咱們把獲得的Token粘貼到https://jwt.io/#debugger-io查看:
若是粘貼進去公鑰的話還能夠看到Token簽名驗證成功:
也能夠試一下,若是咱們的受權服務器沒有allowFormAuthenticationForClients的話,客戶端的憑證須要經過Basic Auth傳而不是Post過去:
還能夠訪問受權服務器來校驗Token:
http://localhost:8080/oauth/check_token?client_id=userservice1&client_secret=1234&token=...
獲得以下結果:
POST請求地址:
http://localhost:8080/oauth/token?grant_type=password&client_id=userservice1&client_secret=1234&username=writer&password=writer
獲得以下圖結果:
再看下Token中的信息:
能夠看到果真包含了咱們TokenEnhancer加入的userDetails自定義信息。
首先打開瀏覽器訪問地址:
http://localhost:8080/oauth/authorize?response_type=code&client_id=userservice3&redirect_uri=https://baidu.com
注意,咱們客戶端跳轉地址須要和數據庫中配置的一致,百度的URL咱們以前已經在數據庫中有配置了,訪問後頁面會跳轉到登陸界面,使用reader:reader登陸:
因爲咱們數據庫中設置的是禁用自動批准受權的模式,因此登陸後來到了批准界面:
點擊贊成後能夠看到數據庫中也會產生受權經過記錄:
而後咱們能夠看到瀏覽器轉到了百度而且提供給了咱們受權碼:
https://www.baidu.com/?code=O8RiCe
數據庫中也記錄了受權碼:
而後POST訪問:http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=userservice3&client_secret=1234&code=O8RiCe&redirect_uri=https://baidu.com
能夠獲得訪問令牌:
雖然userservice3客戶端能夠有READ和WRITE權限,可是咱們登陸的用戶reader只有READ權限,最後拿到的權限只有READ
首先咱們能夠測試一下咱們的安全配置,訪問/hello端點不須要認證能夠匿名訪問:
訪問/user須要身份認證:
無論以哪一種模式拿到訪問令牌,咱們用具備讀權限的訪問令牌GET訪問資源服務器以下地址(請求頭加入Authorization: Bearer XXXXXXXXXX,其中XXXXXXXXXX表明Token):
http://localhost:8081/user/
能夠獲得以下結果:
以POST方式訪問http://localhost:8081/user/顯然是失敗的:
咱們換一個具備讀寫權限的令牌來試試:
果真能夠成功,說明資源服務器的權限控制有效。
在以前,咱們使用的是裸HTTP請求手動的方式來申請和使用令牌,最後咱們來搭建一個OAuth客戶端程序自動實現這個過程:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springsecurity101</artifactId> <groupId>me.josephzhu</groupId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>springsecurity101-cloud-oauth2-client</artifactId> <modelVersion>4.0.0</modelVersion> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> </dependencies> </project>
配置文件以下:
server: port: 8082 servlet: context-path: /ui security: oauth2: client: clientId: userservice3 clientSecret: 1234 accessTokenUri: http://localhost:8080/oauth/token userAuthorizationUri: http://localhost:8080/oauth/authorize scope: FOO resource: jwt: key-value: | -----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwR84LFHwnK5GXErnwkmD mPOJl4CSTtYXCqmCtlbF+5qVOosu0YsM2DsrC9O2gun6wVFKkWYiMoBSjsNMSI3Z w5JYgh+ldHvA+MIex2QXfOZx920M1fPUiuUPgmnTFS+Z3lmK3/T6jJnmciUPY1pe h4MXL6YzeI0q4W9xNBBeKT6FDGpduc0FC3OlXHfLbVOThKmAUpAWFDwf9/uUA//l 3PLchmV6VwTcUaaHp5W8Af/GU4lPGZbTAqOxzB9ukisPFuO1DikacPhrOQgdxtqk LciRTa884uQnkFwSguOEUYf3ni8GNRJauIuW0rVXhMOs78pKvCKmo53M0tqeC6ul +QIDAQAB -----END PUBLIC KEY----- spring: thymeleaf: cache: false #logging: # level: # ROOT: DEBUG
客戶端項目端口8082,幾個須要說明的地方:
首先實現MVC的配置:
package me.josephzhu.springsecurity101.cloud.auth.client; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.request.RequestContextListener; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class WebMvcConfig implements WebMvcConfigurer { @Bean public RequestContextListener requestContextListener() { return new RequestContextListener(); } @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/") .setViewName("forward:/index"); registry.addViewController("/index"); } }
這裏作了兩個事情:
package me.josephzhu.springsecurity101.cloud.auth.client; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @Order(200) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/login**") .permitAll() .anyRequest() .authenticated(); } }
這裏咱們實現的是/路徑和/login路徑容許訪問,其它路徑須要身份認證後才能訪問。
而後咱們來建立一個控制器:
package me.josephzhu.springsecurity101.cloud.auth.client; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; @RestController public class DemoController { @Autowired OAuth2RestTemplate restTemplate; @GetMapping("/securedPage") public ModelAndView securedPage(OAuth2Authentication authentication) { return new ModelAndView("securedPage").addObject("authentication", authentication); } @GetMapping("/remoteCall") public String remoteCall() { ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://localhost:8081/user/name", String.class); return responseEntity.getBody(); } }
這裏能夠看到:
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>Spring Security SSO Client</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"/> </head> <body> <div class="container"> <div class="col-sm-12"> <h1>Spring Security SSO Client</h1> <a class="btn btn-primary" href="securedPage">Login</a> </div> </div> </body> </html>
如今又定義了securedPage頁面,模板以下:
<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>Spring Security SSO Client</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css"/> </head> <body> <div class="container"> <div class="col-sm-12"> <h1>Secured Page</h1> Welcome, <span th:text="${authentication.name}">Name</span> <br/> Your authorities are <span th:text="${authentication.authorities}">authorities</span> </div> </div> </body> </html>
接下去最關鍵的一步是啓用@EnableOAuth2Sso,這個註解包含了@EnableOAuth2Client:
package me.josephzhu.springsecurity101.cloud.auth.client; import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; @Configuration @EnableOAuth2Sso public class OAuthClientConfig { @Bean public OAuth2RestTemplate oauth2RestTemplate(OAuth2ClientContext oAuth2ClientContext, OAuth2ProtectedResourceDetails details) { return new OAuth2RestTemplate(details, oAuth2ClientContext); } }
此外,咱們這裏還定義了OAuth2RestTemplate,網上一些比較老的資料給出的是手動讀取配置文件來實現,最新版本已經能夠自動注入OAuth2ProtectedResourceDetails。
最後是啓動類:
package me.josephzhu.springsecurity101.cloud.auth.client; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class OAuth2ClientApplication { public static void main(String[] args) { SpringApplication.run(OAuth2ClientApplication.class, args); } }
啓動客戶端項目,打開瀏覽器訪問http://localhost:8082/ui/securedPage:
能夠看到頁面自動轉到了受權服務器的登陸頁面:
點擊登陸後出現以下錯誤:
顯然,以前咱們數據庫中配置的redirect_uri是百度首頁,須要包含咱們的客戶端地址,咱們把字段內容修改成4個地址:
https://baidu.com,http://localhost:8082/ui/login,http://localhost:8083/ui/login,http://localhost:8082/ui/remoteCall
刷新頁面,登陸成功:
咱們再啓動另外一個客戶端網站,端口改成8083,而後訪問一樣地址:
能夠看到一樣是登陸狀態,SSO單點登陸測試成功,是否是很方便。
最後,咱們來訪問一下remoteCall接口:
能夠看到輸出了用戶名,對應的資源服務器服務端是:
@PreAuthorize("hasAuthority('READ') or hasAuthority('WRITE')") @GetMapping("name") public String name(OAuth2Authentication authentication) { return authentication.getName(); }
換一個用戶登陸試試:
本文以OAuth 2.0這個維度來小窺了一下Spring Security的功能,介紹了OAuth 2.0的基本概念,體驗了三種經常使用模式,也使用Spring Security實現了OAuth 2.0的三個組件,客戶端、受權服務器和資源服務器,實現了資源服務器的權限控制,最後還使用客戶端測試了一下SSO和OAuth2RestTemplate使用,全部代碼見個人Github https://github.com/JosephZhu1983/SpringSecurity101 ,但願本文對你有用。