Spring boot 學習 ---- Spring Security

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的服務提供方包括兩個服務:

  1. 授權服務(認證服務)
  2. 資源服務

使用Spring Security OAuth2的時候可以選擇他們在同一個應用中實現,也可以選擇建立使用同一個授權服務的多個資源服務。

授權服務

  • AuthorizationEndpoint 服務於認證請求:默認URL:/oauth/authorize
  • TokenEndpoint 服務於訪問令牌請求:默認URL:/oauth/token
  • OAuth2AuthenticationProcessingFilter 用來對請求給出的身份令牌解析鑑權

創建一個簡單的微服務

目錄模塊如下圖:

編寫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可以定義一些安全上的約束等。

授權碼模式

介紹

  1. 資源擁有者打開客戶端,客戶端要求資源擁有者授權,重定向到授權服務器。/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參數(授權碼)。
  2. 瀏覽器出現向授權服務器授權頁面,之後將用戶同意授權
  3. 授權服務器將授權碼(AuthorizationCode)轉經瀏覽器發送給client(通過redirect_uri)。
  4. 客戶端拿着授權碼向授權服務器索要訪問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之後授權碼失效。

簡化模式

  1. 資源擁有者打開客戶端,客戶端要求資源擁有者給予授權,它將瀏覽器被重定向到授權服務器,重定向時會附加客戶端的身份信息

/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參數(授權碼)。

  1. 瀏覽器出現向授權服務器授權頁面,之後將用戶同意授權。
  2. 授權服務器將授權碼(AuthorizationCode)轉經瀏覽器發送給client(通過redirect_uri)。
  3. 客戶端拿着授權碼向授權服務器索要訪問access_token

/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://www.baidu.com

PS: 一般來說,簡化模式用於沒有服務端的第三方單頁面應用,因爲沒有服務端,所以無法接收授權碼。

密碼模式

  1. 資源擁有者將用戶名、密碼發送給客戶端
  2. 客戶端拿着資源擁有者的用戶名、密碼向授權服務器請求令牌(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:資源擁有者密碼。

  1. 授權服務器將令牌(access_token)發送給client

PS:這種模式十分簡單,但是卻意味着直接將用戶敏感信息泄漏給了client,因此這就說明這種模式只能用於client是我 們自己開發的情況下。因此密碼模式一般用於我們自己開發的,第一方原生App或第一方單頁面應用。

客戶端模式

  1. 客戶端向授權服務器發送自己的身份信息,並請求令牌(access_token)
  2. 確認客戶端身份無誤後,將令牌(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://jwt.io/

標準: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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章