一、授权服务器的定位

一言而概之:就是为客户端产生一个Token

如图:

java token信息存储在哪里合适_redis

二、授权服务器的实现

2.1 添加依赖

<!--        服务发现-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

2.2 配置文件

spring:
  application:
    name: authorization-server
  cloud:
    nacos:
      discovery:
        server-addr: nacos-server:8848
server:
  port: 9999

2.3 启动类

@SpringBootApplication
public class AuthorizationApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthorizationApplication.class, args);
    }
}

2.4配置类

2.4.1 授权服务器的配置

@EnableAuthorizationServer
@Configuration
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    public PasswordEncoder passwordEncoder ;

    @Autowired
    private AuthenticationManager authenticationManager ;

    @Autowired
    private UserDetailsService userDetailsService ;
    /**
     * 配置第三方客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("coin-api")
                .secret(passwordEncoder.encode("coin-secret"))
                .scopes("all")
                .authorizedGrantTypes("password","refresh")
                .accessTokenValiditySeconds(24 * 7200)
                .refreshTokenValiditySeconds(7 *  24 * 7200) ;
    }

    /**
     * 设置授权管理器和UserDetailsService
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(new InMemoryTokenStore())
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService) ;
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

2.4.2 Web 安全的配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 注入一个验证管理器
     *
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * 资源的放行
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); // 关闭scrf
        http.authorizeRequests().anyRequest().authenticated();
    }


    /**
     * 创建一个测试的UserDetail
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
        User user = new User("admin", "123456", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"))) ;
        inMemoryUserDetailsManager.createUser(user);
        return inMemoryUserDetailsManager;
    }

    /**
     * 注入密码的验证管理器
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

2.5 获取token测试

第一步:

java token信息存储在哪里合适_java token信息存储在哪里合适_02


第二步:

java token信息存储在哪里合适_源服务器_03

三、验证授权服务

3.1 设置资源服务器

当集成oauth2后,每个服务都要被设置为资源服务器,此时这个服务就会被oauth监管,否则将会被forbidden。

先将授权服务器变成资源服务器作测试

@EnableResourceServer
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
}

3.2 在授权服务器里准备userinfo接口

@RestController
public class UserInfoController {

    /**
     * 获取该用户的对象
     * @param principal
     * @return
     */
    @GetMapping("/user/info")
    public Principal usrInfo(Principal principal){ // 此处的principal 由OAuth2.0 框架自动注入
        // 原理就是:利用Context概念,将授权用户放在线程里面,利用ThreadLocal来获取当前的用户对象 
//        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return principal;
    }
}

3.3 使用token换取user对象

第一步:获取一个token

java token信息存储在哪里合适_服务器_04


第二步:使用Token 换用户对象

java token信息存储在哪里合适_java token信息存储在哪里合适_05

四、将oauth2与gateway集合

首先将授权服务器注册到nacos注册中心中

4.1 修改启动类

@SpringBootApplication
@EnableDiscoveryClient
public class AuthServerApplication {
	public static void main(String[] args) {
		SpringApplication.run(AuthServerApplication.class, args);
	}
}

4.2 修改网关配置文件

#在routes的路由规则下面添加以下配置
- id: auth-server-router
  uri: lb://auth-server
  predicates:
    - Path=/oauth/**

4.3 测试

与第三部分基本相同,只是此时是把请求打在网关上。

Token共享问题

我的token 目前存储在内存里面:

java token信息存储在哪里合适_源服务器_06


注意:此时看似没有问题,但是这个用户数据是保存在内存当中的,当我们访问另外一个资源服务器的时候,这个用户数据就无法被得到。也就是说,当我们仅仅只有一台authorization-server 时,没有任何问题,但是当我们使用多台authorization-server时,由于内存数据无法共享,故用户登录的数据仅仅保存在一台服务器里面,这就会导致某台授权服务器会误判“是否用户登录”这个问题。

java token信息存储在哪里合适_java token信息存储在哪里合适_07

五、使用redis解决用户数据共享问题

将之前数据存储在内存里面的问题解决掉,现在直接把登录成功的数据存储在redis里面:

java token信息存储在哪里合适_服务器_08

5.1 添加依赖

<!--        redis-->
 <dependency>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>
<!--这里的依赖可以是2.2.4.RELEASE-->

5.2 修改配置文件

server:
  port: 9999
spring:
  application:
    name: authorization-server
  cloud:
    nacos:
      discovery:
        server-addr: nacos-server:8848
  redis:
    host: redis-server
    port: 6379
    password: ${你的密码}

5.3 使用RedisTokenStore

修改我们之前的AuthorizationServerConfig配置类:

@EnableAuthorizationServer // 开启授权服务器的功能
@Configuration
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder ;

    @Autowired
    private AuthenticationManager authenticationManager ;

    @Autowired
    private UserDetailsService userDetailsService ;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory ;


    /**
     *  添加第三方的客户端
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("coin-api") // 第三方客户端的名称
                .secret(passwordEncoder.encode("coin-secret")) //  第三方客户端的密钥
                .scopes("all") //第三方客户端的授权范围
                .accessTokenValiditySeconds(3600) // token的有效期
                .refreshTokenValiditySeconds(7*3600);// refresh_token的有效期
        super.configure(clients);
    }

    /**
     * 配置验证管理器,UserdetailService
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(redisTokenStore());
    }
    public TokenStore redisTokenStore(){
        return new RedisTokenStore(redisConnectionFactory) ;
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

5.4 测试多资源服务器下,登录数据共享

开启两个oauth服务

第一步:获取token

java token信息存储在哪里合适_源服务器_09


第二步:换取登录对象

java token信息存储在哪里合适_java token信息存储在哪里合适_10


资源服务器和授权服务的交互示意

java token信息存储在哪里合适_redis_11

六、使用jwt来做token的存储

上面的方案里面,我们提到了让资源服务器不再访问授权服务器,那会存在什么问题呢?

资源服务器访问授权服务的本质在于2点:

第一点:资源服务器无法验证token的正确性,因为它没有存储token

第二点:资源服务要通过授权服务器来换取用户(token 换 user)。

我们来推演:资源服务器当前只能得到用户给他的token,我们能做的改造有限:

第一步:若我们将用户的基本信息存储在token 里面呢?

第二步:定义一种加密规则,让资源服务器也能去判断该token的正确性。

这样,我们的JWT就上场了。看看JWT的定义:

java token信息存储在哪里合适_源服务器_12

6.1 生成私钥和公钥

生成私钥:

keytool -genkeypair -alias coinexchange -keyalg RSA -keypass coinexchange -keystore coinexchange.jks -validity 365 -storepass coinexchange

java token信息存储在哪里合适_java token信息存储在哪里合适_13


具体命令和参数:Keytool 是一个java提供的证书管理工具

java token信息存储在哪里合适_源服务器_14


此时在你所在的目录下就会产生一个jks文件了

java token信息存储在哪里合适_源服务器_15


解析公钥:

keytool -list -rfc --keystore coinexchange.jks | openssl x509 -inform pem -pubkey

输入密钥库口令为: coinexchange

java token信息存储在哪里合适_源服务器_16


将解析出来的公钥放在一个文件的文件里面:

MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzUF4JoV8OzJCJ5hEQPF
e+4/adK1XbF3NXX7F68OvArw8jPCGevy+tv0ATODozNDQb9hDgHf1geTTx1uUi13
PubbyTgNjJbCjUX6z01NLEC5DRQFNCC53JgytIoyH93WQUBA+oX7Gn3/0pAXMP74
2v8DjoVvElkcIhuxJ8T2QTpEPiqDTiSvuTlN4/C6ERDcLNmveTUkcwblacrOD4fx
AqvfJLqwUlQNZD/X2ByuNx6/qRZonFeYIhbS6DwX8j+X/cPebFc+phVAie+GtW39
PtX3gOidtl2HfSZFqo4CidrbPJxp0vzIRwQl/r5i/Vle6sY61MMs4hRmm8fskJ3f
RQIDAQAB
-----END PUBLIC KEY-----

java token信息存储在哪里合适_源服务器_17

6.2 修改配置文件和pom

把之前有关redis的操作全部注掉

yml文件

#  redis:
#    port: 6379
#    host: localhost
#    password:
<!--<dependency>-->
		<!--	<groupId>org.springframework.boot</groupId>-->
		<!--	<artifactId>spring-boot-starter-data-redis</artifactId>-->
		<!--	<version>2.2.4.RELEASE</version>-->
<!--</dependency>-->

6.3 将私钥文件复制到resource下

java token信息存储在哪里合适_java token信息存储在哪里合适_18

6.4 修改授权服务器配置类

@EnableAuthorizationServer //开启授权服务器
@Configuration
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    @Resource
    public PasswordEncoder passwordEncoder ;

    @Resource
    private AuthenticationManager authenticationManager ;

    @Resource
    private UserDetailsService userDetailsService ;

    //@Resource
    //private RedisConnectionFactory redisConnectionFactory ;
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //从内存中加载
        clients.inMemory()
                //第三方客户端的名称username
                .withClient("coin-api")
                //第三方客户端的密码
                .secret(passwordEncoder.encode("coin-secret"))
                //作用域
                .scopes("all")
                //采用密码刷新
                .authorizedGrantTypes("password","refresh")
                //token的过期时间
                .accessTokenValiditySeconds(24 * 7200)
                //刷新token的过期时间
                .refreshTokenValiditySeconds(7 *  24 * 7200) ;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //配置端点的存储区域,当前是存储在内存当中
        endpoints.tokenStore(jwtTokenStore())
                //配置授权管理器
                .authenticationManager(authenticationManager)
                //用来校验身份,如用户名密码
                .userDetailsService(userDetailsService)
                //把认证通过的用户信息加载到jwt当中去
                .tokenEnhancer(jwtAccessTokenConverter());
                //.tokenStore(redisTokenStore());
    }

    //private TokenStore redisTokenStore() {
    //    return new RedisTokenStore(redisConnectionFactory) ;
    //}

    public TokenStore jwtTokenStore(){
        JwtTokenStore jwtTokenStore = new JwtTokenStore(jwtAccessTokenConverter());
        return jwtTokenStore;
    }

    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
        //先加载classpath下的私钥
        ClassPathResource classPathResource = new ClassPathResource("coinexchange.jks");
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource, "coinexchange".toCharArray());
        tokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair("coinexchange","coinexchange".toCharArray()));
        return tokenConverter;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

6.5 获取token测试

java token信息存储在哪里合适_源服务器_19


登录jwt.io解析token信息

java token信息存储在哪里合适_redis_20