OAuth2授权码模式
授权服务器
-
SpringSecurity基本配置,创建用户和角色
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("johnny").password(new BCryptPasswordEncoder().encode("123")).roles("user") .and() .withUser("john").password(new BCryptPasswordEncoder().encode("123")).roles("admin"); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().formLogin(); } } -
TokenStore配置,设置将token保存在内存中或者Redis
@Configuration public class AccessTokenConfig { @Autowired RedisConnectionFactory redisConnectionFactory; /** * 将token存储在内存中 */ @Bean TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); } } -
授权服务器要做两方面的检验,一方面是校验客户端,另一方面则是校验用户。配置客户端的 id,secret、资源 id、授权类型、授权范围以及重定向 uri。配置Token 是否支持刷新、Token 的存储位置、Token 的有效期以及刷新 Token 的有效期
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired TokenStore tokenStore; @Autowired ClientDetailsService clientDetailsService; /** * 授权服务器要做两方面的检验,一方面是校验客户端,另一方面则是校验用户, * 校验用户,SpringSecurity已经配置了,这里就是配置校验客户端。 * 客户端的信息我们可以存在数据库中,这其实也是比较容易的,和用户信息存到数据库中类似, * 但是这里为了简化代码,我还是将客户端信息存在内存中, * 这里我们分别配置了客户端的 id,secret、资源 id、授权类型、授权范围以及重定向 uri。 * 授权类型四种,四种之中不包含 refresh_token 这种类型, * 但是在实际操作中,refresh_token 也被算作一种。 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("johnny") .secret(new BCryptPasswordEncoder().encode("123")) .resourceIds("res1") .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("all") .redirectUris("http://localhost:8082/index.html"); } /** * AuthorizationServerSecurityConfigurer 用来配置令牌端点的安全约束, * 也就是这个端点谁能访问,谁不能访问。checkTokenAccess 是指一个 Token 校验的端点, * 这个端点我们设置为可以直接访问 * (在后面,当资源服务器收到 Token 之后,需要去校验 Token 的合法性,就会访问这个端点)。 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.checkTokenAccess("permitAll()").allowFormAuthenticationForClients(); } /** * AuthorizationServerEndpointsConfigurer 这里用来配置令牌的访问端点和令牌服务。 * authorizationCodeServices用来配置授权码的存储,这里我们是存在在内存中, * tokenServices 用来配置令牌的存储,即 access_token 的存储位置,这里我们也先存储在内存中。 * 授权码是用来获取令牌的,使用一次就失效,令牌则是用来获取资源的 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authorizationCodeServices(authorizationCodeServices()) .tokenServices(authorizationServerTokenServices()); } /** * authorizationCodeServices用来配置授权码的存储,这里我们是存在在内存中 */ @Bean AuthorizationCodeServices authorizationCodeServices() { return new InMemoryAuthorizationCodeServices(); } /** * tokenServices 这个 Bean 主要用来配置 Token 的一些基本信息, * 例如 Token 是否支持刷新、Token 的存储位置、Token 的有效期以及刷新 Token 的有效期等等。 * Token 有效期这个好理解,刷新 Token 的有效期我说一下,当 Token 快要过期的时候,我们需要获取一个新的 Token, * 在获取新的 Token 时候,需要有一个凭证信息,这个凭证信息不是旧的 Token,而是另外一个 refresh_token,这个 refresh_token 也是有有效期的。 */ @Bean AuthorizationServerTokenServices authorizationServerTokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 3); defaultTokenServices.setAccessTokenValiditySeconds(60 * 60 * 2); defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(tokenStore); defaultTokenServices.setClientDetailsService(clientDetailsService); return defaultTokenServices; } }
资源服务器
配置 access_token 的校验地址、client_id、client_secret 这三个信息,当用户来资源服务器请求资源时,会携带上一个 access_token,通过这里的配置,就能够校验出 token 是否正确
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
/**
* kenServices 我们配置了一个 RemoteTokenServices 的实例,
* 这是因为资源服务器和授权服务器是分开的,资源服务器和授权服务器是放在一起的,就不需要配置 RemoteTokenServices 了
* RemoteTokenServices 中我们配置了 access_token 的校验地址、client_id、client_secret 这三个信息,
* 当用户来资源服务器请求资源时,会携带上一个 access_token,
* 通过这里的配置,就能够校验出 token 是否正确等
*/
@Bean
RemoteTokenServices remoteTokenServices() {
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
remoteTokenServices.setClientId("johnny");
remoteTokenServices.setClientSecret("123");
return remoteTokenServices;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("res1").tokenServices(remoteTokenServices());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated();
}
}
//资源响应API
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/admin/hello")
public String helloAdmin(){
return "hello,Admin";
}
}
第三方应用
点击超链接就可以实现第三方登录,超链接的参数如下:
- client_id 客户端 ID,根据我们在授权服务器中的实际配置填写
- response_type 表示响应类型,这里是 code 表示响应一个授权码
- redirect_uri 表示授权成功后的重定向地址,这里表示回到第三方应用的首页
- scope 表示授权范围
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
你好,高永强
<!--到授权服务器获取授权码-->
<a href="http://localhost:8080/oauth/authorize?client_id=johnny&response_type=code&scope=all&redirect_uri=http://localhost:8082/index.html">第三方授权登录</a>
<h1 th:text="${msg}"></h1>
</body>
</html>
@Controller
public class HelloController {
@Autowired
TokenTask tokenTask;
@Autowired
RestTemplate restTemplate;
@GetMapping("/index.html")
/**
* 正常来说,access_token 我们可能需要一个定时任务去维护,
* 不用每次请求页面都去获取,定期去获取最新的 access_token 即可
*/
public String hello(String code,Model model){
model.addAttribute("msg",tokenTask.getData(code));
return "index";
}
}
@Component
@EnableScheduling
public class TokenTask {
@Autowired
RestTemplate restTemplate;
public String access_token = "";
public String refresh_token = "";
/**
* 根据授权码获取Token和refreshToken
*/
public String getData(String code) {
if ("".equals(access_token) && code != null) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("code", code);
map.add("client_id", "johnny");
map.add("client_secret", "123");
map.add("redirect_uri", "http://localhost:8082/index.html");
map.add("grant_type", "authorization_code");
Map<String, String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
access_token = resp.get("access_token");
refresh_token = resp.get("refresh_token");
return getResourceFromServer();
} else {
return getResourceFromServer();
}
}
/**
* 携带Token请求服务器资源
*/
public String getResourceFromServer() {
try {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + access_token);
HttpEntity<Object> httpEntity = new HttpEntity<>(headers);
ResponseEntity<String> entity = restTemplate.exchange("http://localhost:8081/admin/hello", HttpMethod.GET, httpEntity, String.class);
return entity.getBody();
} catch (RestClientException e) {
return "资源请求失败";
}
}
/**
* 每隔30分钟刷新token
*/
@Scheduled(cron = "0 0/30 * * * ?")
public void tokenTask() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", "johnny");
map.add("client_secret", "123");
map.add("refresh_token", refresh_token);
map.add("grant_type", "refresh_token");
Map<String, String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
access_token = resp.get("access_token");
refresh_token = resp.get("refresh_token");
System.out.println(">>>>>>>>定时刷新Token Time:" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));
System.out.println("access_token = " + access_token);
System.out.println("refresh_token = " + refresh_token);
}
}
Token存入Redis
-
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> -
Redis配置
spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password=123 -
修改TokenStore实例
@Configuration public class AccessTokenConfig { @Autowired RedisConnectionFactory redisConnectionFactory; //返回RedisTokenStore @Bean TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); } }
Token管理-定时刷新
<!--到授权服务器获取授权码-->
<a href="http://localhost:8080/oauth/authorize?client_id=johnny&response_type=code&scope=all&redirect_uri=http://localhost:8082/index.html">第三方授权登录</a>
/**
* @PackageName: com.johnny.clientapp
* @ClassName:
* @Description: Token自动刷新
* @author: Johnny
* @date: 2020/4/26
*/
@Component
@EnableScheduling
public class TokenTask {
@Autowired
RestTemplate restTemplate;
public String access_token = "";
public String refresh_token = "";
/**
* 根据授权码获取Token和refreshToken
*/
public String getData(String code) {
if ("".equals(access_token) && code != null) {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("code", code);
map.add("client_id", "johnny");
map.add("client_secret", "123");
map.add("redirect_uri", "http://localhost:8082/index.html");
map.add("grant_type", "authorization_code");
Map<String, String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
access_token = resp.get("access_token");
refresh_token = resp.get("refresh_token");
return getResourceFromServer();
} else {
return getResourceFromServer();
}
}
/**
* 携带Token请求服务器资源
*/
public String getResourceFromServer() {
try {
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + access_token);
HttpEntity<Object> httpEntity = new HttpEntity<>(headers);
ResponseEntity<String> entity = restTemplate.exchange("http://localhost:8081/admin/hello", HttpMethod.GET, httpEntity, String.class);
return entity.getBody();
} catch (RestClientException e) {
return "资源请求失败";
}
}
/**
* 每隔30分钟刷新token
*/
@Scheduled(cron = "0 0/30 * * * ?")
public void tokenTask() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("client_id", "johnny");
map.add("client_secret", "123");
map.add("refresh_token", refresh_token);
map.add("grant_type", "refresh_token");
Map<String, String> resp = restTemplate.postForObject("http://localhost:8080/oauth/token", map, Map.class);
access_token = resp.get("access_token");
refresh_token = resp.get("refresh_token");
System.out.println(">>>>>>>>定时刷新Token Time:" + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));
System.out.println("access_token = " + access_token);
System.out.println("refresh_token = " + refresh_token);
}
}
@Controller
public class HelloController {
@Autowired
TokenTask tokenTask;
@Autowired
RestTemplate restTemplate;
@GetMapping("/index.html")
/**
* 正常来说,access_token 我们可能需要一个定时任务去维护,
* 不用每次请求页面都去获取,定期去获取最新的 access_token 即可
*/
public String hello(String code,Model model){
model.addAttribute("msg",tokenTask.getData(code));
return "index";
}
}