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 表示授权范围
你好,高永强
http://localhost:8082/index.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
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
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
Token存入Redis
- 依赖
org.springframework.boot
spring-boot-starter-data-redis
redis.clients
jedis
2.9.0 - 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管理-定时刷新
http://localhost:8082/index.html">第三方授权登录
/**
* @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
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
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
@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";
}
}