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:
POST請求地址: http://localhost:8080/oauth/token?grant_type=password&client_id=userservice1&client_secret=1234&username=writer&password=writer 獲得以下圖結果:
首先打開瀏覽器訪問地址: http://localhost:8080/oauth/authorize?response_type=code&client_id=userservice3&redirect_uri=https://baidu.com 注意,咱們客戶端跳轉地址須要和數據庫中配置的一致,百度的URL咱們以前已經在數據庫中有配置了,訪問後頁面會跳轉到登陸界面,使用reader:reader登陸:
首先咱們能夠測試一下咱們的安全配置,訪問/hello端點不須要認證能夠匿名訪問:
在以前,咱們使用的是裸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: 能夠看到頁面自動轉到了受權服務器的登陸頁面:
最後,咱們來訪問一下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 github.com/JosephZhu19… ,但願本文對你有用。