1. 程式人生 > 程式設計 >spring cloud oauth2 實現使用者認證登入的示例程式碼

spring cloud oauth2 實現使用者認證登入的示例程式碼

需求

在微服務架構中,我們有很多業務模組,每個模組都需要有使用者認證,許可權校驗。有時候也會接入來自第三方廠商的應用。要求是隻登入一次,即可在各個服務的授權範圍內進行操作。看到這個需求,立馬就想到了這不就是單點登入嗎?於是基於這樣的需求,作者使用spring-cloud-oauth2去簡單的實現了下使用者認證和單點登入。

相關介紹

OAuth2

OAuth2是一個關於授權的網路標準,他定製了設計思路和執行流程。OAuth2一共有四種授權模式:授權碼模式(authorization code)、簡化模式(implicit)、密碼模式(resource owner password)和客戶端模式(client credentials)。資料的所有者告訴系統同意授權第三方應用進入系統,獲取這些資料。於是資料所有者生產了一個短時間內有效的授權碼(token)給第三方應用,用來代替密碼,供第三方使用。具體流程請看下圖,具體的OAuth2介紹,可以參考這篇文章,寫的很詳細。(https://www.jb51.net/article/198292.htm)

spring cloud oauth2 實現使用者認證登入的示例程式碼

Token

令牌(token)和密碼(password)的作用是一樣的,都可以進入系統獲取資源,但是也有幾點不同:

  1. 令牌是短期的,到期會自動失效,使用者無法修改。密碼是長期的,使用者可以修改,如果不修改,就不會發生變化。
  2. 令牌可以被資料所有者撤銷,令牌會立即失效。密碼一般不允許其他人撤銷,只能被操作許可權更高的人或者本人修改/重製。
  3. 令牌是有許可權範圍的,會被資料所有者授予。

實現的功能

本篇介紹的是通過密碼模式來實現單點登入的功能。

​ 在微服務架構中,我們的一個應用可能會有很多個服務執行,協調來處理實際的業務。這就需要用到單點登入的技術,來統一認證調取介面的是哪個使用者。那總不能請求一次,就認證一次,這麼做肯定是不行的。那麼就需要在認證完使用者之後,給這個使用者授權,然後發一個令牌(token),有效期內使用者請求資源時,就只需要帶上這個標識自己身份的token即可。

架構說明
認證中心:oauth2-oauth-server,OAuth2的服務端,主要完成使用者Token的生成、重新整理、驗證等。

微服務:mzh-etl,微服務之一,接收到請求之後回到認證中心(oauth2-oauth-server)去驗證。

程式碼實現

使用到的框架是java基礎的spring boot 和spring-cloud-oauth2

認證中心:

1、引入需要的maven包

<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>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

因為spring-cloud-starter-oauth2中包含了spring-cloud-starter-security,所以就不用再單獨引入了,引入redis包是為了使用redis來儲存token。

2、配置application.yml

這裡主要用到的是redis的配置,mysql資料庫的配置暫時沒有用到。

spring:
 application:
 name: oauth-server
 datasource:
 url: jdbc:mysql://localhost:3306/mzh_oauth?useSSL=false&characterEncoding=UTF-8
 username: root
 password: admin123
 driver-class-name: com.mysql.jdbc.Driver
 hikari:
  connection-timeout: 30000
  idle-timeout: 600000
  max-lifetime: 1800000
  maximum-pool-size: 9
 redis:
 database: 0
 host: localhost
 port: 6379
 jedis:
  pool:
  max-active: 8
  max-idle: 8
  min-idle: 0
 timeout: 10000
server:
 port: 8888
 use-forward-headers: true

management:
 endpoint:
 health:
  enabled: true

3、spring security 許可權配置

需要繼承WebSecurityConfigurerAdapter

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

 @Autowired
 private CustomUserDetailsService customUserDetailsService;
 
 /**
  * 修改密碼的加密方式
  * @return
  */
 @Bean
 public PasswordEncoder passwordEncoder(){
  return new BCryptPasswordEncoder();
 }

 @Bean
 @Override
 public AuthenticationManager authenticationManagerBean() throws Exception{
  return super.authenticationManagerBean();
 }

 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception{
  // 如果使用BCryptPasswordEncoder,這裡就必須指定密碼的加密類
  auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
 }

 @Override
 protected void configure(HttpSecurity http) throws Exception {
  http.authorizeRequests()
    .antMatchers("/oauth/**").permitAll();
 }
}

BCryptPasswordEncoder是一個不可逆的密碼加密類,AuthenticationManager是OAuth2的password必須指定的授權管理Bean。

CustomUserDetailsService這個類是被注入進來的,熟悉spring security的同學應該知道,spring security有一個自己的UserdetailsService用於許可權校驗時獲取使用者資訊,但是很多時候不符合我們的業務場景,就需要重現實現這個類。

4、實現CustomUserDetailsService

UserDetailsService這個類的核心方法就是loadUserByUsername()方法,他接收一個使用者名稱,返回一個UserDetails物件。

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@Component(value = "customUserDetailsService")
public class CustomUserDetailsService implements UserDetailsService {

 @Autowired
 private PasswordEncoder passwordEncoder;

 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  // 1. 根據username 去資料庫查詢 user

  // 2.獲取使用者的角色和許可權

  // 下面是寫死,暫時不和資料庫互動
  if(!(("admin").equals(username))){
   throw new UsernameNotFoundException("the user is not found");
  }else{
   String role = "ADMIN_ROLE";
   List<SimpleGrantedAuthority> authorities = new ArrayList<>();
   authorities.add(new SimpleGrantedAuthority(role));
   String password = passwordEncoder.encode("123456");
   return new User(username,password,authorities);
  }
 }
}

這裡是在程式中寫死了使用者和許可權。賬號:admin,密碼:123456,許可權:ADMIN_ROLE(注意是許可權,不是角色),實際中應該從資料庫獲取使用者和相關的許可權,然後進行認證。

5、OAuth2 配置

OAuth2配置需要繼承AuthorizationServerConfigurerAdapter類

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@Configuration
@EnableAuthorizationServer
public class Oauth2Config extends AuthorizationServerConfigurerAdapter {

 @Autowired
 private PasswordEncoder passwordEncoder;

 @Autowired
 private UserDetailsService customUserDetailsService;

 @Autowired
 private AuthenticationManager authenticationManager;

 @Autowired
 private TokenStore redisTokenStore;

 /**
  * 對AuthorizationServerEndpointsConfigurer引數的重寫
  * 重寫授權管理Bean引數
  * 重寫使用者校驗
  * 重寫token快取方式
  * @param endpointsConfigurer
  * @throws Exception
  */
 @Override
 public void configure(final AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception{
  endpointsConfigurer.authenticationManager(authenticationManager)
    .userDetailsService(customUserDetailsService)
    .tokenStore(redisTokenStore);
 }

 /**
  * 客戶端的引數的重寫
  * 這裡是將資料直接寫入記憶體,實際應該從資料庫表獲取
  * clientId:客戶端Id
  * secret:客戶端的金鑰
  * authorizedGrantTypes:授權方式
  *  authorization_code: 授權碼型別,*  implicit: 隱式授權,*  password: 密碼授權,*  client_credentials: 客戶端授權,*  refresh_token: 通過上面4中方式獲取的重新整理令牌獲取的新令牌,
  *      注意是獲取token和refresh_token之後,通過refresh_toke重新整理之後的令牌
  * accessTokenValiditySeconds: token有效期
  * scopes 用來限制客戶端訪問的許可權,只有在scopes定義的範圍內,才可以正常的換取token
  * @param clients
  * @throws Exception
  */
 @Override
 public void configure(ClientDetailsServiceConfigurer clients) throws Exception{
  clients.inMemory()
    .and()
    .withClient("mzh-etl")
    .secret(passwordEncoder.encode("mzh-etl-8888"))
    .authorizedGrantTypes("refresh_token","authorization_code","password")
    .accessTokenValiditySeconds(3600)
    .scopes("all");
 }

 @Override
 public void configure(AuthorizationServerSecurityConfigurer serverSecurityConfigurer) throws Exception{
  serverSecurityConfigurer.allowFormAuthenticationForClients();
  serverSecurityConfigurer.checkTokenAccess("permitAll()");
  serverSecurityConfigurer.tokenKeyAccess("permitAll()");
  serverSecurityConfigurer.passwordEncoder(passwordEncoder);
 }
}

6、啟動服務

上述步驟完成之後啟動服務,然後觀察IDEA下方的Endpoints中的Mappings,就可以找到相關的認證埠。主要的有以下幾個:

  • POST /oauth/authorize 授權碼模式認證授權介面
  • GET/POST /oauth/token 獲取 token 的介面
  • POST /oauth/check_token 檢查 token 合法性介面

到此,認證中心就算是建立完成了。我們通過idea的REST Client 來請求一個token進行測試。

請求內容如下:

POST http://localhost:8888/oauth/token?grant_type=password&username=admin&password=123456&scope=all 
Accept: */* 
Cache-Control: no-cache 
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

第一行POST http://localhost:8888/oauth/token?grant_type=password&username=admin&password=123456&scope=all 表示發起一個POST請求,請求路徑是/oauth/token,請求引數是grant_type=password表示認證型別是password,username=admin&password=123456表示使用者名稱是admin,密碼是123456,scope=all是許可權相關的,之前在Oauth2Config中配置了scope是all。

第四行表示在請求頭中加入一個欄位Authorization,值為Basic空格base64(clientId:clientSecret),我們之前配置的clientId是“meh-etl”,clientSecret是"meh-etl-8888",所以這個值的base64是:bXpoLWV0bDptemgtZXRsLTg4ODg=。

執行請求之後,如果引數都正確的話,獲取到返回的內容如下:

{
 // token值,後面請求介面時都需要帶上的token
 "access_token": "b4cb804c-93d2-4635-913c-265ff4f37309",// token的形式
 "token_type": "bearer",// 快過期時可以用這個換取新的token
 "refresh_token": "5cac05f4-158f-4561-ab16-b06c4bfe899f",// token的過期時間
 "expires_in": 3599,// 許可權範圍
 "scope": "all"
}

token值過期之後,可以通過refresh_token來換取新的access_token

POST http://localhost:8888/oauth/token?grant_type=refresh_token&refresh_token=706dac10-d48e-4795-8379-efe8307a2282 
Accept: */* 
Cache-Control: no-cache 
Authorization: Basic dXNlci1jbGllbnQ6dXNlci1zZWNyZXQtODg4OA==

這次grant_type的值為“refresh_token”,refresh_token的值是要過期的token的refresh_token值,也就是之前請求獲取Token的refresh_token值,請求之後會返回一個和獲取token時一樣格式的資料。

微服務

1、引入需要的maven包

<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>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置application.yml

spring:
 application:
 name: mzh-etl
 redis:
 database: 1
 host: localhost
 port: 6379
 jedis:
  pool:
  max-active: 8
  max-idle: 8
  min-idle: 0
 timeout: 10000
server:
 port: 8889
security:
 oauth2:
 client:
  # 需要和之前認證中心配置中的一樣
  client-id: mzh-etl
  client-secret: mzh-etl-8888
  # 獲取token的地址
  access-token-uri: http://localhost:8888/oauth/token
 resource:
  id: mzh-etl
  user-info-uri: user-info
 authorization:
  # 檢查token的地址
  check-token-access: http://localhost:8888/oauth/check_token

這裡的配置一定要仔細,必須和之前認證中心中配置的一樣。

3、資源配置

在OAuth2中介面也稱為資源,資源的許可權也就是介面的許可權。spring-cloud-oauth2提供了關於資源的註解

@EnableResourceServer

/**
 * @Author mzh
 * @Date 2020/10/24
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

 @Value("${security.oauth2.client.client-id}")
 private String clientId;

 @Value("${security.oauth2.client.client-secret}")
 private String clientSecret;

 @Value("${security.oauth2.authorization.check-token-access}")
 private String checkTokenEndpointUrl;

 @Autowired
 private RedisConnectionFactory redisConnectionFactory;

 @Bean("redisTokenStore")
 public TokenStore redisTokenStore(){
  return new RedisTokenStore(redisConnectionFactory);
 }

 @Bean
 public RemoteTokenServices tokenService() {
  RemoteTokenServices tokenService = new RemoteTokenServices();
  tokenService.setClientId(clientId);
  tokenService.setClientSecret(clientSecret);
  tokenService.setCheckTokenEndpointUrl(checkTokenEndpointUrl);
  return tokenService;
 }

 @Override
 public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
  resources.tokenServices(tokenService());
 }

 @Override
 public void configure(HttpSecurity http) throws Exception {
  http.authorizeRequests().antMatchers("/get/**").authenticated();
 }
}

4、建立一個介面

@RestController
public class UserController {

 @GetMapping("get")
 @PreAuthorize("hasAuthority('ADMIN_ROLE')")
 public Object get(Authentication authentication){
  authentication.getAuthorities();
  OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
  String token = details.getTokenValue();
  return token;
 }
}

這個介面就是會返回一個請求他時攜帶的token值,@PreAuthorize會在請求介面時檢查是否用許可權“ADMIN_ROLE”(之前認證中心配置的許可權)

5、啟動服務

啟動服務,只有當用戶有“ADMIN_ROLE“的時候,才能正確返回,否則返回401未授權

同樣適用REST Client來發起一個請求:

GET http://localhost:8889/get 
Accept: */* 
Cache-Control: no-cache 
Authorization: bearer b4cb804c-93d2-4635-913c-265ff4f37309

請求路徑是http://localhost:8889/get 然後在請求頭部帶上我們上一步驟獲取到的token,放入到Authorization中,格式是bearer空格token值,如果請求成功,就會把token原樣返回。

到此這篇關於spring cloud oauth2 實現使用者認證登入的示例程式碼的文章就介紹到這了,更多相關spring cloud oauth2 認證登入內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!