微信受權就是這個原理,Spring Cloud OAuth2 受權碼模式

上一篇文章Spring Cloud OAuth2 實現單點登陸介紹了使用 password 模式進行身份認證和單點登陸。本篇介紹 Spring Cloud OAuth2 的另一種受權模式-受權碼模式。html

受權碼模式的認證過程是這樣的:前端

一、用戶客戶端請求認證服務器的認證接口,並附上回調地址;java

二、認證服務接口接收到認證請求後調整到自身的登陸界面;mysql

三、用戶輸入用戶名和密碼,點擊確認,跳轉到受權、拒絕提示頁面(也可省略);git

四、用戶點擊受權或者默認受權後,跳轉到微服務客戶端的回調地址,並傳入參數 code;github

五、回調地址通常是一個 RESTful 接口,此接口拿到 code 參數後,再次請求認證服務器的 token 獲取接口,用來換取 access_token 等信息;web

六、獲取到 access_token 後,拿着 token 去請求各個微服務客戶端的接口。redis

注意上面所說的用戶客戶端能夠理解爲瀏覽器、app 端,微服務客戶端就是咱們系統中的例如訂單服務、用戶服務等微服務,認證服務端就是用來作認證受權的服務,相對於認證服務端來講,各個業務微服務也能夠稱做是它的客戶端。spring

認證服務端配置

認證服務端繼續用上一篇文章的配置,代碼不須要任何改變,只須要在數據庫里加一條記錄,來支持新加的微服務客戶端的認證sql

咱們要建立的客戶端的 client-id 爲 code-client,client-secret 爲 code-secret-8888,可是一樣須要加密,能夠用以下代碼獲取:

System.out.println(new BCryptPasswordEncoder().encode("code-secret-8888"));

除了以上這兩個參數,要將 authorized_grant_types 設置爲 authorization_code,refresh_token,web_server_redirect_uri 設置爲回調地址,稍後微服務客戶端會建立這個接口。

而後將這條記錄組織好插入數據庫中。

INSERT INTO oauth_client_details
    (client_id, client_secret, scope, authorized_grant_types,
    web_server_redirect_uri, authorities, access_token_validity,
    refresh_token_validity, additional_information, autoapprove)
VALUES
    ('code-client', '$2a$10$jENDQZRtqqdr6sXGQK.L0OBADGIpyhtaRfaRDTeLKI76I/Ir1FDn6', 'all',
    'authorization_code,refresh_token', 'http://localhost:6102/client-authcode/login', null, 3600, 36000, null, true);

建立受權模式的微服務

引入 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>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.14.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

引入 okhttp 和 thymeleaf 是由於要作一個簡單的頁面並模擬正常的認證過程。

配置文件 application.yml

spring:
  application:
    name: client-authcode
server:
  port: 6102
  servlet:
    context-path: /client-authcode


security:
  oauth2:
    client:
      client-id: code-client
      client-secret: code-secret-8888
      user-authorization-uri: http://localhost:6001/oauth/authorize
      access-token-uri: http://localhost:6001/oauth/token
    resource:
      jwt:
        key-uri: http://localhost:6001/oauth/token_key
        key-value: dev
    authorization:
      check-token-access: http://localhost:6001/oauth/check_token

建立 resourceConfig

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Bean
    public TokenStore jwtTokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();

        accessTokenConverter.setSigningKey("dev");
        accessTokenConverter.setVerifierKey("dev");
        return accessTokenConverter;
    }

    @Autowired
    private TokenStore jwtTokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.tokenStore(jwtTokenStore);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/login").permitAll();
    }
}

使用 jwt 做爲 token 的存儲,注意容許 /login 接口無受權訪問,這個地址是認證的回調地址,會返回 code 參數。

建立 application.java啓動類

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

到這步能夠先停一下了。咱們把認證服務端和剛剛建立的認證客戶端啓動起來,就能夠手工測試一下了。回調接口不是還沒建立呢嗎,不要緊,咱們權當那個地址如今就是爲了接收 code 參數的。
一、在瀏覽器訪問 /oauth/authorize 受權接口,接口地址爲:

http://localhost:6001/oauth/authorize?client_id=code-client&response_type=code&redirect_uri=http://localhost:6102/client-authcode/login

注意 response_type 參數設置爲 code,redirect_uri 設置爲數據庫中插入的回調地址。

二、輸入上面地址後,會自動跳轉到認證服務端的登陸頁面,輸入用戶名、密碼,這裏用戶名是 admin,密碼是 123456

三、點擊肯定後,來到受權確認頁面,頁面上有 Authorize 和 Deny (受權和拒絕)兩個按鈕。可經過將 autoapprove 字段設置爲 0 來取消此頁面的展現,默認直接贊成受權。

四、點擊贊成受權後,跳轉到了回調地址,雖然是 404 ,可是咱們只是爲了拿到 code 參數,注意地址後面的 code 參數。

五、拿到這個 code 參數是爲了向認證服務器 /oauth/token 接口請求 access_token ,繼續用 REST Client 發送請求,一樣的,你也能夠用 postman 等工具測試。

注意 grant_type 參數設置爲 authorization_code,code 就是上一步回調地址中加上的,redirect_uri 仍然要帶上,回做爲驗證條件,若是不帶或者與前面設置的不一致,會出現錯誤。

請求頭 Authorization ,仍然是 Basic + 空格 + base64(client_id:client_secret),能夠經過 https://www.sojson.com/base64.html 網站在線作 base64 編碼。

code-client:code-secret-8888 經過 base64 編碼後結果爲 Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==

POST http://localhost:6001/oauth/token?grant_type=authorization_code&client=code-client&code=BbCE34&redirect_uri=http://localhost:6102/client-authcode/login
Accept: */*
Cache-Control: no-cache
Authorization: Basic Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==

發送請求後,返回的 json 內容以下:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTU3MjYwMTMzMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI2OWRmY2M4Yy1iZmZiLTRiNDItYTZhZi1hN2IzZWUyZjI1ZTMiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.WlgGnBkNdg2PwKqjbZWo6QmUmq0QluZLgIWJXaZahSU",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6IjY5ZGZjYzhjLWJmZmItNGI0Mi1hNmFmLWE3YjNlZTJmMjVlMyIsImV4cCI6MTU3MjYzMzczMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJkNzk2OWRhMS04NTg4LTQ2YzMtYjdlNS1jMGM5NzcxNTM5Y2YiLCJjbGllbnRfaWQiOiJjb2RlLWNsaWVudCJ9.TEz0pQOhST9-ozdoJWm6cf1SoWvPC6W-5JW9yjZJXek",
  "expires_in": 3599,
  "scope": "all",
  "jwt-ext": "JWT 擴展信息",
  "jti": "69dfcc8c-bffb-4b42-a6af-a7b3ee2f25e3"
}

和上一篇文章 password 模式拿到的 token 內容是一致的,接下來的請求都須要帶上 access_token 。

六、把獲取到的 access_token 代入到下面的請求中 ${access_token} 的位置,就能夠請求微服務中的須要受權訪問的接口了。

GET http://localhost:6102/client-authcode/get
Accept: */*
Cache-Control: no-cache
Authorization: bearer ${access_token}

接口內容以下:

@org.springframework.web.bind.annotation.ResponseBody
@GetMapping(value = "get")
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public Object get(Authentication authentication)
{
    //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    authentication.getCredentials();
    OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
    String token = details.getTokenValue();
    return token;
}

通過以上的手工測試,證實此過程是通的,可是尚未達到自動化。若是你集成過微信登陸,那你必定知道咱們在回調地址中作了什麼,拿到返回的 code 參數去 token 接口換取 access_token 對不對,沒錯,思路都是同樣的,咱們的回調接口中一樣要拿 code 去換取 access_token。

爲此,我作了一個簡單的頁面,而且在回調接口中請求獲取 token 的接口。

建立簡單的登陸頁面

在 resources 目錄下建立 templates 目錄,用來存放 thymeleaf 的模板,不作樣式,只作最簡單的演示,建立 index.html 模板,內容以下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>古時的風箏-OAuth2 Client</title>
</head>
<body>
<div>
    <a href="http://localhost:6001/oauth/authorize?client_id=code-client&response_type=code&redirect_uri=http://localhost:6102/client-authcode/login">登陸</a>
    <span th:text="'當前認證用戶:' + ${username}"></span>
    <span th:text="${accessToken}"></span>
</div>
</body>
</html>

回調接口及其餘接口

@Slf4j
@Controller
public class CodeClientController {

    /**
     * 用來展現index.html 模板
     * @return
     */     
    @GetMapping(value = "index")
    public String index(){
        return "index";
    }

    @GetMapping(value = "login")
    public Object login(String code,Model model) {
        String tokenUrl = "http://localhost:6001/oauth/token";
        OkHttpClient httpClient = new OkHttpClient();
        RequestBody body = new FormBody.Builder()
                .add("grant_type", "authorization_code")
                .add("client", "code-client")
                .add("redirect_uri","http://localhost:6102/client-authcode/login")
                .add("code", code)
                .build();

        Request request = new Request.Builder()
                .url(tokenUrl)
                .post(body)
                .addHeader("Authorization", "Basic Y29kZS1jbGllbnQ6Y29kZS1zZWNyZXQtODg4OA==")
                .build();
        try {
            Response response = httpClient.newCall(request).execute();
            String result = response.body().string();
            ObjectMapper objectMapper = new ObjectMapper();
            Map tokenMap = objectMapper.readValue(result,Map.class);
            String accessToken = tokenMap.get("access_token").toString();
            Claims claims = Jwts.parser()
                    .setSigningKey("dev".getBytes(StandardCharsets.UTF_8))
                    .parseClaimsJws(accessToken)
                    .getBody();
            String userName = claims.get("user_name").toString();
            model.addAttribute("username", userName);
            model.addAttribute("accessToken", result);
            return "index";
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    @org.springframework.web.bind.annotation.ResponseBody
    @GetMapping(value = "get")
    @PreAuthorize("hasAnyRole('ROLE_ADMIN')")
    public Object get(Authentication authentication) {
        //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        authentication.getCredentials();
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        String token = details.getTokenValue();
        return token;
    }
}

其中 index() 方法是爲了展現 thymeleaf 模板,login 方法就是回調接口,這裏用了 okhttp3 用做接口請求,請求認證服務端的 /oauth/token 接口來換取 access_token,只是把咱們手工測試的步驟自動化了。

訪問 index.html 頁面

咱們假設這個頁面就是一個網站的首頁,未登陸的用戶會在網站上看到登陸按鈕,咱們訪問這個頁面:http://localhost:6102/client-authcode/index,看到的頁面是這樣的

接下來,點擊登陸按鈕,經過上面的模板代碼看出,點擊後其實就是跳轉到了咱們手工測試第一步訪問的那個地址,以後的操做和上面手工測試的是一致的,輸入用戶名密碼、點擊贊成受權。

接下來,頁面跳轉回回調地址<http://localhost:6102/client-authcode/login?code=xxx 的時候,login 方法拿到 code 參數,開始構造 post 請求體,並把 Authorization 加入請求頭,而後請求 oauth/token 接口,最後將拿到的 token 和 經過 token 解析後的 username 返回給前端,最後呈現的效果以下:

最後,拿到 token 後的客戶端,就能夠將 token 加入到請求頭後,去訪問須要受權的接口了。

結合上一篇文章,咱們就實現了 password 和 受權碼兩種模式的 oauth2 認證。

本篇源碼微服務客戶端對應的源碼地址爲: 點擊查看 github 源碼

相關閱讀

Spring Cloud OAuth2 實現單點登陸

不要吝惜你的「推薦」呦

歡迎關注,不按期更新本系列和其餘文章
古時的風箏 ,進入公衆號能夠加入交流羣

相關文章
相關標籤/搜索