接上篇文章,在這個流程中,PostMan可以代表客戶端應用,訂單服務是資源服務器,唯一缺少的是 認證服務器 ,下面來搭建認證服務器
項目結構:
Pom.xml : DependencyManager 引入SpringCloud的配置,Dependency引入 spring-cloud-starter-oauth2
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.nb.security</groupId> <artifactId>nb-server-auth</artifactId> <version>0.0.1-SNAPSHOT</version> <properties> <mybatis-plus.version>3.1.2</mybatis-plus.version> <java.version>1.8</java.version> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <mybatis-plus.version>3.1.2</mybatis-plus.version> <druid.version>1.1.17</druid.version> <jwt.version>0.9.1</jwt.version> <commons.version>2.6</commons.version> <aliyun-java-sdk-core.version>3.2.3</aliyun-java-sdk-core.version> <aliyun-java-sdk-dysmsapi.version>1.0.0</aliyun-java-sdk-dysmsapi.version> <aliyun.oss.version>3.6.0</aliyun.oss.version> <qc.cos.version>5.6.5</qc.cos.version> </properties> <dependencyManagement> <dependencies> <dependency> <!-- Import dependency management from Spring Boot --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.1.6.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <!--集成mybatisplus--> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.1</version> </dependency> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.29</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!--mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> <scope>runtime</scope> </dependency> <!--commons-lang3--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> </dependencies> <build> <plugins> <!--指定JDK編譯版本 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- 打包跳過測試 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
application.yml :
server: port: 9090 spring: application: name: auth-server datasource: url: jdbc:mysql://localhost:3306/db_oauth?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8 username: root password: root driver-class-name: com.mysql.jdbc.Driver #mybatis plus 設置 mybatis-plus: mapper-locations: classpath*:mapper/*Mapper.xml global-config: # 關閉MP3.0自帶的banner banner: false db-config: #主鍵類型 0:"數據庫ID自增",1:"該類型為未設置主鍵類型", 2:"用戶輸入ID",3:"全局唯一ID (數字類型唯一ID)", 4:"全局唯一ID UUID",5:"字符串全局唯一ID (idWorker 的字符串表示)"; id-type: 0 # 默認數據庫表下划線命名 table-underline: true configuration: # 這個配置會將執行的sql打印出來,在開發或測試的時候可以用 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
下面開始新建認證服務器的配置類,寫代碼之前,先來看一下下圖,
作為認證服務器,OAuth2 協議里的其他幾個角色他都要知道,認證服務器都要知道各個角色都是誰,他們各自的特征是什么。
1,要知道有哪些客戶端應用 來申請令牌
2,要知道有哪些合法的用戶
3,要知道發出去的令牌,能夠訪問哪些資源服務器
寫代碼
我們需要新建一個認證服務器配置類 OAuth2AuthServerConfig ,繼承 AuthorizationServerConfigurerAdapter ,AuthorizationServerConfigurerAdapter 是認證服務器適配器,我們看一下的源碼:
/** * @author Dave Syer * */ public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer { @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { } }
里面有三個方法,這三個方法,正對應上圖中箭頭所指的三個問題,我們需要重寫這三個方法,實現自己的配置。
1,配置Client信息
從圖中可以看出,認證服務器要配置兩個Client,一個是【客戶端應用】,他需要來認證服務器申請令牌,一個是 【訂單服務】,他要來認證服務器驗令牌。
重寫AuthorizationServerConfigurerAdapter 的 configure(ClientDetailsServiceConfigurer clients) throws Exception 方法
/** * Created by: 李浩洋 on 2019-10-29 * * 認證服務器 **/ @Configuration //這是一個配置類 @EnableAuthorizationServer //當前應用是一個認證服務器 public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {//AuthorizationServerConfigurerAdapter:認證服務器適配器 //Spring 對密碼加密的封裝,自己配置下 @Autowired private PasswordEncoder passwordEncoder; /** * 配置客戶端應用的信息,讓認證服務器知道有哪些客戶端應用來申請令牌。 * * ClientDetailsServiceConfigurer:客戶端的詳情服務的配置 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory()//配置在內存里,后面修改為數據庫里 //~============== 注冊【客戶端應用】,使客戶端應用能夠訪問認證服務器 =========== .withClient("orderApp") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //orderApp有哪些權限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //資源服務器的id。發給orderApp的token,能訪問哪些資源服務器,可以多個 .authorizedGrantTypes("password")//授權方式,再給orderApp做授權的時候可以用哪種授權方式授權 //~=============客戶端應用配置結束 ===================== .and() //~============== 注冊【資源服務器-訂單服務】(因為訂單服務需要來認證服務器驗令牌),使訂單服務也能夠訪問認證服務器 =========== .withClient("orderServer") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //有哪些權限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //資源服務器的id .authorizedGrantTypes("password");//授權方式 } }
2,配置用戶 信息
告訴認證服務器,有哪些用戶可以來訪問認證服務器
重寫AuthorizationServerConfigurerAdapter 的 configure(AuthorizationServerEndpointsConfigurer endpoints) 方法
/** * Created by: 李浩洋 on 2019-10-29 * * 認證服務器 **/ @Configuration //這是一個配置類 @EnableAuthorizationServer //當前應用是一個認證服務器 public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {//AuthorizationServerConfigurerAdapter:認證服務器適配器 //Spring 對密碼加密的封裝,自己配置下 @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationManager authenticationManager; /** * 1,配置客戶端應用的信息,讓認證服務器知道有哪些客戶端應用來申請令牌。 * * ClientDetailsServiceConfigurer:客戶端的詳情服務的配置 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory()//配置在內存里,后面修改為數據庫里 //~============== 注冊【客戶端應用】,使客戶端應用能夠訪問認證服務器 =========== .withClient("orderApp") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //orderApp有哪些權限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //資源服務器的id。發給orderApp的token,能訪問哪些資源服務器,可以多個 .authorizedGrantTypes("password")//授權方式,再給orderApp做授權的時候可以用哪種授權方式授權 //~=============客戶端應用配置結束 ===================== .and() //~============== 注冊【資源服務器-訂單服務】(因為訂單服務需要來認證服務器驗令牌),使訂單服務也能夠訪問認證服務器 =========== .withClient("orderServer") .secret(passwordEncoder.encode("123456")) //spring .scopes("read","write") //orderServer有哪些權限 .accessTokenValiditySeconds(3600) //token的有效期 .resourceIds("order-server") //資源服務器的id。 .authorizedGrantTypes("password");//授權方式, } /** *,2,配置用戶信息 * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //傳給他一個authenticationManager用來校驗傳過來的用戶信息是不是合法的,注進來一個,自己實現 endpoints.authenticationManager(authenticationManager); } /** * 3,配置資源服務器過來驗token 的規則 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { /** * 過來驗令牌有效性的請求,不是誰都能驗的,必須要是經過身份認證的。 * 所謂身份認證就是,必須攜帶clientId,clientSecret,否則隨便一請求過來驗token是不驗的 */ security.checkTokenAccess("isAuthenticated()"); } }
上邊的 加密解密類 PasswordEncoder 和 配置用戶信息的 AuthenticationManager 還沒有地方來,下邊配置這倆類。
新建配置類 OAuth2WebSecurityConfig 繼承 WebSecurityConfigurerAdapter
package com.nb.security.server.auth; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * Created by: 李浩洋 on 2019-10-29 **/ @Configuration @EnableWebSecurity //使安全配置生效 public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; /** * AuthenticationManagerBuilder 是用來構建 AuthenticationManager(處理登錄操作)的 * 需要兩個東西:userDetailsService 、passwordEncoder * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) //獲取用戶信息 .passwordEncoder(passwordEncoder); //比對密碼 } /** * 把AuthenticationManager暴露為bean * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
+++++++++++++++ 備注 ++++++++++++++++++++++++++
2019-12-15-23:00, passwordEncoder 本應該配置在上述類里,但是配置后報錯循環依賴,暫時將其寫在啟動類里,不報錯
+++++++++++++++++++++++++++++++++++++++++++++
UserDetailsService接口
UserDetailsService接口,只有一個方法,返回UserDetails 接口:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
loadUserByUsername,這里不用比對密碼,比對密碼是在AuthenticationManager里做的。
UserDetails接口如下,提供了一些見名知意的方法,我們需要自定義自己的UserDetails實現類,比如你的User類實現這個接口 :
public interface UserDetails extends Serializable { // ~ Methods // ======================================================================================================== Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); }
自定義UserDetailsService 實現類:
/** * Created by: 李浩洋 on 2019-10-29 **/ @Component//TODO:這里不寫 ("userDetailsService") public class UserDetailsServiceImpl implements UserDetailsService { /** * */ @Autowired private PasswordEncoder passwordEncoder; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return User.withUsername(username) .password(passwordEncoder.encode("123456")) .authorities("ROLE_ADMIN") //權限 .build();//構建一個User對象 } }
AuthenticationManager 接口
也只有一個方法,實現類一般不需要自己實現。入參 是一個 Authentication 接口的實現,其中封裝了認證的信息,不同的認證發的信息不一樣,如用戶名/密碼 登錄需要用戶名密碼,OAuth2則需要appId,appSecret,redirectURI等,
不同的認證方式傳過來的實現不同, authenticate 方法驗證完了后將其中的信息更新調,返回。
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
所有的准備工作都做好了,下面啟動應用,來申請一個OAuth2令牌
postman請求http://localhost:9090/oauth/token ,HttpBasic傳入客戶端id和客戶端密碼。
用戶名隨便輸,密碼要和UserDetailsService一致,grant_type是說OAuth2的授權類型是授權碼類型,scope是該客戶端申請的權限,必須在Client配置里面包含
輸入錯誤的密碼:
輸入不存在的scope:
代碼github:https://github.com/lhy1234/springcloud-security/tree/chapt-4-4-andbefore
+++++++++++++分割線++++++++++++++++++++++
小結
用圖片生動解釋了 OAuth2 中的角色和Spring接口 AuthorizationServerConfigurerAdapter 三個方法的關系,方便記憶
實現了OAuth2的密碼模式,來申請token
存在問題:passwordEncoder 如果放在了 OAuth2WebSecurityConfig配置類里面,就會報循環依賴錯誤,有待解決