spring boot 2.0 整合 oauth2 authorization code授權碼模式

weixin_34402408發表於2018-07-09

oauth2 authorization code 大致流程

  1. 使用者開啟客戶端後,客戶端要求使用者給予授權。
  2. 使用者同意給予客戶端授權。
  3. 客戶端使用授權得到的code,向認證伺服器申請token令牌。
  4. 認證伺服器對客戶端進行認證以後,確認無誤,同意發放令牌。
  5. 客戶端請求資源時攜帶token令牌,向資源伺服器申請獲取資源。
  6. 資源伺服器確認令牌無誤,同意向客戶端開放資源。

security oauth2 整合的核心配置類

  1. 授權認證服務配置 AuthorizationServerConfiguration
  2. security 配置 SecurityConfiguration

工程結構目錄

7434356-eb3678bb4db29ced.png
image.png

pom.xml

<artifactId>security_oauth2_authorization</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>security_oauth2_authorization</name>
<description>oauth2 authorization_code 授權模式</description>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.2.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <security-jwt.version>1.0.9.RELEASE</security-jwt.version>
    <jjwt.version>0.9.0</jjwt.version>
  <spring-cloud.version>Finchley.RC2</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <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.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-jwt</artifactId>
        <version>${security-jwt.version}</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

授權認證服務配置類 AuthorizationServerConfiguration

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private MyUserDetailsService userDetailsService;


    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        String secret = passwordEncoder.encode("secret");
        clients.inMemory() // 使用in-memory儲存
                .withClient("client") // client_id
                .secret(secret) // client_secret
                //.autoApprove(true)   //如果為true 則不會跳轉到授權頁面,而是直接同意授權返回code
                .authorizedGrantTypes("authorization_code","refresh_token") // 該client允許的授權型別
                .scopes("app"); // 允許的授權範圍
    }

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.authenticationManager(authenticationManager).userDetailsService(userDetailsService)
                .accessTokenConverter(accessTokenConverter())
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); //支援GET  POST  請求獲取token;
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                String userName = authentication.getUserAuthentication().getName();
                final Map<String, Object> additionalInformation = new HashMap<String, Object>();
                additionalInformation.put("user_name", userName);
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                OAuth2AccessToken token = super.enhance(accessToken, authentication);
                return token;
            }
        };
        //converter.setSigningKey("bcrypt");
        KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("kevin_key.jks"), "123456".toCharArray())
                .getKeyPair("kevin_key");
        converter.setKeyPair(keyPair);
        return converter;
    }
}

web 安全配置 WebSecurityConfig

@Order(10)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyUserDetailsService userDetailsFitService;
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/","/oauth/**","/login","/health", "/css/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsFitService).passwordEncoder(passwordEncoder());
        auth.parentAuthenticationManager(authenticationManagerBean());
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

}

url 註冊配置 MvcConfig

@Configuration
public class MvcConfig implements  WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login"); //自定義的登陸頁面
        registry.addViewController("/oauth/confirm_access").setViewName("oauth_approval"); //自定義的授權頁面
    }
}

# security 登陸認證 MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
        if ("admin".equalsIgnoreCase(name)) {
            User user = mockUser();
            return user;
        }
        return null;
    }

    private User mockUser() {
        Collection<GrantedAuthority> authorities = new HashSet<>();
        authorities.add(new SimpleGrantedAuthority("admin"));
        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        String pwd = passwordEncoder.encode("123456");
        User user = new User("admin",pwd,authorities);
        return user;
    }
}

自定義登陸頁面 login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
    <link href="https://cdn.bootcss.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">

</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <div class="portlet-body">
                <form class="form-horizontal" action="login" role="form" method="post">
                    <div class="form-group">
                        <label for="username" class="col-md-2 control-label">username</label>
                        <div class="col-md-6">
                            <input type="text" class="form-control" id="username" name="username" placeholder="username"> </div>
                    </div>
                    <div class="form-group">
                        <label for="password" class="col-md-2 control-label">Password</label>
                        <div class="col-md-6">
                            <input type="password" class="form-control" id="password" name="password" placeholder="password"> </div>
                    </div>

                    <div class="form-group">
                        <div class="col-md-offset-2 col-md-10">
                            <button type="submit" class="btn blue">登陸</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>

</div>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"  crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/bootstrap/4.1.1/js/bootstrap.min.js" crossorigin="anonymous"></script>
</body>
</html>

自定義授權頁面 oauth_approval.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>授權</title>
    <link href="https://cdn.bootcss.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
</head>
<body>
<div class="container">
    <h2>你是否授權client_id=client訪問你的受保護資源?</h2>
    <div style="height: 20px;"></div>
    <form id="confirmationForm" name="confirmationForm"
          action="../oauth/authorize" method="post">

    <input name="user_oauth_approval" value="true" type="hidden"/>
    <button class="btn btn-primary" type="submit">Approve</button>
    </form>
    <div style="height: 20px;"></div>
    <form id="denyForm" name="confirmationForm"
          action="../oauth/authorize" method="post">
        <input name="user_oauth_approval" value="false" type="hidden"/>
        <button class="btn btn-primary" type="submit">Deny</button>
    </form>
</div>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"  crossorigin="anonymous"></script>
<script src="https://cdn.bootcss.com/bootstrap/4.1.1/js/bootstrap.min.js" crossorigin="anonymous"></script>
</body>
</html>

application.yml

server:
  port: 18084

spring:
  application:
    name: oauth2-server   # 應用名稱

  thymeleaf:
        prefix: classpath:/templates/

logging:
  level:
    org.springframework.security: DEBUG

1. 訪問oauth2 服務

http://localhost:18084/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://baidu.com&state=123

  client_id:第三方應用在授權伺服器註冊的 Id

  response_type:固定值 code。

  redirect_uri:授權伺服器授權重定向哪兒的 URL。

  scope:許可權

  state:隨機字串,可以省略

如果未登陸則出現登入頁面,輸入使用者名稱:admin 密碼:123456 登陸系統


7434356-b0a36e835311700d.png
image.png

2. 成功登陸後自動跳轉到授權頁面

7434356-66ad07d63e917e29.png
image.png
  1. 點選“approve” 同意授權獲取code返回:https://www.baidu.com/?code=1LerDZ&state=123
    7434356-5e9607d2832400a4.png
    image.png
  2. 點選“deny” 拒絕授權 返回:https://www.baidu.com/?error=access_denied&error_description=User%20denied%20access&state=123
    7434356-bd3f587437841364.png
    image.png

3. 攜帶授權之後返回的code 獲取token

http://localhost:18084/oauth/token?client_id=client&grant_type=authorization_code&redirect_uri=http://baidu.com&code=bzxoHn
如果沒有登陸會出現登陸頁面 輸入:賬號 client 密碼 secret 登陸

7434356-df665cacc91798e7.png
image.png

這裡的賬號和密碼 是我們註冊的 client_id 和 client_secret
7434356-931150f2352d878c.png
image.png

成功登陸後獲取token


7434356-5fe860b6f06a12bc.png
image.png

4.攜帶toekn 訪問資源

http://localhost:18084/users/list?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiKiJdLCJ1c2VyX25hbWUiOiJxaWFvcnVsYWkiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNTI5MjMxMzE3LCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl0sImp0aSI6Ijc1Mzg3OWMyLTI4ZDktNDgxMy04YTAxLWZkNzQ4OGNlOWRkMCIsImNsaWVudF9pZCI6ImNsaWVudF8yIn0.V9I2lBYKk7sNsygj_bwrJZF06A8LhZx2x_MHmapppGE

demo地址:

相關文章