spring cloud oauth2 實現用戶認證登陸

spring-cloud-oauth2 實現用戶認證及單點登陸

需求

​ 在微服務架構中,咱們有不少業務模塊,每一個模塊都須要有用戶認證,權限校驗。有時候也會接入來自第三方廠商的應用。要求是隻登陸一次,便可在各個服務的受權範圍內進行操做。看到這個需求,立馬就想到了這不就是單點登陸嗎?因而基於這樣的需求,做者使用spring-cloud-oauth2去簡單的實現了下用戶認證和單點登陸。html

相關介紹

OAuth2

OAuth2是一個關於受權的網絡標準,他定製了設計思路和執行流程。OAuth2一共有四種受權模式:受權碼模式(authorization code)、簡化模式(implicit)、密碼模式(resource owner password)和客戶端模式(client credentials)。數據的全部者告訴系統贊成受權第三方應用進入系統,獲取這些數據。因而數據全部者生產了一個短期內有效的受權碼(token)給第三方應用,用來代替密碼,供第三方使用。具體流程請看下圖,具體的OAuth2介紹,能夠參考這篇文章,寫的很詳細。(http://www.ruanyifeng.com/blog/2019/04/oauth_design.html)java

img

Token

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

  1. 令牌是短時間的,到期會自動失效,用戶沒法修改。密碼是長期的,用戶能夠修改,若是不修改,就不會發生變化。
  2. 令牌能夠被數據全部者撤銷,令牌會當即失效。密碼通常不容許其餘人撤銷,只能被操做權限更高的人或者本人修改/重製。
  3. 令牌是有權限範圍的,會被數據全部者授予。

實現的功能

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

​ 在微服務架構中,咱們的一個應用可能會有不少個服務運行,協調來處理實際的業務。這就須要用到單點登陸的技術,來統一認證調取接口的是哪一個用戶。那總不能請求一次,就認證一次,這麼作確定是不行的。那麼就須要在認證完用戶以後,給這個用戶受權,而後發一個令牌(token),有效期內用戶請求資源時,就只須要帶上這個標識本身身份的token便可。redis

架構說明

認證中心:oauth2-oauth-server,OAuth2的服務端,主要完成用戶Token的生成、刷新、驗證等。spring

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

代碼實現

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

認證中心:
一、引入須要的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。json

二、配置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
三、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用於權限校驗時獲取用戶信息,可是不少時候不符合咱們的業務場景,就須要重現實現這個類。

四、實現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(注意是權限,不是角色),實際中應該從數據庫獲取用戶和相關的權限,而後進行認證。

五、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);
    }
}
六、啓動服務

上述步驟完成以後啓動服務,而後觀察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,密碼是123456scope=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時同樣格式的數據。

微服務
一、引入須要的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>
二、配置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

這裏的配置必定要仔細,必須和以前認證中心中配置的同樣。

三、資源配置

在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();
    }
}
四、建立一個接口
@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」(以前認證中心配置的權限)

五、啓動服務

啓動服務,只有當用戶有「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原樣返回。

本文由博客羣發一文多發等運營工具平臺 OpenWrite 發佈

相關文章
相關標籤/搜索