Spring Cloud集成OAuth2

Spring Cloud集成OAuth2

什么是OAuth2

OAuth2 是一個(gè)授權(quán)框架,或稱(chēng)授權(quán)標(biāo)準(zhǔn),它可以使第三方應(yīng)用程序或客戶(hù)端獲得對(duì)HTTP服務(wù)上(例如 Google,GitHub )用戶(hù)帳戶(hù)信息的有限訪(fǎng)問(wèn)權(quán)限。OAuth 2 通過(guò)將用戶(hù)身份驗(yàn)證委派給托管用戶(hù)帳戶(hù)的服務(wù)以及授權(quán)客戶(hù)端訪(fǎng)問(wèn)用戶(hù)帳戶(hù)進(jìn)行工作

具體介紹請(qǐng)參考大牛文章:阮一峰的《理解OAuth 2.0》

OAuth2能做什么

OAuth2 允許用戶(hù)提供一個(gè)令牌,而不是用戶(hù)名和密碼來(lái)訪(fǎng)問(wèn)他們存放在特定服務(wù)提供者的數(shù)據(jù)。每一個(gè)令牌授權(quán)一個(gè)特定的網(wǎng)站(例如,視頻編輯網(wǎng)站)在特定的時(shí)段(例如,接下來(lái)的2小時(shí)內(nèi))內(nèi)訪(fǎng)問(wèn)特定的資源(例如僅僅是某一相冊(cè)中的視頻)。這樣,OAuth允許用戶(hù)授權(quán)第三方網(wǎng)站訪(fǎng)問(wèn)他們存儲(chǔ)在另外的服務(wù)提供者上的信息,而不需要分享他們的訪(fǎng)問(wèn)許可或他們數(shù)據(jù)的所有內(nèi)容

舉個(gè)栗子:比如我們常用的微信公眾號(hào),當(dāng)我們第一次打開(kāi)公眾號(hào)中網(wǎng)頁(yè)的時(shí)候會(huì)彈出是否允許授權(quán),當(dāng)我們點(diǎn)擊授權(quán)的時(shí)候,公眾號(hào)網(wǎng)站就能獲取到我們的頭像和昵稱(chēng)等信息。這個(gè)過(guò)程就是通過(guò)OAuth2 來(lái)實(shí)現(xiàn)的

怎么去使用OAuth2

OAuth2是一套協(xié)議,我們可以根據(jù)協(xié)議來(lái)自己編寫(xiě)程序來(lái)實(shí)現(xiàn)OAuth2的功能,當(dāng)然我們也可以通過(guò)一些框架來(lái)實(shí)現(xiàn)。由于我們的技術(shù)棧是Spring Cloud,那我們就開(kāi)看看Spring Cloud怎么集成OAuth2。

Spring Cloud集成OAuth2

引入POM依賴(lài)

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

編寫(xiě)登錄服務(wù)

因?yàn)槭跈?quán)是用戶(hù)的授權(quán),所以必須有用戶(hù)登錄才能授權(quán),這里我們使用spring security來(lái)實(shí)現(xiàn)登錄功能

建表語(yǔ)句

CREATE TABLE `ts_users` (
  `user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用戶(hù)ID',
  `user_name` varchar(50) NOT NULL COMMENT '用戶(hù)名',
  `user_pwd` varchar(100) NOT NULL COMMENT '用戶(hù)密碼',
  PRIMARY KEY (`user_id`) USING BTREE,
  UNIQUE KEY `idx_user_name` (`user_name`) USING BTREE,
  KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='用戶(hù)信息表';

Security配置:SecurityConfig

package com.walle.gatewayserver.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author zhangjiapeng
 * @Package com.walle.gatewayserver.config
 * @Description: ${todo}
 * @date 2019/1/10 16:05
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 用戶(hù)驗(yàn)證服務(wù)
    @Autowired
    @Qualifier("userDetailServiceImpl")
    private UserDetailsService userDetailsService;

    // 加密方式 security2.0以后 密碼無(wú)法明文保存,必須要經(jīng)過(guò)加密
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

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

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/hello");
    }


    // 配置攔截規(guī)則
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().formLogin()
                .and().csrf().disable()
                .httpBasic();
    }
}

登錄實(shí)現(xiàn):UserDetailServiceImpl

package com.walle.gatewayserver.service.impl;

import com.walle.common.entity.UserInfo;
import com.walle.gatewayserver.dao.UserInfoDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * @author zhangjiapeng
 * @Package com.walle.gatewayserver.service.impl
 * @Description: ${todo}
 * @date 2019/1/10 15:54
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {


    @Autowired
    private UserInfoDao userInfoDao;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根據(jù)用戶(hù)名查找用戶(hù)
        UserInfo userInfo = userInfoDao.getByUserName(username);
        if(userInfo == null){
            throw  new  UsernameNotFoundException("用戶(hù)不存在");
        }
        // 權(quán)限
        GrantedAuthority authority = new SimpleGrantedAuthority("admin");
        List<GrantedAuthority> authorities = new ArrayList<>(1);
        authorities.add(authority);

        UserDetails userDetails = new User(userInfo.getUsername(),passwordEncoder.encode(userInfo.getPassword()),authorities);
        // 返回用戶(hù)信息,注意加密
        return userDetails;
    }
}

暴露接口,這里有兩個(gè)接口,一個(gè)開(kāi)放給web,一個(gè)開(kāi)放給android

@RestController
@Slf4j
public class UserInfoController {

    @GetMapping("/user/web")
    public String web(){
        return  "hello web";
    }

    @GetMapping("/user/android")
    public String android(){
        return  "hello android";
    }
}

啟動(dòng)服務(wù):訪(fǎng)問(wèn)http://localhost:9001/user/web,然后會(huì)看到登錄界面

1547452036997.png

輸入賬號(hào)密碼后看到

1547452089041.png

訪(fǎng)問(wèn)http://localhost:9001/user/android

1547452126286.png

這時(shí)候我們的登錄用戶(hù)可以訪(fǎng)問(wèn)到所有的資源,但是我們想讓web登錄的用戶(hù)只能看到web,android的用戶(hù)只能看到android。我們通過(guò)OAuth2來(lái)實(shí)現(xiàn)這個(gè)功能

編寫(xiě)授權(quán)服務(wù)

package com.walle.gatewayserver.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.InMemoryClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

import java.util.concurrent.TimeUnit;

/**
 * @author zhangjiapeng
 * @Package com.walle.gatewayserver.config
 * @Description: ${todo}
 * @date 2019/1/10 16:39
 */

@Configuration
@EnableAuthorizationServer
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailServiceImpl")
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;


    @Autowired
    private AuthenticationManager authenticationManager;

    @Bean
    public InMemoryTokenStore tokenStore(){
        return new InMemoryTokenStore();
    }


    @Bean
    public InMemoryClientDetailsService clientDetails() {
        return new InMemoryClientDetailsService();
    }

    // 配置token
    @Bean
    @Primary
    public DefaultTokenServices tokenService(){
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(clientDetails());
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30));
        return tokenServices;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore())
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)
                .tokenServices(tokenService());
    }

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

    // 設(shè)置客戶(hù)端信息
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("web")
                .scopes("web")
                .secret(passwordEncoder.encode("web"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://www.baidu.com")
                .and().withClient("android")
                .scopes("android")
                .secret(passwordEncoder.encode("android"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .redirectUris("http://www.baidu.com");
    }
}

這里是通過(guò)內(nèi)存模式配置了兩個(gè)客戶(hù)端

客戶(hù)端:web 密碼: web scopes: web

客戶(hù)端:android 密碼: web scopes: android

配置資源服務(wù)

package com.walle.gatewayserver.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

/**
 * @author zhangjiapeng
 * @Package com.walle.gatewayserver.config
 * @Description: ${todo}
 * @date 2019/1/10 16:29
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .and()
                    .requestMatchers().antMatchers("/user/**")
                .and()
                    .authorizeRequests()
                        .antMatchers("/user/web").access("#oauth2.hasScope('web')")
                                       .antMatchers("/user/android").access("#oauth2.hasScope('android')")
                        .anyRequest().permitAll();

    }
}

這里我們可以看到,我們通過(guò)配置資源服務(wù)攔截所有的/user/**的請(qǐng)求,然后請(qǐng)求/user/android必須有scope=android

這時(shí)候在登錄訪(fǎng)問(wèn)下http://localhost:9001/user/android

1547452947310.png

這時(shí)候我們看到報(bào)錯(cuò)頁(yè)面,需要驗(yàn)證才能訪(fǎng)問(wèn)。

獲取授權(quán)

訪(fǎng)問(wèn)http://localhost:9001/oauth/authorize?client_id=android&response_type=code&redirect_uri=http://www.baidu.com

然后跳轉(zhuǎn)到一個(gè)授權(quán)頁(yè)面

1547453423313.png

是不是跟微信很像,這里說(shuō)是否授權(quán)web,我們選擇Approve ,然后頁(yè)面跳轉(zhuǎn)到了

https://www.baidu.com/?code=XOCtGr

我們拿到了一個(gè)code,然后我們通過(guò)code去獲取access_token

POST訪(fǎng)問(wèn)http://localhost:9001/oauth/token?clientId=android&grant_type=authorization_code&code=A9bCN5&redirect_uri=http://www.baidu.com

1547453517990.png

這樣我們就獲得了access_token

這時(shí)候我們?cè)L問(wèn)http://localhost:9001/user/android?access_token=59cf521c-026f-4df1-974e-3e4bfc42e432

看到


1547453588529.png

OK,android可以訪(fǎng)問(wèn)了,我們?cè)囋噖eb能不能訪(fǎng)問(wèn)呢,訪(fǎng)問(wèn)http://localhost:9001/user/web?access_token=59cf521c-026f-4df1-974e-3e4bfc42e432

1547453650333.png

提示我們不合適的scope,這樣我們就實(shí)現(xiàn)了不同客戶(hù)端訪(fǎng)問(wèn)不同資源的權(quán)限控制

完!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容