天天看點

OAuth2(一):了解OAuth2授權方式,入門項目搭建

[最近工作需要,了解了一些OAuth2,并進行了實際操作,網上資料比較亂,這裡總結一下,本地一共記錄了6篇,這裡做适當删減,便于了解,更新會有點慢!]

[完整代碼我會上傳到我的github,點選直達,有需要的可以自行參考!]

OAuth2

OAuth2.0 是目前最流行的授權機制,用來授權第三方應用,擷取使用者資料,是一種協定規範。資料的所有者告訴系統,同意授權第三方應用進入系統,擷取這些資料。系統進而産生一個短期的進入令牌(token),用來代替密碼,供第三方應用使用。

OAuth2是一個關于授權的開放标準,核心思路是通過各類認證手段(具體什麼手段OAuth2不關心)認證使用者身份,并頒發token(令牌),使得第三方應用可以使用該令牌在限定時間、限定範圍通路指定資源。主要涉及的RFC規範有RFC6749(整體授權架構),RFC6750(令牌使用),RFC6819(威脅模型)這幾個,一般我們需要了解的就是RFC6749。

[文字都有,參考了其他人的]

OAuth2(一):了解OAuth2授權方式,入門項目搭建

RFC6749 重要概念

  • resource owner: 擁有被通路資源的使用者
  • user-agent: 一般來說就是浏覽器
  • client: 第三方應用
  • Authorization server: 認證伺服器,用來進行使用者認證并頒發token
  • Resource server:資源伺服器,擁有被通路資源的伺服器,需要通過token來确定是否有權限通路

授權方式

  • 授權碼(authorization-code)
  • 隐藏式(implicit)
  • 密碼式(password):
  • 用戶端憑證(client credentials)

  注意,不管哪一種授權方式,第三方應用申請令牌之前,都必須先到系統備案,說明自己的身份,然後會拿到兩個身份識别碼:用戶端 ID(client ID)和用戶端密鑰(client secret)。這是為了防止令牌被濫用,沒有備案過的第三方應用,是不會拿到令牌的。 更多的請參考: 阮一峰   這裡介紹一下我們要搭建的,即授權碼方式,也是目前應用最廣泛的,一些網站可以支援QQ、微信等三方登入,也是使用的此方式。

OAuth2(一):了解OAuth2授權方式,入門項目搭建

授權碼流程圖 總結一下 大緻流程

  1. 攜帶 client_id,redirect_uri 去請求 OAuth 的 oauth/authorize 服務,登入後擷取授權碼
  2. 背景使用獲得的授權碼,配合 client_id,client_secret 去請求 OAuth 的擷取 oauth/token 服務,擷取到 access_token 憑證
  3. 使用擷取到的 access_token 去請求資源伺服器擷取相關資料。

快速入門 一、搭建 Authorization Server  1.建立 auth-server 工程,引入相關依賴

<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>
    <version>${oauth2.version}</version>
</dependency>
           

配置 application.yaml

server:
  port: 7777

spring:
  messages:
    basename: i18n/messages
    encoding: UTF-8
  thymeleaf:
    cache: false # 開發時關閉緩存,不然沒法看到實時頁面
    mode: HTML # 用非嚴格的 HTML
    encoding: UTF-8
    servlet:
      content-type: text/html
           

2. 配置 SecurityConfiguration 繼承 WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder encoder() {
        return new BCryptPasswordEncoder();
    }

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

    /**
     * 配置認證資訊
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在記憶體中建立2個使用者,便于測試,後期可改為資料庫中擷取
        auth.inMemoryAuthentication()
                .passwordEncoder(encoder())
                .withUser("user").password(encoder().encode("user")).roles("USER")
                .and()
                .withUser("admin").password(encoder().encode("admin")).roles("ADMIN");
    }

    /**
     * 配置核心過濾器
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //解決靜态資源被攔截的問題
        web.ignoring().antMatchers("/asserts/**");
    }

    /**
     * 配置Security的認證政策
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置登入頁并允許通路
        http.formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
            // 配置登出頁面
            .logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/")
                .and()
            .authorizeRequests()
                .antMatchers("/oauth/**", "/login/**", "/logout/**")
                .permitAll()
            // 其餘所有請求全部需要鑒權認證
            .anyRequest()
                .authenticated()
                .and()
            // 關閉跨域保護,Basic登入
            .csrf().disable()
            .httpBasic().disable();
    }
}
           

3.配置 OAuth2Configuration 繼承 AuthorizationServerConfigurerAdapter

/**
*
* describe OAuth2 配置 - TOKEN 儲存在記憶體中
* @author 20018704
* @date 2020/9/2 10:39
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {

    private static final String CLIENT_ID = "cms";
    private static final String SECRET_CHAR_SEQUENCE = "secret";
    private static final String SCOPE_READ = "read";
    private static final String SCOPE_WRITE = "write";
    private static final String TRUST = "trust";
    private static final String USER = "user";
    private static final String ALL = "all";
    private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 2 * 60;
    private static final int REFRESH_TOKEN_VALIDITY_SECONDS = 2 * 60;
    // 密碼模式授權模式
    private static final String GRANT_TYPE_PASSWORD = "password";
    // 授權碼模式
    private static final String AUTHORIZATION_CODE = "authorization_code";
    // refresh token模式
    private static final String REFRESH_TOKEN = "refresh_token";
    // 簡化授權模式
    private static final String IMPLICIT = "implicit";
    // 指定哪些資源是需要授權驗證的
    private static final String RESOURCE_ID = "resource_id";

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    /**
     * 用來配置令牌端點(Token Endpoint)的安全限制
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("isAuthenticated()")    // 開啟/oauth/token_key驗證端口認證權限通路
                .checkTokenAccess("permitAll()")  // 開啟/oauth/check_token驗證端口認證權限通路
                .allowFormAuthenticationForClients();   // 允許表單認證否則在授權碼模式下會導緻無法根據code擷取token 
    }

    /**
     * 用來配置用戶端詳情服務(ClientDetailsService),用戶端詳情資訊在這裡進行初始化
     * 你能夠把用戶端詳情資訊寫死在這裡或者是通過資料庫來存儲調取詳情資訊。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 使用記憶體存儲
        clients.inMemory()
                .withClient(CLIENT_ID) //标記用戶端id
                .secret(passwordEncoder.encode(SECRET_CHAR_SEQUENCE)) //用戶端安全碼
                .autoApprove(true) //為true 直接自動授權成功傳回code
                //重定向uri,uri中攜帶的必須為這裡面填寫的之一,比如可以在下面的接口中,處理傳回的code
                .redirectUris("http://127.0.0.1:8084/cms/thirdLogin") 
                .scopes(ALL) //允許授權範圍
                .authorizedGrantTypes(GRANT_TYPE_PASSWORD, AUTHORIZATION_CODE, REFRESH_TOKEN, IMPLICIT); //允許授權類型
    }

    /**
     * 用來配置授權(authorization)以及令牌(token)的通路端點和令牌服務(token services)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 使用記憶體儲存生成的token
        endpoints.authenticationManager(authenticationManager).tokenStore(memoryTokenStore());
    }

    public TokenStore memoryTokenStore() {
        // 最基本的InMemoryTokenStore生成token
        return new InMemoryTokenStore();
    }
}
           

4.自定義登入頁面

添加 Login 方法

@Controller
public class LoginController {

    @GetMapping(value = {"/login"})
    public ModelAndView toLogin(){
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("login");
        return modelAndView;
    }
}
           

使用 thymeleaf 模闆引擎來建構 login 頁面,相關國際化配置不在這裡展示。

<!DOCTYPE html>
<html  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="description" content="" />
    <meta name="author" content="" />
    <title>Signin Template for Bootstrap</title>
    <!-- Bootstrap core CSS -->
    <link href="../static/asserts/css/bootstrap.min.css" target="_blank" rel="external nofollow"  th:href="@{asserts/css/bootstrap.min.css}" target="_blank" rel="external nofollow"  rel="stylesheet" />
    <!-- Custom styles for this template -->
    <link href="../static/asserts/css/signin.css" target="_blank" rel="external nofollow"  th:href="@{asserts/css/signin.css}" target="_blank" rel="external nofollow"  rel="stylesheet"/>
</head>

<body class="text-center">
<form class="form-signin" th:action="@{/login}" method="post">
    <img class="mb-4" th:src="@{asserts/img/bootstrap-solid.svg}" alt="" width="72" height="72" />
    <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Oauth2.0 Login</h1>
    <label class="sr-only" th:text="#{login.username}">Username</label>
    <input type="text" class="form-control" name="username" th:placeholder="#{login.username}" required="" autofocus="" value="nicky" />
    <label class="sr-only" th:text="#{login.password} ">Password</label>
    <input type="password" class="form-control" name="password" th:placeholder="#{login.password}" required="" value="123" />
    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" value="remember me"  th:text="#{login.remember}" />
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btnName}">Sign in</button>
    <p class="mt-5 mb-3 text-muted">© 2019</p>
    <a class="btn btn-sm" th:href="@{/login()} " target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >中文</a>
    <a class="btn btn-sm" th:href="@{/login()} " target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow" >English</a>
</form>
</body>
</html>
           

整個認證伺服器完整結構

OAuth2(一):了解OAuth2授權方式,入門項目搭建

5.啟動認證伺服器,進行測試

a.通路預設授權端點 /oauth/authorize ,擷取授權碼

localhost:7777/oauth/authorize?client_id=cms&response_type=code 注意,若後面攜帶了redirect_uri,則必須與配置的redirectUris完全一緻,這裡是  http://127.0.0.1:8084/cms/thirdLogin,

localhost:7777/oauth/authorize?client_id=cms&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A8084%2Fcms%3F%2FthirdLogin%3FauthType%3Dtest
           

如果攜帶的redirect_uri不一緻,則會提示invalid_grant錯誤:

OAuth2(一):了解OAuth2授權方式,入門項目搭建

b.傳回到登入頁面,進行登入

OAuth2(一):了解OAuth2授權方式,入門項目搭建

c.登入成功,擷取到 code

OAuth2(一):了解OAuth2授權方式,入門項目搭建

d.postman模拟後端通路令牌端點 /oauth/token,擷取 access_token

OAuth2(一):了解OAuth2授權方式,入門項目搭建

e.通路令牌解析端點,模拟校驗資源服務令牌解析

OAuth2(一):了解OAuth2授權方式,入門項目搭建

二、搭建 Resource Server(非Client端) 1.建立 spring boot 工程,引入依賴

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR6</spring-cloud.version>
    <oauth2.version>2.2.4.RELEASE</oauth2.version>
</properties>

<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>
        <version>${oauth2.version}</version>
    </dependency>
</dependencies>
           

配置 application.yaml 檔案,聲明 check_token

security:
  oauth2:
    resource:
      token-info-uri: http://localhost:7777/oauth/check_token
      user-info-uri: http://localhost:7777/user

server:
  port: 7000
           

2.配置 ResourceServerConfiguration 繼承 ResourceServerConfigurerAdapter

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

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

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
            .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) // 匿名使用者通路無權限資源時的異常
            .accessDeniedHandler(new CustomAccessDeineHandler())    // 認證過的使用者通路無權限資源時的異常
            .and()
            .authorizeRequests().anyRequest().authenticated()
            .and()
            .csrf().disable().httpBasic();
    }
}
           

編寫兩個異常處理類

/**
* 用來解決認證過的使用者通路無權限資源時的異常
* AccessDeniedHandler 預設實作是 AccessDeniedHandlerImpl。該類對異常的處理是傳回403錯誤碼。
*/
public class CustomAccessDeineHandler implements AccessDeniedHandler {


    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/javascript;charset=utf-8");
        response.getWriter().print("沒有通路權限!");
    }
}


/**
* AuthenticationEntryPoint 用來解決匿名使用者通路無權限資源時的異常
* AuthenticationEntryPoint 預設實作是 LoginUrlAuthenticationEntryPoint, 該類的處理是轉發或重定向到登入頁面
*/
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/javascript;charset=utf-8");
        response.getWriter().print("您還未進行登入,沒有通路權限!");
    }
}
           

3.編寫使用者 Controller 資源接口

@RestController
public class UserController {

    @GetMapping("/get_resource")
    public String getResource() {
        return "OK";
    }

    @GetMapping("/user")
    public User getUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String name = authentication.getName();
        User user = new User();
        user.setName(name);
        return user;
    }

    @GetMapping("/me")
    public User getMe(Authentication authentication) {
        User user = new User();
        user.setName(authentication.getName());
        return user;
    }
}
           

4.啟動 Resource Server

4.1直接通路資源路徑,提示資源未認證

OAuth2(一):了解OAuth2授權方式,入門項目搭建

(未配置AuthenticationEntryPoint,出現上面的内容,否則是下面的内容!)

OAuth2(一):了解OAuth2授權方式,入門項目搭建

4.2攜帶上面postman擷取的 access_token 通路,成功通路到受限資源

OAuth2(一):了解OAuth2授權方式,入門項目搭建
OAuth2(一):了解OAuth2授權方式,入門項目搭建