一、授权服务器的定位
一言而概之:就是为客户端产生一个Token
如图:
二、授权服务器的实现
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测试
第一步:
第二步:
三、验证授权服务
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
第二步:使用Token 换用户对象
四、将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 目前存储在内存里面:
注意:此时看似没有问题,但是这个用户数据是保存在内存当中的,当我们访问另外一个资源服务器的时候,这个用户数据就无法被得到。也就是说,当我们仅仅只有一台authorization-server 时,没有任何问题,但是当我们使用多台authorization-server时,由于内存数据无法共享,故用户登录的数据仅仅保存在一台服务器里面,这就会导致某台授权服务器会误判“是否用户登录”这个问题。
五、使用redis解决用户数据共享问题
将之前数据存储在内存里面的问题解决掉,现在直接把登录成功的数据存储在redis里面:
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
第二步:换取登录对象
资源服务器和授权服务的交互示意
六、使用jwt来做token的存储
上面的方案里面,我们提到了让资源服务器不再访问授权服务器,那会存在什么问题呢?
资源服务器访问授权服务的本质在于2点:
第一点:资源服务器无法验证token的正确性,因为它没有存储token
第二点:资源服务要通过授权服务器来换取用户(token 换 user)。
我们来推演:资源服务器当前只能得到用户给他的token,我们能做的改造有限:
第一步:若我们将用户的基本信息存储在token 里面呢?
第二步:定义一种加密规则,让资源服务器也能去判断该token的正确性。
这样,我们的JWT就上场了。看看JWT的定义:
6.1 生成私钥和公钥
生成私钥:
keytool -genkeypair -alias coinexchange -keyalg RSA -keypass coinexchange -keystore coinexchange.jks -validity 365 -storepass coinexchange
具体命令和参数:Keytool 是一个java提供的证书管理工具
此时在你所在的目录下就会产生一个jks文件了
解析公钥:
keytool -list -rfc --keystore coinexchange.jks | openssl x509 -inform pem -pubkey
输入密钥库口令为: coinexchange
将解析出来的公钥放在一个文件的文件里面:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzUF4JoV8OzJCJ5hEQPF
e+4/adK1XbF3NXX7F68OvArw8jPCGevy+tv0ATODozNDQb9hDgHf1geTTx1uUi13
PubbyTgNjJbCjUX6z01NLEC5DRQFNCC53JgytIoyH93WQUBA+oX7Gn3/0pAXMP74
2v8DjoVvElkcIhuxJ8T2QTpEPiqDTiSvuTlN4/C6ERDcLNmveTUkcwblacrOD4fx
AqvfJLqwUlQNZD/X2ByuNx6/qRZonFeYIhbS6DwX8j+X/cPebFc+phVAie+GtW39
PtX3gOidtl2HfSZFqo4CidrbPJxp0vzIRwQl/r5i/Vle6sY61MMs4hRmm8fskJ3f
RQIDAQAB
-----END PUBLIC KEY-----
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下
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测试
登录jwt.io解析token信息