Spring Security
理論
認證
認證是指判斷用戶的身份。比如用戶名密碼登錄,二維碼登錄,指紋認證等。。。
會話
爲了避免用戶每次都需要認證,用戶的信息保存在會話中。常用的有基於session 的認證方式,
基於token的認證方式。
授權
授權是爲了限制用戶對資源的使用。授權需要用戶先認證通過後再進行授權,用戶擁有資源的訪問則正常訪問,沒有權限則拒絕訪問。
授權的數據模型
用戶對哪些資源進行哪些操作。
who -> what -> how
資源:資源分爲兩類。
-
功能資源:系統中的菜單,按鈕,代碼方法等。對於web而言,每個功能資源對於一個URL
-
數據資源:數據資源由資源類型和資源實例組成。
- 資源類型:一張表就是一個資源類型
- 資源實例:表中的具體某一行數據爲資源實例
授權我們至少需要創建5張表:
用戶
角色
權限
用戶角色關係表
角色權限關係表
其中角色只是爲了方便給用戶授權。
最終目的是給每個用戶賦予權限。
6表則是如下關係:
用戶
角色
資源
權限
用戶角色關係表
角色權限關係表
RBAC
-
基於角色的訪問(需要修改代碼)
-
基於資源的訪問(推薦)
Spring Boot 中使用
Spring Security 認證
SpringSecurity默認提供認證頁面
maven 依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
認證配置
- 定義用戶信息服務
@Bean
public UserDetailsService userDetailsService(){
// 基於內存的認證
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("zhangsan").password("123").authorities("ADMIN").build());
manager.createUser(User.withUsername("lizi").password("456").authorities("USER").build());
return manager;
}
- 密碼編碼器
@Bean
public PasswordEncoder passwordEncoder(){
// 無加密(通常不用這個)
// return NoOpPasswordEncoder.getInstance();
// BCryp加密
return new BCryptPasswordEncoder();
}
- 安全攔截機制(相當於攔截器)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 對請求進行驗證
.antMatchers("/role/**").permitAll() // 開放授權,不攔截
.antMatchers("/admin/**").hasAuthority("ADMIN") // 必須有ADMIN權限
.antMatchers("/user/**").hasAnyAuthority("USER", "ADMIN") //有任意一種權限 (當然這裏也可以使用角色控制資源訪問但是不推薦)
.anyRequest() //任意請求(這裏主要指方法)
.authenticated() //// 需要身份認證
.and() //表示一個配置的結束
.formLogin().permitAll() //開啓SpringSecurity內置的表單登錄,會提供一個/login接口
.and()
.logout().permitAll() //開啓SpringSecurity內置的退出登錄,會爲我們提供一個/logout接口
.and()
.csrf().disable(); //關閉csrf跨站僞造請求
}
認證流程
修改爲數據庫認證配置
之前的基於內存的認證 去除 即 去除之前的UserDetailsService 配置
@Service
public class UserDetailService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
* 根據賬號查詢用戶信息
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 從數據獲取賬號信息
UserDto userDto = userRepository.getUserByUserName(username);
if (userDto == null) {
return null;
}
UserDetails userDetails = User.withUsername(userDto.getUsername())
.password(userDto.getPassword()).authorities("ADMIN").build();
return userDetails;
}
}
Security 授權流程
底層原理使用投票的形式。
Spring Security會話
獲取用戶身份
@GetMapping("/info")
public String info(){
String username = null;
// 當前認證通過的用戶身份
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Object principal = authentication.getPrincipal();
if (principal != null) {
username = "匿名";
}
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
}
return username;
}
控制會話
我們可以通過一下選項準確控制會話何時創建以與spring security 的交互方式
機制 | 描述 |
---|---|
always | 如果沒有session存在就創建一個 |
ifRequired | 如果需要就創建一個Session(默認) 登錄時 |
never | Spring Security將不會創建session,但如果應用中其他地方創建了session,那麼Spring Security 將會使用它 |
stateless | Spring Security 將絕對不會 創建session,也不使用session |
配置方式:
在Spring Security 的WebSecurityConfigurerAdapter
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
自定義頁面
自定義login頁面
@Override
protected void configure(HttpSecurity http) throws Exception {
// 自定義登錄
http.formLogin().loginPage("/login-view")
.loginProcessingUrl("/login")
.successForwardUrl("/login-success")
.permitAll();
}
自定義logout頁面
@Override
protected void configure(HttpSecurity http) throws Exception {
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login-view?logout=true");
// 配置session
}
Spring Security 授權
- 建表SQL
CREATE DATABASE /*!32312 IF NOT EXISTS*/`security` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci */ /*!80016 DEFAULT ENCRYPTION='N' */;
USE `security`;
/*Table structure for table `t_permission` */
DROP TABLE IF EXISTS `t_permission`;
CREATE TABLE `t_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`code` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_role` */
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_name` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
`insert_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`status` int(11) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_role_permission` */
DROP TABLE IF EXISTS `t_role_permission`;
CREATE TABLE `t_role_permission` (
`role_id` bigint(20) NOT NULL,
`permission_id` bigint(20) NOT NULL,
PRIMARY KEY (`role_id`,`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_user` */
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` char(64) NOT NULL,
`password` char(64) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`mobile` char(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
/*Table structure for table `t_user_role` */
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`user_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
- 獲取權限SQL
SELECT * FROM t_permission WHERE id IN (
SELECT permission_id FROM t_role_permission WHERE role_id IN (
SELECT role_id FROM t_user_role WHERE user_id = (
SELECT id FROM t_user WHERE username = 'zhangsan'
)
)
)
# 或者使用下面的SQL
SELECT * FROM t_permission AS p,t_role_permission AS rp,t_role AS r
WHERE r.id=rp.role_id AND rp.permission_id=p.id AND r.id
IN
(SELECT r.id FROM t_user AS u,t_role AS r,t_user_role AS ur
WHERE u.username ='zhangsan' AND u.id=ur.user_id AND ur.role_id=r.id);
web授權(url 攔截授權)
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // 對請求進行驗證
.antMatchers("/role/**").permitAll() // 開放授權
.antMatchers("/admin/**").hasAuthority("ADMIN") // 必須有ADMIN權限
.antMatchers("/user/**").hasAnyAuthority("USER", "ADMIN") //有任意一種權限
.antMatchers("/xx/**").hasAnyRole("TK"); // 包含這個角色
}
HttpSecurity 常用方法及說明
方法 | 說明 |
---|---|
openidLogin() | 用於基於 OpenId 的驗證 |
headers() | 將安全標頭添加到響應 |
cors() | 配置跨域資源共享( CORS ) |
sessionManagement() | 允許配置會話管理 |
portMapper() | 允許配置一個PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的對象使用 PortMapper 從 HTTP 重定向到 HTTPS 或者從 HTTPS 重定向到 HTTP。默認情況下,Spring Security使用一個PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443 |
jee() | 配置基於容器的預認證。 在這種情況下,認證由Servlet容器管理 |
x509() | 配置基於x509的認證 |
rememberMe | 允許配置“記住我”的驗證 |
authorizeRequests() | 允許基於使用HttpServletRequest限制訪問 |
requestCache() | 允許配置請求緩存 |
exceptionHandling() | 允許配置錯誤處理 |
securityContext() | 在HttpServletRequests之間的SecurityContextHolder上設置SecurityContext的管理。 當使用WebSecurityConfigurerAdapter時,這將自動應用 |
servletApi() | 將HttpServletRequest方法與在其上找到的值集成到SecurityContext中。 當使用WebSecurityConfigurerAdapter時,這將自動應用 |
csrf() | 添加 CSRF 支持,使用WebSecurityConfigurerAdapter時,默認啓用 |
logout() | 添加退出登錄支持。當使用WebSecurityConfigurerAdapter時,這將自動應用。默認情況是,訪問URL”/ logout”,使HTTP Session無效來清除用戶,清除已配置的任何#rememberMe()身份驗證,清除SecurityContextHolder,然後重定向到”/login?success” |
anonymous() | 允許配置匿名用戶的表示方法。 當與WebSecurityConfigurerAdapter結合使用時,這將自動應用。 默認情況下,匿名用戶將使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,幷包含角色 “ROLE_ANONYMOUS” |
formLogin() | 指定支持基於表單的身份驗證。如果未指定FormLoginConfigurer#loginPage(String),則將生成默認登錄頁面 |
oauth2Login() | 根據外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份驗證 |
requiresChannel() | 配置通道安全。爲了使該配置有用,必須提供至少一個到所需信道的映射 |
httpBasic() | 配置 Http Basic 驗證 |
addFilterAt() | 在指定的Filter類的位置添加過濾器 |
方法授權(方法攔截授權)
基於方法的授權本質上是使用AOP的方法進行授權。所以我們可以在方法之前授權或者在方法之後授權。
- @PreAuthorize(hasAuthority(權限名)) | @PreAuthorize(hasAnyAuthority(權限名...)) 方法之前
- @PostAuthorize(hasAuthority(權限名)) |@PostAuthorize(hasAnyAuthority(權限名..)) 方法之後
使用方式
配置註解
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
Spring Boot項目地址 https://github.com/yusonghu/spring-security-demo
Spring Cloud Security OAuth2
介紹
Spring Security OAuth2 是對OAuth2的一種實現,OAuth2的服務提供方包括兩個服務:
- 授權服務(認證服務)
- 資源服務
使用Spring Security OAuth2的時候可以選擇他們在同一個應用中實現,也可以選擇建立使用同一個授權服務的多個資源服務。
授權服務
AuthorizationEndpoint
服務於認證請求:默認URL:/oauth/authorizeTokenEndpoint
服務於訪問令牌請求:默認URL:/oauth/tokenOAuth2AuthenticationProcessingFilter
用來對請求給出的身份令牌解析鑑權
創建一個簡單的微服務
目錄模塊如下圖:
編寫POM.xml
- 父工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>org.example</groupId>
<artifactId>spring-security-oauth2-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>spring-security-oauth2-uaa</module>
<module>spring-security-oauth2-order</module>
<module>spring-security-oauth2-gateway</module>
<module>spring-security-oauth2-discovery</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.name}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<!--<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
</configuration>
</plugin>
</plugins>
</build>
</project>
- 授權認證服務(uaa)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-uaa</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</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-freemarker</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
- 訂單服務(order)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-order</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
- 網關(gateway)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-gateway</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
- 服務發現(discovery)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-security-oauth2-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-security-oauth2-discovery</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
</project>
授權服務器配置
@Configuration
@EnableAuthorizationServer
public class Authorization extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 使用內存的方式
clients.inMemory() // 使用內存的方式
.withClient("phone") // 客戶端id
.secret(new BCryptPasswordEncoder().encode("secret")) // 客戶端密鑰
.resourceIds("res") // 資源列表
.authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 該client允許的授權類型authorization_code,password,refresh_token,implicit,client_credentials
.scopes("all") // 授權允許範圍
.autoApprove(false) // false 跳轉到授權頁面 true 直接發令牌
.redirectUris("http://www.baidu.com"); //驗證回調地址
}
}
管理令牌
// 令牌存儲策略
// 注入bean
@Bean
public TokenStore tokenStore(){
// 基於內存的token
return new InMemoryTokenStore();
}
@Autowired
private TokenStore tokenStore;
@Autowired
private ClientDetailsService clientDetailsService;
// 令牌訪問服務
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService);
defaultTokenServices.setSupportRefreshToken(true); // 是否產生刷新令牌
defaultTokenServices.setAccessTokenValiditySeconds(7200); // 令牌有效期默認兩小時
defaultTokenServices.setRefreshTokenValiditySeconds(2592000); // 刷新令牌默認有效期3天
defaultTokenServices.setTokenStore(tokenStore); // 令牌存儲策略
return defaultTokenServices;
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthorizationCodeServices authorizationCodeServices;
// 令牌訪問端點
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure(endpoints);
endpoints.authenticationManager(authenticationManager) // 密碼模式
.authorizationCodeServices(authorizationCodeServices) // 授權碼模式
.tokenServices(tokenServices()) // 令牌管理服務
.allowedTokenEndpointRequestMethods(HttpMethod.POST); // 允許POST請求
}
// 令牌訪問安全策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure(security);
security.tokenKeyAccess("permitAll()") // /oauth/token_key 公開
.checkTokenAccess("permitAll()") // /oauth/check_token 公開
.allowFormAuthenticationForClients(); //允許表單提交
}
- Spring Security OAuth2 默認令牌端點
/oauth/authorize
授權端點/oauth/token
令牌端點/oauth/error
授權服務錯誤信息端點/oauth/confirm_access
用戶確認授權提交端點/oauth/check_token
用於資源服務訪問的令牌解析端點/oauth/token_key
提供公有密匙的端點,如果使用JWT的話
之後可以把之前spring boot 的授權給拷貝過來。
授權服務配置總結:授權服務配置分成三大塊,可以關聯記憶。
既然要完成認證,它首先得知道客戶端信息從哪兒讀取,因此要進行客戶端詳情配置。
既然要頒發token,那必須得定義token的相關endpoint,以及token如何存取,以及客戶端支持哪些類型的
token。
既然暴露除了一些endpoint,那對這些endpoint可以定義一些安全上的約束等。
授權碼模式
介紹
- 資源擁有者打開客戶端,客戶端要求資源擁有者授權,重定向到授權服務器。
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
參數列表如下:client_id
:客戶端准入標識response_type
:授權碼模式固定爲codescope
:客戶端權限redirect_uri
:跳轉uri,當授權碼申請成功後會跳轉到此地址,並在後邊帶上code參數(授權碼)。
- 瀏覽器出現向授權服務器授權頁面,之後將用戶同意授權
- 授權服務器將授權碼(AuthorizationCode)轉經瀏覽器發送給client(通過redirect_uri)。
- 客戶端拿着授權碼向授權服務器索要訪問access_token,請求如下:
-
獲取授權碼url
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
-
獲取token(POST)
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
PS:授權碼模式拿到的授權碼獲取一次token之後授權碼失效。
簡化模式
- 資源擁有者打開客戶端,客戶端要求資源擁有者給予授權,它將瀏覽器被重定向到授權服務器,重定向時會附加客戶端的身份信息
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
參數列表如下:
-
client_id:客戶端准入標識。
-
response_type:授權碼模式固定爲code。
-
scope:客戶端權限。
-
redirect_uri:跳轉uri,當授權碼申請成功後會跳轉到此地址,並在後邊帶上code參數(授權碼)。
- 瀏覽器出現向授權服務器授權頁面,之後將用戶同意授權。
- 授權服務器將授權碼(AuthorizationCode)轉經瀏覽器發送給client(通過redirect_uri)。
- 客戶端拿着授權碼向授權服務器索要訪問access_token
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com
PS: 一般來說,簡化模式用於沒有服務端的第三方單頁面應用,因爲沒有服務端,所以無法接收授權碼。
密碼模式
- 資源擁有者將用戶名、密碼發送給客戶端
- 客戶端拿着資源擁有者的用戶名、密碼向授權服務器請求令牌(access_token)
uaa/oauth/token?%20client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123](http://uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123)
參數列表如下:
-
client_id:客戶端准入標識。
-
client_secret:客戶端祕鑰。
-
grant_type:授權類型,填寫password表示密碼模式
-
username:資源擁有者用戶名。
-
password:資源擁有者密碼。
- 授權服務器將令牌(access_token)發送給client
PS:這種模式十分簡單,但是卻意味着直接將用戶敏感信息泄漏給了client,因此這就說明這種模式只能用於client是我 們自己開發的情況下。因此密碼模式一般用於我們自己開發的,第一方原生App或第一方單頁面應用。
客戶端模式
- 客戶端向授權服務器發送自己的身份信息,並請求令牌(access_token)
- 確認客戶端身份無誤後,將令牌(access_token)發送給client
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
參數列表如下:
-
client_id:客戶端准入標識。
-
client_secret:客戶端祕鑰。
-
grant_type:授權類型,填寫client_credentials表示客戶端模式
PS: 這種模式是最方便但最不安全的模式。因此這就要求我們對client完全的信任,而client本身也是安全的。因
此這種模式一般用來提供給我們完全信任的服務器端服務。比如,合作方系統對接,拉取一組用戶信息。
添加資源服務
資源服務配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
// 資源ID (這個得與授權服務得客戶端得ResourceId 一致)
public static final String RESOURCE_ID = "res1";
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.resourceId(RESOURCE_ID) //資源ID
.tokenServices(tokenService()) // 驗證令牌服務
.stateless(true); // 無狀態
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests().antMatchers("/**")
.access("#oauth2.hasAnyScope('all')") // 這個scope是授權服務器得授的授權範圍
.and().csrf().disable() // 禁用csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不啓用session
}
// 驗證token
@Bean
public ResourceServerTokenServices tokenService() {
// 遠程服務請求授權服務器校驗token,必須指定校驗token、client_id、client_secret
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
remoteTokenServices.setClientId("phone"); // client_id
remoteTokenServices.setClientSecret("secret"); // client_secret
return remoteTokenServices;
}
}
資源服務安全攔截
這個這個配置就是服務內部的權限管理
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//安全攔截機制(最重要)
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable().authorizeRequests()
.anyRequest().permitAll();
}
}
JWT 令牌
介紹
JSON Web Token(JWT) 是一個開放的行業標準,它定義了一種簡潔的、自包含的協議格式,用於在通信雙方傳遞JSON對象。
標準:https://tools.ietf.org/html/rfc7519
結構
- Header 頭部包括令牌的類型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
例如:
{ "alg": "HS256", "typ": "JWT" }
- Payload 第二部分是負載,內容也是一個json對象,它是存放有效信息的地方,它可以存放jwt提供的現成字段,比 如:iss(簽發者),exp(過期時間戳), sub(面向的用戶)等,也可自定義字段。
例如:
{ "sub": "1234567890", "name": "456", "admin": true }
- Signature 第三部分是簽名,此部分用於防止jwt內容被篡改。
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
base64UrlEncode(header):jwt令牌的第一部分。
base64UrlEncode(payload):jwt令牌的第二部分。
secret:簽名所使用的密鑰。
將授權服務器頒發JWT令牌
- 修改存儲方式
private String SINGING_KEY = "SIGIN_UAA";
@Bean
public TokenStore tokenStore() {
// JWT 令牌存儲方案
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(SINGING_KEY); // 對稱密鑰,資源服務器使用該密鑰來驗證
return converter;
}
- 定義令牌服務
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
// 令牌訪問服務
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setClientDetailsService(clientDetailsService);
defaultTokenServices.setSupportRefreshToken(true); // 是否產生刷新令牌
defaultTokenServices.setAccessTokenValiditySeconds(7200); // 令牌有效期默認兩小時
defaultTokenServices.setRefreshTokenValiditySeconds(2592000); // 刷新令牌默認有效期3天
defaultTokenServices.setTokenStore(tokenStore); // 令牌存儲策略
// 設置令牌增強
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
return defaultTokenServices;
}
-
資源服務修改爲本地校驗
- 拷貝一份TokenConfig到資源服務中校驗
@Configuration public class TokenConfig { private String SINGING_KEY = "SIGIN_UAA"; @Bean public TokenStore tokenStore() { // JWT 令牌存儲方案 return new JwtTokenStore(accessTokenConverter()); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(SINGING_KEY); // 對稱密鑰,資源服務器使用該密鑰來驗證 return converter; } }
- 將遠程校驗設置校驗服務爲本地校驗
@Autowired private TokenStore tokenStore; @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); resources.resourceId(RESOURCE_ID) //資源ID // .tokenServices(tokenService()) // 遠程驗證令牌服務 .tokenStore(tokenStore) // 本地驗證令牌服務 .stateless(true); // 無狀態 }
將客戶端信息接入到數據庫中
建表
- 接入客戶端信息
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客戶端標 識',
`resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '接入資源列表',
`client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '客戶端祕鑰',
`scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`archived` tinyint(4) DEFAULT NULL,
`trusted` tinyint(4) DEFAULT NULL,
`autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='接入客戶端信息';
/*Data for the table `oauth_client_details` */
insert into `oauth_client_details`(`client_id`,`resource_ids`,`client_secret`,`scope`,`authorized_grant_types`,`web_server_redirect_uri`,`authorities`,`access_token_validity`,`refresh_token_validity`,`additional_information`,`create_time`,`archived`,`trusted`,`autoapprove`) values ('pc','res2','$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm','ROLE_API','client_credentials,password,authorization_code,implicit,refresh_token','http://www.baidu.com',NULL,31536000,2592000,NULL,'2020-07-31 16:18:54',0,0,'false'),('phone','res1','$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm','ROLE_ADMIN,ROLE_USER,ROLE_API','client_credentials,password,authorization_code,implicit,refresh_token','http://www.baidu.com',NULL,7200,259200,NULL,'2020-07-31 16:18:47',0,0,'false');
- 存儲授權碼錶
CREATE TABLE `oauth_code` (
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`authentication` blob,
KEY `code_index` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
- 官方給出的表SQL
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails` (
`appId` varchar(128) NOT NULL,
`resourceIds` varchar(256) DEFAULT NULL,
`appSecret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`grantTypes` varchar(256) DEFAULT NULL,
`redirectUrl` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additionalInformation` varchar(4096) DEFAULT NULL,
`autoApproveScopes` varchar(256) DEFAULT NULL,
PRIMARY KEY (`appId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_access_token` */
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
`authentication` blob,
`refresh_token` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_approvals` */
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals` (
`userId` varchar(256) DEFAULT NULL,
`clientId` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`status` varchar(10) DEFAULT NULL,
`expiresAt` timestamp NULL DEFAULT NULL,
`lastModifiedAt` timestamp NULL DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_client_details` */
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(128) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_client_token` */
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication_id` varchar(128) NOT NULL,
`user_name` varchar(256) DEFAULT NULL,
`client_id` varchar(256) DEFAULT NULL,
PRIMARY KEY (`authentication_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_code` */
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(256) DEFAULT NULL,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `oauth_refresh_token` */
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token` (
`token_id` varchar(256) DEFAULT NULL,
`token` blob,
`authentication` blob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*Table structure for table `tb_permission` */
DROP TABLE IF EXISTS `tb_permission`;
CREATE TABLE `tb_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父權限',
`name` varchar(64) NOT NULL COMMENT '權限名稱',
`enname` varchar(64) NOT NULL COMMENT '權限英文名稱',
`url` varchar(255) NOT NULL COMMENT '授權路徑',
`description` varchar(200) DEFAULT NULL COMMENT '備註',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8 COMMENT='權限表';
/*Table structure for table `tb_role` */
DROP TABLE IF EXISTS `tb_role`;
CREATE TABLE `tb_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) DEFAULT NULL COMMENT '父角色',
`name` varchar(64) NOT NULL COMMENT '角色名稱',
`enname` varchar(64) NOT NULL COMMENT '角色英文名稱',
`description` varchar(200) DEFAULT NULL COMMENT '備註',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='角色表';
/*Table structure for table `tb_role_permission` */
DROP TABLE IF EXISTS `tb_role_permission`;
CREATE TABLE `tb_role_permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
`permission_id` bigint(20) NOT NULL COMMENT '權限 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COMMENT='角色權限表';
/*Table structure for table `tb_user` */
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '用戶名',
`password` varchar(64) NOT NULL COMMENT '密碼,加密存儲',
`phone` varchar(20) DEFAULT NULL COMMENT '註冊手機號',
`email` varchar(50) DEFAULT NULL COMMENT '註冊郵箱',
`created` datetime NOT NULL,
`updated` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`) USING BTREE,
UNIQUE KEY `phone` (`phone`) USING BTREE,
UNIQUE KEY `email` (`email`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用戶表';
/*Table structure for table `tb_user_role` */
DROP TABLE IF EXISTS `tb_user_role`;
CREATE TABLE `tb_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用戶 ID',
`role_id` bigint(20) NOT NULL COMMENT '角色 ID',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=38 DEFAULT CHARSET=utf8 COMMENT='用戶角色表';
ClientDetailService和AuthorizationCodeServices從數據庫中讀取
// 將客戶端信息從數據庫中來
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
// 使用數據庫的方式
clients.withClientDetails(clientDetailsService);
}
將授權碼存入數據庫
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource){
// 設置授權碼模式的授權碼如何存取
return new JdbcAuthorizationCodeServices(dataSource);
}
接入網關
網關資源服務配置
@Configuration
public class ResourceServerConfig {
// 資源ID (這個得與授權服務得客戶端得ResourceId 一致)
public static final String RESOURCE_ID = "res1";
/**
* Uaa資源配置
*/
@Configuration
@EnableResourceServer
public class UaaServerConfig extends ResourceServerConfigurerAdapter{
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.resourceId(RESOURCE_ID) //資源ID
.tokenStore(tokenStore) // 本地驗證令牌服務
.stateless(true); // 無狀態
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
// 將uaa全部放行
http.authorizeRequests().antMatchers("/uaa/**")
.permitAll()
.and().csrf().disable() // 禁用csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 無狀態
}
}
/**
* Order資源配置
*/
@Configuration
@EnableResourceServer
public class OrderServerConfig extends ResourceServerConfigurerAdapter{
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.resourceId(RESOURCE_ID) //資源ID
.tokenStore(tokenStore) // 本地驗證令牌服務
.stateless(true); // 無狀態
}
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
// 將uaa全部放行
http.authorizeRequests().antMatchers("/order/**")
// order路徑需要ROLE_API 權限
.access("#oauth2.hasAnyScope('ROLE_API')") // 這個scope是授權服務器得授的授權範圍
.and().csrf().disable() // 禁用csrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 無狀態
}
}
}
PS:記得把TokenConfig和WebSecurityConfig拷貝過來,記得將所有路徑放行
網關轉發token並解析jwt
- 添加網關過濾器
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof OAuth2Authentication)) {
return null;
}
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
// 獲取當前用戶身份信息
String principal = oAuth2Authentication.getName();
// 獲取當前用戶權限信息
List<String> authorities = new ArrayList<>();
oAuth2Authentication.getAuthorities().stream().forEach(v -> authorities.add(v.getAuthority()));
// 把身份和權限信息放在json中,加入http的header中
OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
Map<String,Object> jsonToken = new HashMap<>(requestParameters);
if (oAuth2Authentication != null) {
jsonToken.put("principal",principal);
jsonToken.put("authorities",authorities);
}
currentContext.addZuulRequestHeader("json-token", Base64.encode(JSON.toJSONString(jsonToken)));
// 轉發給微服務
return null;
}
}
- 添加zuul配置
@Configuration
public class ZuulConfig {
@Bean
public AuthFilter preFileter() {
return new AuthFilter();
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(18000L);
source.registerCorsConfiguration("/**", config);
CorsFilter corsFilter = new CorsFilter(source);
FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}
- 給資源解析從網關解析出來的token放進去。
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
// 解析頭重的token
String token = httpServletRequest.getHeader("json-token");
if (token != null) {
String tokenJSON = Base64.decodeStr(token);
// 將token轉成json對象
JSONObject jsonObject = JSON.parseObject(tokenJSON);
// 用戶身份信息
String principal = jsonObject.getString("principal");
UserDTO userDTO = JSON.parseObject(principal, UserDTO.class);
// 用戶權限
JSONArray jsonArray = jsonObject.getJSONArray("authorities");
String[] authorities = jsonArray.toArray(new String[jsonArray.size()]);
// 將用戶信息和權限填充到用戶身份token對象重
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
// 創建spring security detail
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
// 將authenticationToken填充到安全上下文
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
- 在資源中獲取用戶信息。
UserDTO userDTO = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
項目地址:https://github.com/yusonghu/spring-security-oauth2-demo