SpringBoot整合OAuth2授权码模式实操记录

213 阅读6分钟

OAuth2授权码模式

授权服务器

  1. 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();
        }
    }
    
  2. TokenStore配置,设置将token保存在内存中或者Redis

    @Configuration
    public class AccessTokenConfig {
        @Autowired
        RedisConnectionFactory redisConnectionFactory;
        /**
         * 将token存储在内存中
         */
        @Bean
        TokenStore tokenStore() {
            return new RedisTokenStore(redisConnectionFactory);
        }
    }
    
  3. 授权服务器要做两方面的检验,一方面是校验客户端,另一方面则是校验用户。配置客户端的 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

  1. 依赖

    <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>
    
  2. Redis配置

    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=123
    
  3. 修改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";
    }
}