oauth2的运行流程
- 用户打开客户端以后,客户端要求用户给予授权。
- 用户同意给予客户端授权。
- 客户端使用上一步获得的授权,向认证服务器申请令牌。
- 认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
- 客户端使用令牌,向资源服务器申请获取资源。
- 资源服务器确认令牌无误,同意向客户端开放资源。
四种模式
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
其中授权码模式是功能最完整、流程最严密的授权模式。
(1)用户访问客户端,后者将前者导向认证服务器,假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(2)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌:GET /oauth/token?response_type=code&client_id=demo&redirect_uri=重定向页面链接。请求成功返回code授权码,一般有效时间是10分钟。
(3)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。POST /oauth/token?response_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=重定向页面链接。
直接开启代码
1、创建项目
父模块authorization-demo依赖springboot包含了两个子模块客户端client及模拟的微信认证服务端wechat
2、maven依赖
子模块client简单的web项目,仅仅依赖web组件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
wechat模块加上security集成oauth2和简单页面模板引擎thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
3、客户端代码
启动类配置一个RestTemplate和服务端微信通讯
package com.varcode.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* Created by hlin on 2021/2/18
*/
@SpringBootApplication
public class ClientApplication {
public static void main(String[] args) {
SpringApplication.run(ClientApplication.class, args);
}
@Bean
RestTemplate restTemplate(){
return new RestTemplate();
}
}
一个回调controller,用于微信redirect的地址,获取授权码code
package com.varcode.client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* Created by hlin on 2021/2/18
*/
@RestController
public class WechatController {
@Autowired
RestTemplate restTemplate;
@RequestMapping("/client/wechat/redirect")
public String getToken(@RequestParam String code){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params= new LinkedMultiValueMap<>();
params.add("grant_type","authorization_code");
params.add("code",code);
params.add("client_id","demo");
params.add("client_secret","secret");
params.add("redirect_uri","http://localhost:8081/client/wechat/redirect");
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
ResponseEntity<String> response = restTemplate.postForEntity("http://localhost:8080/oauth/token", requestEntity, String.class);
String token = response.getBody();
return token;
}
}
配置文件一个当前服务的端口 server.port=8081
4、wechat服务端代码
启动类
package com.varcode.wechat;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Created by hlin on 2021/2/18
*/
@SpringBootApplication
public class WechatApplication {
public static void main(String[] args) {
SpringApplication.run(WechatApplication.class, args);
}
}
security配置类
package com.varcode.wechat;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
/**
* Created by hlin on 2021/2/18
*/
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 创建两个微信用户
manager.createUser(User.withUsername("wx_test17726").password("123456").authorities("USER").build());
manager.createUser(User.withUsername("wx_test548956").password("123456").authorities("USER").build());
return manager;
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
requestMatchers()
// 必须登录过的用户才可以进行 oauth2 的授权码申请
.antMatchers("/", "/home","/login","/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login")
.and()
.httpBasic()
.disable()
.exceptionHandling()
.accessDeniedPage("/login?authorization_error=true")
.and()
.csrf()
.requireCsrfProtectionMatcher(new AntPathRequestMatcher("/oauth/authorize"))
.disable();
}
}
oauth2配置类
package com.varcode.wechat;
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.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
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.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;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
/**
* Created by hlin on 2021/2/18
*/
@Configuration
public class OAuth2Configuration {
private static final String WECHAT_RESOURCE_ID = "wechat";
@Configuration
@EnableResourceServer()
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(WECHAT_RESOURCE_ID).stateless(true);
// 如果关闭 stateless,则 accessToken 使用时的 session id 会被记录,后续请求不携带 accessToken 也可以正常响应
// resources.resourceId(QQ_RESOURCE_ID).stateless(false);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatchers()
// 保险起见,防止被主过滤器链路拦截
.antMatchers("/wechat/**").and()
.authorizeRequests()
.antMatchers("/wechat/user/**").access("#oauth2.hasScope('user_info')")
.antMatchers("/wechat/group/**").access("#oauth2.hasScope('group_info')")
.anyRequest().authenticated();
}
}
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// @formatter:off
clients.inMemory().withClient("demo")
.resourceIds(WECHAT_RESOURCE_ID)
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT")
.scopes("user_info", "group_info")
.secret("secret")
.redirectUris("http://localhost:8081/client/wechat/redirect")
.autoApprove(true)
.autoApprove("user_info");
//.and()多个资源认证的客户端
}
@Bean
public ApprovalStore approvalStore() {
TokenApprovalStore store = new TokenApprovalStore();
store.setTokenStore(tokenStore());
return store;
}
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
// 需要使用 redis 的话,放开这里
// return new RedisTokenStore(redisConnectionFactory);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.realm(WECHAT_RESOURCE_ID).allowFormAuthenticationForClients();
}
}
}
mvc配置,几个模板页面的映射地址
package com.varcode.wechat;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Created by hlin on 2021/2/18
*/
@Configuration
public class SpringMvcConfiguration extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
服务端保护的资源接口
package com.varcode.wechat;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by hlin on 2021/2/18
*/
@RestController
@RequestMapping("/wechat")
public class WechatController {
@RequestMapping("/user/{id}")
public String userInfo(@PathVariable String id){
//模拟数据库查询结果
if("wx_test17726".equals(id)){
return "微信昵称:我要出去浪,微信号:wx_test17726,性别:男,年龄:22,头像:灰色头像";
}
if("wx_test548956".equals(id)){
return "微信昵称:快来抓住我,微信号:wx_test548956,性别:女,年龄:16,头像:非主流头像";
}
return null;
}
@RequestMapping("/group")
public String groupInfo(){
//模拟数据库查询结果
return "微信群名称:撸代码,群号:wxq_test145263,群人数:180";
}
}
配置文件
server.port=8080
# security过滤器顺序稍微靠前
spring.security.filter.order=3
# 需要使用 redis 的话,放开这里
#spring.redis.host=127.0.0.1
#spring.redis.database=0
三个静态测试页面,templates目录下
hello.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello Oauth2!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
</body>
</html>
home.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Oauth2 Example Home Page</title>
</head>
<body>
<h1>Start!</h1>
<p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Oauth2 Example Login Page</title>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
5、测试认证流程
分别启动client和wechat服务
1、浏览器访问地址:http://localhost:8080/wechat/user/wx_test17726
需要认证提示
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<oauth>
<error_description>Full authentication is required to access this resource</error_description>
<error>unauthorized</error>
</oauth>
2、获取授权码
http://localhost:8080/oauth/authorize?client_id=demo&response_type=code&redirect_uri=http://localhost:8081/client/wechat/redirect跳转登录页面,根据配置是需要登录才可以获取授权码信息的,相当于用户手动允许登录授权
3、输入配置的模拟用户名和密码wx_test17726,123456 进入client回调redirect接口获取access_token:
{"access_token":"8a087fae-ae29-4235-ba94-5cd29141e485","token_type":"bearer","refresh_token":"bb1761e7-4ce9-479b-8430-790aaf63cd52","expires_in":43199,"scope":"user_info group_info"}
redirect_uri 执行 302 重定向,并且带上生成的 code,注意重定向到的是 8001 端口,这个时候已经是另外一个应用了,即client
地址栏code申请一次过期,access_token有时效,当天申请也是有次数限制,需客户端加入缓存redis等
4、携带access_token访问受保护的资源接口
至此整个oauth2授权码模式流程结束!