Spring Cloud集成OAuth2
什么是OAuth2
OAuth2 是一個授權(quán)框架橙凳,或稱授權(quán)標準,它可以使第三方應用程序或客戶端獲得對HTTP服務上(例如 Google笑撞,GitHub )用戶帳戶信息的有限訪問權(quán)限岛啸。OAuth 2 通過將用戶身份驗證委派給托管用戶帳戶的服務以及授權(quán)客戶端訪問用戶帳戶進行工作
具體介紹請參考大牛文章:阮一峰的《理解OAuth 2.0》
OAuth2能做什么
OAuth2 允許用戶提供一個令牌,而不是用戶名和密碼來訪問他們存放在特定服務提供者的數(shù)據(jù)茴肥。每一個令牌授權(quán)一個特定的網(wǎng)站(例如坚踩,視頻編輯網(wǎng)站)在特定的時段(例如,接下來的2小時內(nèi))內(nèi)訪問特定的資源(例如僅僅是某一相冊中的視頻)瓤狐。這樣瞬铸,OAuth允許用戶授權(quán)第三方網(wǎng)站訪問他們存儲在另外的服務提供者上的信息,而不需要分享他們的訪問許可或他們數(shù)據(jù)的所有內(nèi)容
舉個栗子:比如我們常用的微信公眾號芬首,當我們第一次打開公眾號中網(wǎng)頁的時候會彈出是否允許授權(quán)赴捞,當我們點擊授權(quán)的時候,公眾號網(wǎng)站就能獲取到我們的頭像和昵稱等信息郁稍。這個過程就是通過OAuth2 來實現(xiàn)的
怎么去使用OAuth2
OAuth2是一套協(xié)議,我們可以根據(jù)協(xié)議來自己編寫程序來實現(xiàn)OAuth2的功能胜宇,當然我們也可以通過一些框架來實現(xiàn)耀怜。由于我們的技術(shù)棧是Spring Cloud,那我們就開看看Spring Cloud怎么集成OAuth2桐愉。
Spring Cloud集成OAuth2
引入POM依賴
<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>
編寫登錄服務
因為授權(quán)是用戶的授權(quán)财破,所以必須有用戶登錄才能授權(quán),這里我們使用spring security來實現(xiàn)登錄功能
建表語句
CREATE TABLE `ts_users` (
`user_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
`user_name` varchar(50) NOT NULL COMMENT '用戶名',
`user_pwd` varchar(100) NOT NULL COMMENT '用戶密碼',
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='用戶信息表';
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 {
// 用戶驗證服務
@Autowired
@Qualifier("userDetailServiceImpl")
private UserDetailsService userDetailsService;
// 加密方式 security2.0以后 密碼無法明文保存从诲,必須要經(jīng)過加密
@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();
}
}
登錄實現(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ù)用戶名查找用戶
UserInfo userInfo = userInfoDao.getByUserName(username);
if(userInfo == null){
throw new UsernameNotFoundException("用戶不存在");
}
// 權(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);
// 返回用戶信息左痢,注意加密
return userDetails;
}
}
暴露接口,這里有兩個接口系洛,一個開放給web俊性,一個開放給android
@RestController
@Slf4j
public class UserInfoController {
@GetMapping("/user/web")
public String web(){
return "hello web";
}
@GetMapping("/user/android")
public String android(){
return "hello android";
}
}
啟動服務:訪問http://localhost:9001/user/web,然后會看到登錄界面
輸入賬號密碼后看到
訪問http://localhost:9001/user/android
這時候我們的登錄用戶可以訪問到所有的資源描扯,但是我們想讓web登錄的用戶只能看到web定页,android的用戶只能看到android。我們通過OAuth2來實現(xiàn)這個功能
編寫授權(quán)服務
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()");
}
// 設置客戶端信息
@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");
}
}
這里是通過內(nèi)存模式配置了兩個客戶端
客戶端:web 密碼: web scopes: web
客戶端:android 密碼: web scopes: android
配置資源服務
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();
}
}
這里我們可以看到绽诚,我們通過配置資源服務攔截所有的/user/**的請求典徊,然后請求/user/android必須有scope=android
這時候在登錄訪問下http://localhost:9001/user/android
這時候我們看到報錯頁面杭煎,需要驗證才能訪問。
獲取授權(quán)
然后跳轉(zhuǎn)到一個授權(quán)頁面
是不是跟微信很像卒落,這里說是否授權(quán)web羡铲,我們選擇Approve ,然后頁面跳轉(zhuǎn)到了
https://www.baidu.com/?code=XOCtGr
我們拿到了一個code儡毕,然后我們通過code去獲取access_token
這樣我們就獲得了access_token
這時候我們訪問http://localhost:9001/user/android?access_token=59cf521c-026f-4df1-974e-3e4bfc42e432
看到
OK犀勒,android可以訪問了,我們試試web能不能訪問呢妥曲,訪問http://localhost:9001/user/web?access_token=59cf521c-026f-4df1-974e-3e4bfc42e432
提示我們不合適的scope贾费,這樣我們就實現(xiàn)了不同客戶端訪問不同資源的權(quán)限控制
完!