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ì)看到登錄界面

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

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

這時(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

這時(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è)面

是不是跟微信很像,這里說(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

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

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

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