一、OAuth2.0是OAuth協(xié)議的延續(xù)版本,但不向后兼容OAuth 1.0即完全廢止了OAuth1.0。 OAuth 2.0關(guān)注客戶端開發(fā)者的簡易性。要么通過組織在資源擁有者和HTTP服務(wù)商之間的被批準(zhǔn)的交互動作代表用戶,要么允許第三方應(yīng)用代表用戶獲得訪問的權(quán)限。同時為Web應(yīng)用,桌面應(yīng)用和手機,和起居室設(shè)備提供專門的認證流程。
二、使用場景:
1、自己開發(fā)應(yīng)用時,需要獲取其他應(yīng)用的資源。比如:使用QQ登錄,然后獲取QQ頭像等信息
2、SSO認證服務(wù)器,在自己開發(fā)應(yīng)用時使用統(tǒng)一的認證過程,不需要單獨重寫重寫認證體系
三、概念
?。?)?Third-party application:第三方應(yīng)用程序,本文中又稱"客戶端"(client)。
?。?)HTTP service:HTTP服務(wù)提供商,本文中簡稱"服務(wù)提供商"。
?。?)Resource Owner:資源所有者,本文中又稱"用戶"(user)。
?。?)User Agent:用戶代理,本文中就是指瀏覽器。
?。?)Authorization server:認證服務(wù)器,即服務(wù)提供商專門用來處理認證的服務(wù)器。
?。?)Resource server:資源服務(wù)器,即服務(wù)提供商存放用戶生成的資源的服務(wù)器。它與認證服務(wù)器,可以是同一臺服務(wù)器,也可以是不同的服務(wù)器。
OAuth在"客戶端"與"服務(wù)提供商"之間,設(shè)置了一個授權(quán)層(authorization layer)。"客戶端"不能直接登錄"服務(wù)提供商",只能登錄授權(quán)層,以此將用戶與客戶端區(qū)分開來。"客戶端"登錄授權(quán)層所用的令牌(token),與用戶的密碼不同。用戶可以在登錄的時候,指定授權(quán)層令牌的權(quán)限范圍和有效期。
"客戶端"登錄授權(quán)層以后,"服務(wù)提供商"根據(jù)令牌的權(quán)限范圍和有效期,向"客戶端"開放用戶儲存的資料。
四、模式運行流程

(A)用戶打開客戶端以后,客戶端要求用戶給予授權(quán)。
(B)用戶同意給予客戶端授權(quán)。
(C)客戶端使用上一步獲得的授權(quán),向認證服務(wù)器申請令牌。
(D)認證服務(wù)器對客戶端進行認證以后,確認無誤,同意發(fā)放令牌。
(E)客戶端使用令牌,向資源服務(wù)器申請獲取資源。
(F)資源服務(wù)器確認令牌無誤,同意向客戶端開放資源。
五、授權(quán)模式
授權(quán)碼模式(authorization code)
簡化模式(implicit)
密碼模式(resource owner password credentials)
客戶端模式(client credentials)
1)授權(quán)碼模式

(A)用戶訪問客戶端,后者將前者導(dǎo)向認證服務(wù)器。
?。˙)用戶選擇是否給予客戶端授權(quán)。
?。–)假設(shè)用戶給予授權(quán),認證服務(wù)器將用戶導(dǎo)向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權(quán)碼。
?。―)客戶端收到授權(quán)碼,附上早先的"重定向URI",向認證服務(wù)器申請令牌。這一步是在客戶端的后臺的服務(wù)器上完成的,對用戶不可見。
?。‥)認證服務(wù)器核對了授權(quán)碼和重定向URI,確認無誤后,向客戶端發(fā)送訪問令牌(access token)和更新令牌(refresh token)。
2)簡化模式

(B)用戶決定是否給于客戶端授權(quán)。
?。–)假設(shè)用戶給予授權(quán),認證服務(wù)器將用戶導(dǎo)向客戶端指定的"重定向URI",并在URI的Hash部分包含了訪問令牌。
(D)瀏覽器向資源服務(wù)器發(fā)出請求,其中不包括上一步收到的Hash值。
?。‥)資源服務(wù)器返回一個網(wǎng)頁,其中包含的代碼可以獲取Hash值中的令牌。
(F)瀏覽器執(zhí)行上一步獲得的腳本,提取出令牌。
?。℅)瀏覽器將令牌發(fā)給客戶端。
3)密碼模式

?。ˋ)用戶向客戶端提供用戶名和密碼。
(B)客戶端將用戶名和密碼發(fā)給認證服務(wù)器,向后者請求令牌。
?。–)認證服務(wù)器確認無誤后,向客戶端提供訪問令牌。
4)客戶端模式

(A)客戶端向認證服務(wù)器進行身份認證,并要求一個訪問令牌。
?。˙)認證服務(wù)器確認無誤后,向客戶端提供訪問令牌。
六、授權(quán)碼模式例子
這里說明一下這里主要只通過授權(quán)碼模式來講解oauth2的使用過程。
授權(quán)碼模式(authorization code)是功能最完整、流程最嚴(yán)密的授權(quán)模式。它的特點就是通過客戶端的后臺服務(wù)器,與"服務(wù)提供商"的認證服務(wù)器進行互動。
簡化模式(implicit grant type)不通過第三方應(yīng)用程序的服務(wù)器,直接在瀏覽器中向認證服務(wù)器申請令牌,跳過了"授權(quán)碼"這個步驟,因此得名。所有步驟在瀏覽器中完成,令牌對訪問者是可見的,且客戶端不需要認證。
密碼模式(Resource Owner Password Credentials Grant)中,用戶向客戶端提供自己的用戶名和密碼。客戶端使用這些信息,向"服務(wù)商提供商"索要授權(quán)。
客戶端模式(Client Credentials Grant)指客戶端以自己的名義,而不是以用戶的名義,向"服務(wù)提供商"進行認證。嚴(yán)格地說,客戶端模式并不屬于OAuth框架所要解決的問題。在這種模式中,用戶直接向客戶端注冊,客戶端以自己的名義要求"服務(wù)提供商"提供服務(wù),其實不存在授權(quán)問題。
相對來說授權(quán)碼的方式使用上面,是非常嚴(yán)謹?shù)?。不存在,其他模式的相對弊病?/p>
7、代碼部分
1)需要的依賴
<parent>
? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? <artifactId>spring-boot-starter-parent</artifactId>
? ? ? ? <version>2.0.0.RELEASE</version>
? ? </parent>
? ? <dependencies>
? ? ? ? <dependency>
? ? ? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? ? ? <artifactId>spring-boot-starter-web</artifactId>
? ? ? ? </dependency>
? ? ? ? <dependency>
? ? ? ? ? ? <groupId>org.springframework.boot</groupId>
? ? ? ? ? ? <artifactId>spring-boot-starter-security</artifactId>
? ? ? ? </dependency>
? ? ? ? <dependency>
? ? ? ? ? ? <groupId>org.springframework.security.oauth.boot</groupId>
? ? ? ? ? ? <artifactId>spring-security-oauth2-autoconfigure</artifactId>
? ? ? ? ? ? <version>2.1.2.RELEASE</version>
? ? ? ? </dependency>
? ? </dependencies>
2)認證服務(wù)器

主要配置:SecurityConfiguration、AuthServerConfiguration
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
? ? @Autowired
? ? private BCryptPasswordEncoder passwordEncoder;
? ? @Override
? ? protected void configure(HttpSecurity http) throws Exception {
? ? ? ? http
? ? ? ? ? ? .csrf().disable()
? ? ? ? ? ? .exceptionHandling()
? ? ? ? .and()
? ? ? ? ? ? .authorizeRequests()
? ? ? ? ? ? .anyRequest().authenticated()
? ? ? ? .and()
? ? ? ? ? ? .formLogin();
? ? }
? ? @Override
? ? protected void configure(AuthenticationManagerBuilder builder) throws Exception {
? ? ? ? //內(nèi)存用戶不多解釋
? ? ? ? builder.inMemoryAuthentication()
? ? ? ? ? ? ? ? .withUser("admin")
? ? ? ? ? ? ? ? .password(passwordEncoder.encode("admin"))
? ? ? ? ? ? ? ? .roles("ADMIN");
? ? }
? ? @Override
? ? @Bean
? ? public AuthenticationManager authenticationManagerBean() throws Exception {
? ? ? ? return super.authenticationManagerBean();
? ? }
@Bean @Override protected UserDetailsService userDetailsService() {? ? return super.userDetailsService(); }
}
@Configuration
@EnableAuthorizationServer
public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {
? ? @Autowired
? ? private BCryptPasswordEncoder passwordEncoder;
? ? @Autowired
? ? private AuthenticationManager authenticationManager;
? ? @Override
? ? public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
? ? ? ? //這里client使用存在模式,可以實際過程調(diào)整為jdbc的方式
? ? ? ? //這里說明一下,redirectUris的連接可以是多個,這里通過access_token都可以訪問的
? ? ? ? //簡單點,就是授權(quán)的過程
? ? ? ? clients.inMemory()
? ? ? ? ? ? ? ? .withClient("client")
? ? ? ? ? ? ? ? .secret(passwordEncoder.encode("secret"))
? ? ? ? ? ? ? ? .authorizedGrantTypes("authorization_code", "refresh_token")
? ? ? ? ? ? ? ? .scopes("All")
? ? ? ? ? ? ? ? .autoApprove(true)
? ? ? ? ? ? ? ? .redirectUris("http://localhost:9001/login", "http://localhost:9002/login", "http://localhost:9003/authorize/login");
? ? }
? ? @Override
? ? public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
? ? ? ? //權(quán)限控制
? ? ? ? security.tokenKeyAccess("permitAll()")
? ? ? ? ? ? ? ? .checkTokenAccess("isAuthenticated()")
? ? ? ? ? ? ? ? .allowFormAuthenticationForClients();
? ? }
? ? @Override
? ? public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
? ? ? ? //認證體系使用security的方式
? ? ? ? endpoints.authenticationManager(authenticationManager);
//允許調(diào)用方式
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); endpoints.userDetailsService(userDetailsService);
? ? }
說明:這里我為了更好的區(qū)分,把認證服務(wù)器和資源服務(wù)器分開的,實際上可以使用認證服務(wù)器作為資源服務(wù)器
server:
? port: 9000
? servlet:
? ? context-path: /auth #這里一定要加上contextPath,這個坑自己體會吧
yaml配置
3)資源服務(wù)器

主要配置:ResourceServerConfiguration、application.yaml
/**
* 資源服務(wù)器的配置也很簡單
* 主要是EnableResourceServer,以及資源的控制
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
? ? @Override
? ? public void configure(HttpSecurity http) throws Exception {
? ? ? ? http
? ? ? ? ? ? .csrf().disable()
? ? ? ? ? ? .exceptionHandling()
? ? ? ? .and()
? ? ? ? ? ? .authorizeRequests()
? ? ? ? ? ? .anyRequest().authenticated();
? ? }
}
server:
? port: 9002
security:
? oauth2:
? ? client:
? ? ? client-id: client
? ? ? client-secret: secret
? ? ? access-token-uri: http://localhost:9000/auth/oauth/token
? ? ? user-authorization-uri: http://localhost:9000/auth/oauth/authorize
? ? resource:
? ? ? token-info-uri: http://localhost:9000/auth/oauth/check_token
說明:資源服務(wù)器主要用于資源攔截,需要獲取授權(quán)碼才能訪問
4)sso客戶端

主要配置:SecurityConfiguration、application.yaml
/**
* 這里使用的是sso的方式,可以用于單點登錄
* 構(gòu)造方式也很簡單,主要是sso的配置
*/
@Configuration
@EnableOAuth2Sso
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
? ? @Override
? ? protected void configure(HttpSecurity http) throws Exception {
? ? ? ? http
? ? ? ? ? ? .csrf().disable()
? ? ? ? ? ? .authorizeRequests()
? ? ? ? ? ? .anyRequest().authenticated();
? ? }
}
server:
? port: 9001
security:
? oauth2:
? ? client:
? ? ? client-id: client
? ? ? client-secret: secret
? ? ? access-token-uri: http://localhost:9000/auth/oauth/token
? ? ? user-authorization-uri: http://localhost:9000/auth/oauth/authorize
? ? resource:
? ? ? token-info-uri: http://localhost:9000/auth/oauth/check_token
? ? ? #user-info-uri: http://localhost:9002/user/me
? ? ? #這里兩種獲取用戶的方式,都可以。但是只能存在一種
5)客戶端:當(dāng)然瀏覽器可以為一種客戶端,自己開發(fā)的應(yīng)用也可以為客戶端
瀏覽器:
a、獲取授權(quán)碼
oauth/authorize?response_type=code&client_id=&redirect_uri=
本文中:
http://localhost:9000/auth/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://localhost:9002/loginresponse_type=code&client_id=client&redirect_uri=http://localhost:9002/login
b、通過code獲取令牌
oauth/token?client_id=&client_secret=&grant_type=authorization_code&redirect_uri=&code=
本文中:
http://localhost:9000/auth/oauth/token?client_id=client&client_secret=secret&grant_type=authorization_code&redirect_uri=http://localhost:9002/login&code=jrbBZS
獲取的對應(yīng)值
{
? ? "access_token": "06c1db9b-aac3-4a9a-acaf-56f5a5d0ea21",
? ? "token_type": "bearer",
? ? "refresh_token": "046d3fe7-52c4-43e5-902a-673ab2b0d3d4",
? ? "expires_in": 42981,
? ? "scope": "All"
}
access_token:表示訪問令牌,必選項。
token_type:表示令牌類型,該值大小寫不敏感,必選項,可以是bearer類型或mac類型。
expires_in:表示過期時間,單位為秒。如果省略該參數(shù),必須其他方式設(shè)置過期時間。
refresh_token:表示更新令牌,用來獲取下一次的訪問令牌,可選項。
scope:表示權(quán)限范圍,如果與客戶端申請的范圍一致,此項可省略。
c、更新令牌
oauth/token?grant_type=refresh_token&refresh_token=
http://localhost:9000/auth/oauth/token?grant_type=refresh_token&refresh_token=046d3fe7-52c4-43e5-902a-673ab2b0d3d4
注意:在使用refresh_token刷新令牌的時候,需要在認證服務(wù)器上面設(shè)置

SecurityConfiguration加入UserDetailsService?
@Bean
? ? @Override
? ? protected UserDetailsService userDetailsService() {
? ? ? ? returnsuper.userDetailsService();
? ? }
AuthServerConfiguration也加入UserDetailsService?
@Autowired
? ? private UserDetailsService userDetailsService;
? ? @Override
? ? public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
? ? ? ? //認證體系使用security的方式
? ? ? ? endpoints.authenticationManager(authenticationManager);
? ? ? ? endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
? ? ? ? endpoints.userDetailsService(userDetailsService);
? ? }
單點登陸
//獲取用戶信息,說明這里主要目的就是通過資源服務(wù)器去獲取用戶信息
? ? ? ? ? ? Map principal = HttpUtils.doGet(resourceServerProperties.getUserInfoUri() + "?access_token=" + map.get("access_token"), Map.class);
? ? ? ? ? ? //這里通過本地登錄單點登錄
? ? ? ? ? ? String username = principal.get("name").toString();
? ? ? ? ? ? //如果用戶存在則不添加,這里如果生產(chǎn)應(yīng)用中,可以更具規(guī)則修改
? ? ? ? ? ? if (userRepository.findByUsername(username) == null) {
? ? ? ? ? ? ? ? Role role = roleRepository.findByRoleType(Role.RoleType.USER);
? ? ? ? ? ? ? ? User newUser = new User();
? ? ? ? ? ? ? ? newUser.setUsername(username);
? ? ? ? ? ? ? ? newUser.setPassword(passwordEncoder.encode(username));
? ? ? ? ? ? ? ? newUser.getRoles().add(role);
? ? ? ? ? ? ? ? userRepository.save(newUser);
? ? ? ? ? ? }
? ? ? ? ? ? //這里通過本地登錄的方式來獲取會話
? ? ? ? ? ? HttpHeaders httpHeaders = new HttpHeaders();
? ? ? ? ? ? httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
? ? ? ? ? ? LinkedMultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
? ? ? ? ? ? params.add("username", username);
? ? ? ? ? ? params.add("password", username);
? ? ? ? ? ? HttpEntity<LinkedMultiValueMap<String, ? extends Object>> httpEntity = new HttpEntity(params, httpHeaders);
? ? ? ? ? ? String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/login";
? ? ? ? ? ? ResponseEntity<Object> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Object.class);
? ? ? ? ? ? //將登錄后的header原本的給瀏覽器,這就是當(dāng)前瀏覽器的會話
? ? ? ? ? ? HttpHeaders headers = exchange.getHeaders();
? ? ? ? ? ? for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
? ? ? ? ? ? ? ? entry.getValue().stream().forEach(value -> response.addHeader(entry.getKey(), value));
? ? ? ? ? ? }
? ? ? ? ? ? //這個狀態(tài)是根據(jù)security的返回數(shù)據(jù)設(shè)定的
? ? ? ? ? ? response.setStatus(exchange.getStatusCode().value());
登錄的實現(xiàn)過程
@RestController
@RequestMapping("/authorize")
public class AuthorizedResource {
? ? @Autowired
? ? private AuthorizationCodeResourceDetails authorizationCodeResourceDetails;
? ? @Autowired
? ? private ResourceServerProperties resourceServerProperties;
? ? @Autowired
? ? private RestTemplate restTemplate;
? ? @Autowired
? ? private BCryptPasswordEncoder passwordEncoder;
? ? @Autowired
? ? private UserRepository userRepository;
? ? @Autowired
? ? private RoleRepository roleRepository;
? ? @RequestMapping("/login")
? ? public void login(String code, HttpServletRequest request, HttpServletResponse response) throws Exception {
? ? ? ? if (!StringUtils.isEmpty(code)) {
? ? ? ? ? ? LinkedMultiValueMap<String, Object> valueMap = new LinkedMultiValueMap<>();
? ? ? ? ? ? valueMap.add("client_id", authorizationCodeResourceDetails.getClientId());
? ? ? ? ? ? valueMap.add("client_secret", authorizationCodeResourceDetails.getClientSecret());
? ? ? ? ? ? valueMap.add("grant_type", authorizationCodeResourceDetails.getGrantType());
? ? ? ? ? ? valueMap.add("redirect_uri", authorizationCodeResourceDetails.getPreEstablishedRedirectUri());
? ? ? ? ? ? valueMap.add("code", code);
? ? ? ? ? ? Map<String, String> map = HttpUtils.doFrom(authorizationCodeResourceDetails.getAccessTokenUri(), valueMap, Map.class);
? ? ? ? ? ? System.out.println(map);
? ? ? ? ? ? //獲取用戶信息,說明這里主要目的就是通過資源服務(wù)器去獲取用戶信息
? ? ? ? ? ? Map principal = HttpUtils.doGet(resourceServerProperties.getUserInfoUri() + "?access_token=" + map.get("access_token"), Map.class);
? ? ? ? ? ? //這里通過本地登錄單點登錄
? ? ? ? ? ? String username = principal.get("name").toString();
? ? ? ? ? ? //如果用戶存在則不添加,這里如果生產(chǎn)應(yīng)用中,可以更具規(guī)則修改
? ? ? ? ? ? if (userRepository.findByUsername(username) == null) {
? ? ? ? ? ? ? ? Role role = roleRepository.findByRoleType(Role.RoleType.USER);
? ? ? ? ? ? ? ? User newUser = new User();
? ? ? ? ? ? ? ? newUser.setUsername(username);
? ? ? ? ? ? ? ? newUser.setPassword(passwordEncoder.encode(username));
? ? ? ? ? ? ? ? newUser.getRoles().add(role);
? ? ? ? ? ? ? ? userRepository.save(newUser);
? ? ? ? ? ? }
? ? ? ? ? ? //這里通過本地登錄的方式來獲取會話
? ? ? ? ? ? HttpHeaders httpHeaders = new HttpHeaders();
? ? ? ? ? ? httpHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
? ? ? ? ? ? LinkedMultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
? ? ? ? ? ? params.add("username", username);
? ? ? ? ? ? params.add("password", username);
? ? ? ? ? ? HttpEntity<LinkedMultiValueMap<String, ? extends Object>> httpEntity = new HttpEntity(params, httpHeaders);
? ? ? ? ? ? String url = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/login";
? ? ? ? ? ? ResponseEntity<Object> exchange = restTemplate.exchange(url, HttpMethod.POST, httpEntity, Object.class);
? ? ? ? ? ? //將登錄后的header原本的給瀏覽器,這就是當(dāng)前瀏覽器的會話
? ? ? ? ? ? HttpHeaders headers = exchange.getHeaders();
? ? ? ? ? ? for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
? ? ? ? ? ? ? ? entry.getValue().stream().forEach(value -> response.addHeader(entry.getKey(), value));
? ? ? ? ? ? }
? ? ? ? ? ? //這個狀態(tài)是根據(jù)security的返回數(shù)據(jù)設(shè)定的
? ? ? ? ? ? response.setStatus(exchange.getStatusCode().value());
? ? ? ? }
? ? }
}
工程文件配置

application.yaml
server:
? port: 9003
? servlet:
? ? session:
? ? ? cookie:
? ? ? ? name: ACCESS_SESSION
security:
? oauth2:
? ? client:
? ? ? client-id: client
? ? ? client-secret: secret
? ? ? grant-type: authorization_code
? ? ? access-token-uri: http://localhost:9000/auth/oauth/token
? ? ? user-authorization-uri: http://localhost:9000/auth/oauth/authorize
? ? ? pre-established-redirect-uri: http://localhost:9003/authorize/login
? ? resource:
? ? ? user-info-uri: http://localhost:9002/user/me
? ? sso:
? ? ? login-path: /authorize/login
spring:
? datasource:
? ? driver-class-name: com.mysql.jdbc.Driver
? ? url: jdbc:mysql://127.0.0.1:3306/model?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
? ? username: root
? ? password:
? jpa:
? ? hibernate:
? ? ? ddl-auto: update
? ? ? naming:
? ? ? ? physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
? ? ? ? implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
? ? show-sql: true
? ? database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
? ? database: mysql
github:?https://github.com/lilin409546297/security-oauth2-sso