下文在SpringCloud基础权限框架搭建(1)-Zuul整合SpringSecurityOAuth2(JWT)+Turbine的基础上,添加了配置中心(SpringCloudConfig)、消息总线(SpringCloudBus|RabbitMQ)、服务与服务间的信息传递(token传递/session共享)
相关部署问题见SpringCloud基础权限框架搭建(3):服务部署记录
1)SpringCloudConfig -
分布式系统的配置管理方案,包含了Client和Server两个部分,server提供配置文件的存储、以接口的形式将配置文件的内容提供出去,client通过接口获取数据、并依此数据初始化自己的应用。
2)SpringCloudBus -
管理和传播所有分布式项目中的消息中心,本质是利用了MQ的广播机制在分布式的系统中传播消息,目前常用的有Kafka和RabbitMQ
3)redis - 高性能的key-value数据库,具备丰富的数据结构,同时支持数据持久化
4)RabbitMQ - 实现 AMQP(高级消息队列协议)的消息中间件的一种,用于在分布式系统中存储转发消息
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.springcloud.book</groupId>
<artifactId>cloud1.0</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>cloud1.0-config-server</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 9090
spring:
application:
name: sc-config-server
cloud:
config:
server:
git:
uri: https://github.com/2578197547/SpringCloud1.0.git #配置git仓库的地址
#username: #git仓库的账号
#password: #git仓库的密码
search-paths: cloud1.0/config #git仓库地址下的相对地址,可以配置多个,使用,分割
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8761}/eureka/
instance:
prefer-ip-address: true
ConfigServerApplication
@SpringBootApplication
@EnableConfigServer
@EnableDiscoveryClient//配置本应用将使用服务注册和服务发现
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
在https://github.com/2578197547/SpringCloud1.0/tree/master/cloud1.0/config/
我上传了一个配置文件config-info-dev.yml,内容很简单仅供参考
cn:
springcloud:
book:
defaultUser: xielijie
这里我们做下测试
访问http://localhost:9090/config-info-dev.yml
再构建一个数据服务测试工程sc-data-service用于测试客户端访问配置中心
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.springcloud.book</groupId>
<artifactId>cloud1.0</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<name>cloud1.0-data-service</name>
<packaging>jar</packaging>
<artifactId>cloud1.0-data-service</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 需要通过oauth2进行权限配置的添加(1)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>cn.springcloud.book</groupId>
<artifactId>cloud1.0-common</artifactId>
<version>${parent.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- 需要通过oauth2进行权限配置的添加(2)-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
</project>
现在我们在git上添加配置文件application-dataService.yml
management:
security:
enabled: false
endpoints:
web:
exposure:
include: hystrix.stream
bootstrap.yml
注意:spring.cloud.config相关的属性必须配置在bootstrap.properties中,config部分内容才能被正确加载。因为config的相关配置会先于application.properties,而bootstrap.properties的加载先于application.properties
我在bootstrap.yml加载了工程依赖的配置文件application-dataService.yml和测试文件config-info-dev.yml
spring:
application:
name: sc-data-service
cloud:
config:
discovery:
enabled: true #开启Config服务发现支持
service-id: sc-config-server #指定server端的name,也就是server端spring.application.name的值
label: master
name: config-info,application
profile: dev,dataService
server:
port: 8099
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8761}/eureka/
instance:
prefer-ip-address: true
DataServiceApplication.java
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class DataServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DataServiceApplication.class, args);
}
}
ResourceServerConfiguration.java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
public static final String public_cert = "public.cert";
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource(public_cert);
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}catch (IOException e) {
throw new RuntimeException(e);
}
//使用converter.setVerifierKey(publicKey);会出现 Cannot convert access token to JSON
converter.setVerifier(new RsaVerifier(publicKey));
return converter;
}
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/actuator/hystrix.stream").permitAll()
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore());
}
}
映射的配置对象DataConfig.java
@Component
@RefreshScope // 使用该注解的类,会在接到SpringCloud配置中心配置刷新的时候,自动将新的配置更新到该类对应的字段中。
@ConfigurationProperties(prefix = "cn.springcloud.book")
public class DataConfig {
private String defaultUser;
public String getDefaultUser() {
return defaultUser;
}
public void setDefaultUser(String defaultUser) {
this.defaultUser = defaultUser;
}
}
DataController.java
客户端使用配置中心属性有两种方式,一种通过@ConfigurationProperties指定属性下的所有值注入某个对象,一种是通过@Value属性指定特定属性
@RestController
@RefreshScope // 使用该注解的类,会在接到SpringCloud配置中心配置刷新的时候,自动将新的配置更新到该类对应的字段中。
public class DataController {
@Autowired
HttpServletRequest httpServletRequest;
@Autowired
private DataConfig dataConfig;
@Value("${cn.springcloud.book.defaultUser}")
private String defaultUser;
//返回配置文件内配置的默认用户
@RequestMapping("/getService")
public String getService(@RequestParam(value = "request") String request){
return "hello:"+request;
}
//返回配置文件内配置的默认用户
@GetMapping("/getDefaultUser")
public String getDefaultUser(){
return "@ConfigurationProperties:"+dataConfig.getDefaultUser()+"|@Value:"+defaultUser;
}
//返回生产者数据
@RequestMapping("/getProviderData")
public List<String> getProviderData(){
List<String> provider = new ArrayList<String>();
provider.add("first");
provider.add("second");
provider.add("third");
return provider;
}
@RequestMapping("/getProviderDataByPost")
@ResponseBody
public List<String> getProviderDataByPost(@RequestParam String serviceName,@RequestParam String serviceValue){
List<String> provider = new ArrayList<String>();
provider.add(serviceName);
provider.add(serviceValue);
return provider;
}
}
获取完token访问http://localhost:8099/getDefaultUser?access_token=......
返回
配置中心整合成功,后续我们可以把各个服务的配置文件都整合到github上
实现了配置中心后我们可以集中管理各环境的配置文件,但是还有个问题,如果我们修改了配置文件为了让新的配置生效我们必须重启对应项目,这样处理相当费事,而springcloud已经给我们提供了解决方案,那就是使用spring-boot-starter-actuator
,spring-boot-starter-actuator
是一套监控系统健康情况的工具,它提供了刷新机制(/refresh
),我们在修改完配置文件后只要访问该接口便能实现动态刷新配置。
但是这种处理还是有一定问题,那就是我们每次修改完配置文件后就要调用对应客户端的/refresh
接口,如果一个服务实现负载均衡后有多个相同的服务服务就意味着我们每个服务都要调用一次,那有没有办法当我们修改配置文件后一次性给所有服务刷新配置呢,此时我们可以引入SpringCloudBus
(消息总线,利用MQ的广播机制在分布式的系统中传播消息,这里使用了RabbitMQ)
(图片与部分资料引用自springcloud(九):配置中心和消息总线(配置中心终结版))
pom.xml
添加
SpringCloudBus
引用
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
application.yml
添加rabbitmq相关配置,暴露暴露sc-config-server的/bus/refresh端点(springboot2.X版本需要指定暴露端点添加management.endpoints.web.exposure.include配置,我们这里直接暴露所有端点,注意这种方式我们需要在接口前添加/actuator/)
server:
port: 9090
spring:
application:
name: sc-config-server
cloud:
config:
server:
git:
uri: https://github.com/2578197547/SpringCloud1.0.git #配置git仓库的地址
#username: #git仓库的账号
#password: #git仓库的密码
search-paths: cloud1.0/config #git仓库地址下的相对地址,可以配置多个,使用,分割
bus:
trace:
enabled: true
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: testlocalhost
username: admin
password: admin
management:
security:
enabled: false
endpoints:
web:
exposure:
include: "*"
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8761}/eureka/
instance:
prefer-ip-address: true
pom.xml
添加spring-boot-starter-actuator和spring-cloud-starter-bus-amqp
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
application-dataService.yml
添加rabbitmq相关配置,暴露暴露sc-config-server的/refresh、/bus-refresh、/hystrix.stream等端点
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: testlocalhost
username: admin
password: admin
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8761}/eureka/
instance:
prefer-ip-address: true
management:
security:
enabled: false
endpoints:
web:
exposure:
include: refresh,bus-refresh,hystrix.stream
ResourceServerConfiguration
开放sc-config-server访问权限
package cn.springcloud.book.config;
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
public static final String public_cert = "public.cert";
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource(public_cert);
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}catch (IOException e) {
throw new RuntimeException(e);
}
//使用converter.setVerifierKey(publicKey);会出现 Cannot convert access token to JSON
converter.setVerifier(new RsaVerifier(publicKey));
return converter;
}
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/sc-user-server/**","/sc-auth-server/**","/sc-config-server/**","/actuator/hystrix.stream").permitAll()
.antMatchers("/**").authenticated();
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenStore(tokenStore());
}
}
依次启动sc-eureka-server、sc-config-server、sc-auth-server、sc-zuul-server、sc-user-server、sc-data-service
登陆完成后获取token
访问http://localhost:7777/sc-data-service/getDefaultUser?access_token=......
返回
修改config-info-dev.yml并提交
再次访问http://localhost:7777/sc-data-service/getDefaultUser?access_token=......
仍然返回@ConfigurationProperties:xielijie|@Value:xielijie,此时访问http://localhost:7777/sc-config-server/actuator/bus-refresh
(post请求)
等待响应完成再次访问getDefaultUser
接口发现配置已经刷新
/actuator/bus-refresh(post)
是刷新所有使用配置中心的客户端,也可以通过/actuator/bus-refresh?destination=customers:8000(post)
指定刷新服务
后续我们将项目部署到服务器上后可以将/actuator/bus-refresh
接口配置到github的WebHook,每当发生push事件,自动调用该接口,由此便可以实现配置中心修改实时刷新客户端
因为auth2的机制,我们统一使用令牌进行资源访问,因此我们在受限资源内都能获取到token,通过在token内添加我们的自定义信息便能实现在服务与服务间传递信息。在SpringCloud基础权限框架搭建(1)-Zuul整合SpringSecurityOAuth2(JWT)+Turbine中授权服务器sc-auth-server整合一节我们有提及实现方式(目前添加了userName、roles、organization等信息)
以下为代码实现
cloud1.0-auth-server工程
SysUserRepository.java
@Repository
@Transactional
public interface SysUserRepository extends CrudRepository<SysUser, Integer> {
@Query("select a from SysUser a where a.name=:name")
public SysUser getUserByName(@Param("name") String name);
}
SysRoleRepository.java
@Repository
public interface SysRoleRepository extends CrudRepository<SysRole, Integer> {
@Query(value="SELECT r.* FROM sys_user u "
+ "LEFT JOIN sys_role_user ru ON ru.sys_user_id = u.id "
+ "LEFT JOIN sys_role r ON r.id = ru.sys_role_id "
+ "WHERE u.name = :name",nativeQuery = true)
public List<SysRole> getRoleListByUserName(@Param("name") String name);
}
SysUserServiceImpl.java
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
SysUserRepository sysUserRepository;
@Autowired
SysRoleRepository sysRoleRepository;
@Override
public SysUser getUserByName(String name) {
SysUser sysUser = sysUserRepository.getUserByName(name);
List<SysRole> list = sysRoleRepository.getRoleListByUserName(name);
sysUser.setRoles(list);
return sysUser;
}
}
CustomTokenEnhancer.java
public class CustomTokenEnhancer implements TokenEnhancer {
@Autowired
SysUserService sysUserService;
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> additionalInformation = new HashMap<>();
String userName = authentication.getUserAuthentication().getName();
SysUser sysUser = sysUserService.getUserByName(userName);
additionalInformation.put("userId", sysUser.getId());
additionalInformation.put("userName", userName);
additionalInformation.put("roles", sysUser.getRoles());
additionalInformation.put("organizations", authentication.getName());
((DefaultOAuth2AccessToken) accessToken)
.setAdditionalInformation(additionalInformation);
return accessToken;
}
}
为了方便各个服务中解析token,我们把解析方法放在cloud1.0-common中
cloud1.0-common
pom.xml添加
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
JWTUtil.java
public class JWTUtil {
public static Map<String, Object> jwtVerify(String token) throws Exception{
Map<String, Object> claimsMap = new HashMap<String, Object>();
Resource resource = new ClassPathResource("public.cert");
if(resource.exists()){
String publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
//校验jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publicKey));
//获取jwt原始内容
String claims = jwt.getClaims();
claimsMap = JSON.parseObject(claims, new TypeReference<Map<String, Object>>(){});
}else{
return claimsMap;
}
return claimsMap;
}
}
测试
依次启动完各个服务后,先通过http://localhost:7777/sc-user-server/user/login?username=admin&password=admin
获取token
再访问http://localhost:7777/sc-data-service/getTokenDetail?access_token=......
返回
{
"user_name": "admin",
"scope": ["server"],
"roles": [{
"name": "ADMIN",
"id": 1
}, {
"name": "USER",
"id": 2
}],
"organizations": "admin",
"exp": 1580224344,
"userName": "admin",
"userId": 2,
"authorities": ["ROLE_ADMIN", "ROLE_USER"],
"jti": "f635b4ae-5a5c-47b8-a5ce-560468b675b1",
"client_id": "client_2"
}
通过token在服务与服务间传递信息已经实现
该实现仅适用于浏览器作为客户端时使用
这里我们把redis配置配置到公共模块方便各个服务引用
pom.xml
添加引用
<!-- redis缓存/session共享-begin -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- redis缓存/session共享-end -->
application-redis.yml
spring:
profiles: redis
redis:
# Redis数据库索引(默认为0)
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password: root
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制)200
max-active: 8
# 连接池中的最大空闲连接 20
max-idle: -1
# 连接池中的最小空闲连接 0
min-idle: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接超时时间(毫秒)默认是2000ms
timeout: 2000ms
由于在公共模块添加了redis依赖,各个引用了该模块的服务都要单独引用以上配置,各个服务引用如下
spring:
profiles:
include: redis
SessionConfig.java
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30,redisFlushMode=RedisFlushMode.IMMEDIATE)
public class SessionConfig {
}
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.springcloud.book.common.config.CommonConfiguration,\
cn.springcloud.book.common.config.SessionConfig
JWTUtil.java
用于解析jwt令牌
public class JWTUtil {
public static Map<String, Object> jwtVerify(String token) throws Exception{
Map<String, Object> claimsMap = new HashMap<String, Object>();
Resource resource = new ClassPathResource("public.cert");
if(resource.exists()){
String publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
//校验jwt
Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publicKey));
//获取jwt原始内容
String claims = jwt.getClaims();
claimsMap = JSON.parseObject(claims, new TypeReference<Map<String, Object>>(){});
}else{
return claimsMap;
}
return claimsMap;
}
public static Map<String, Object> analysisReq(HttpServletRequest request) throws Exception{
Map<String, Object> claimsMap = new HashMap<String, Object>();
//如果通信中包含令牌,将令牌解析成用户信息返回
String access_token = request.getParameter("access_token");
String Authorization = request.getHeader("Authorization");
if(access_token!=null&&access_token.length()>1){
claimsMap = jwtVerify(access_token);
}else if (Authorization!=null&&Authorization.length()>1&&Authorization.toUpperCase().startsWith("BEARER")) {
Authorization = Authorization.replaceFirst("(?i)bearer(\\s*)", "");//bearer(\\s*)忽视大小写
claimsMap = JWTUtil.jwtVerify(Authorization);
}
return claimsMap;
}
}
除了上文说的引用application-redis.yml,要实现session共享我们还要修改各个服务的资源服务器
SpringSecurity默认会在需要时创建新的session,如果不修改该配置每次访问受auth2保护的资源其sessionId都将发生改变,redis中存储的session信息将无法在不同服务间共享(zuul将会提示ERR no such key异常)
ResourceServerConfig.java
见configure(HttpSecurity http)
@Configuration
@EnableResourceServer //这个类表明了此应用是OAuth2 的资源服务器,此处主要指定了受资源服务器保护的资源链接
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean//需要以公钥作为bean外部接口才能访问受限资源
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("public.cert");
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
}catch (IOException e) {
throw new RuntimeException(e);
}
//使用converter.setVerifierKey(publicKey);会出现 Cannot convert access token to JSON
converter.setVerifier(new RsaVerifier(publicKey));
return converter;
}
@Bean
public TokenStore tokenStore() {//配置token模式
return new JwtTokenStore(accessTokenConverter());
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore());
}
/*
* 配置受资源服务器保护的资源链接,仅接受签名校验
* */
@Override
public void configure(HttpSecurity http) throws Exception {
/**
* always – 如果session不存在总是需要创建;
* ifRequired – 仅当需要时,创建session(默认配置);
* never – 框架从不创建session,但如果已经存在,会使用该session ;
* stateless – Spring Security不会创建session,或使用session;
*/
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER);
/*
* "migrateSession",即认证时,创建一个新http session,原session失效,属性从原session中拷贝过来
* "none",原session保持有效;
* "newSession",新创建session,且不从原session中拷贝任何属性。
*/
http.sessionManagement().sessionFixation().none();
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/actuator/hystrix.stream").permitAll()
.anyRequest().authenticated();//校验所有请求
}
}
UserController.java添加设置缓存接口
访问过/oauth/token接口后存储在redis的session的key发生变化,目前未找到解决该问题的方案所以我这边将登陆接口和设置缓存接口分离开来,通过bearer token方式获取令牌保证每次获取完令牌sessionId改变保证调用符合逻辑
@RequestMapping("/setSession")
public Map<String,String> setSession(HttpServletRequest request){
Map<String, String> result = new HashMap<String, String>();
try {
HttpSession session = request.getSession();
Map<String, Object> claimsMap = new HashMap<String, Object>();
//如果通信中包含令牌,将令牌解析成用户信息保存到上下文
String access_token = request.getParameter("access_token");
String Authorization = request.getHeader("Authorization");
if(access_token!=null&&access_token.length()>1){
claimsMap = JWTUtil.jwtVerify(access_token);
}else if (Authorization!=null&&Authorization.length()>1&&Authorization.toUpperCase().startsWith("BEARER")) {
Authorization = Authorization.replaceFirst("(?i)bearer(\\s*)", "");//bearer(\\s*)忽视大小写
claimsMap = JWTUtil.jwtVerify(Authorization);
}
claimsMap.put("setTime", System.currentTimeMillis());
session.setAttribute("claimsMap", claimsMap);
result.put("O_CODE", "1");
result.put("O_NOTE", "SUCCED");
} catch (Exception e) {
e.printStackTrace();
result.put("O_CODE", "-1");
result.put("O_NOTE", "FAILED");
}
return result;
}
DataController.java添加读取缓存接口
@RequestMapping("/getUser")
public String getUser() {
HttpSession session = httpServletRequest.getSession();
Map<String, Object> claimsMap = (Map<String, Object>) session.getAttribute("claimsMap");
System.out.println("访问端口:" + httpServletRequest.getServerPort());
return "SessionId:"+session.getId()+"|SysUser:"+claimsMap;
}
启动完各个服务后
访问http://localhost:7777/sc-user-server/user/login?username=OneTest&password=OneTest
拿到令牌
sc-user-server
服务设置缓存http://localhost:7777/sc-user-server/user/setSession?access_token=......
{"O_CODE":"1","O_NOTE":"SUCCED"}
sc-data-service
服务获取缓存信息http://localhost:7777/sc-data-service/getUser?access_token=......
SessionId:f78a33e2-f489-4811-bfda-3716d0fb27b4|SysUser:{user_name=OneTest, scope=["server"], roles=[null], organizations=OneTest, exp=1580519951, userName=OneTest, userId=6, jti=a43736a2-bed7-4f91-965d-d04bf7b44a8a, client_id=client_2, setTime=1580476911807}
成功实现服务间session共享
在4.2.中我们已经整合了redis的引用到cloud1.0-common工程,所以我就直接在该工程添加cache 的配置类
RedisConfig.java
/**
* redis key生成策略
* target: 类
* method: 方法
* params: 参数
* @return KeyGenerator
* 注意: 该方法只是声明了key的生成策略,还未被使用,需在@Cacheable注解中指定keyGenerator
* 如: @Cacheable(value = "key", keyGenerator = "keyGenerator"),为标识keyGenerator时使用默认策略
* 格式:key::类名+方法名
*/
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
/**
* key redis serializer: {@link StringRedisSerializer} and
* key redis serializer: {@link Jackson2JsonRedisSerializer}
**/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置value的序列化规则和 key的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.springcloud.book.common.config.CommonConfiguration,\
cn.springcloud.book.common.config.SessionConfig,\
cn.springcloud.book.common.config.RedisConfig
测试redis存取数据
cloud1.0-user-server工程
TestEndPointController.java添加接口
@RequestMapping("/redisTemplateTest")
public String redisTemplateTest() {
stringRedisTemplate.opsForValue().set("stringRedisTemplate", "set-stringRedisTemplate");
Map<String,Object> map1 = new HashMap<String,Object>();
map1.put("1", System.currentTimeMillis());
map1.put("2", "redisTemplate2");
redisTemplate.opsForValue().set("redisTemplate1", map1);
if(!redisTemplate.hasKey("redisTemplate2")){
Map<String,Object> map2 = new HashMap<String,Object>();
map2.put("1", System.currentTimeMillis());
map2.put("2", "redisTemplate2");
redisTemplate.opsForValue().set("redisTemplate2", map2,30, TimeUnit.SECONDS);
}
return "stringRedisTemplate:"+stringRedisTemplate.opsForValue().get("stringRedisTemplate")
+"
"
+"|redisTemplate1:"+redisTemplate.opsForValue().get("redisTemplate1")
+"
"
+"|redisTemplate2:"+redisTemplate.opsForValue().get("redisTemplate2");
}
测试
http://localhost:7777/sc-user-server/redisTemplateTest?access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJPbmVUZXN0Iiwic2NvcGUiOlsic2VydmVyIl0sInJvbGVzIjpbbnVsbF0sIm9yZ2FuaXphdGlvbnMiOiJPbmVUZXN0IiwiZXhwIjoxNTgzNjM1NTUzLCJ1c2VyTmFtZSI6Ik9uZVRlc3QiLCJ1c2VySWQiOjYsImp0aSI6IjFhMzA3MTFlLWMwODEtNGNjZC1hZjhlLWY3ZTdiM2RmMGMwMSIsImNsaWVudF9pZCI6ImNsaWVudF8yIn0.csQSBxn__ovhr4lFb-zlyLpHKU1ubipw1nQnfQm0wcVKfYo_I1G_OBLtOCfy8TnMzzsAaEmsHwTT1D8maA44CTUWmQw3L9BsNb7jpo_wtdbOwE-73qJ-WIJRx4tmmMMX9wHObErDQe8thROaieTWptdA80wUuVc4oVNpvj3nWTlVE4OHSms3kibLZdH3WzG5N9AhlHgtblgIzLZ5mLKgdNnZ7Foz-H_Ru08b7oYZW2EG2DZUNSeJ7WxrRZxPosiqAcrwBvhcMiTjAz8Ph6QTb5cp3jXcFQB0jwcGoHhd6azo9e5lRNXSBmDyiQqS--SfaW3apBSdYNoZlmldM5FxWw
返回
stringRedisTemplate:set-stringRedisTemplate
|redisTemplate1:{1=1583651631899, 2=redisTemplate2}
|redisTemplate2:{1=1583651623140, 2=redisTemplate2}
测试redis缓存
cloud1.0-user-server工程
TestEndPointController.java添加接口
@RequestMapping("/getRedisData")
@Cacheable(value="redis-data", keyGenerator="keyGenerator")
public String getRedisData() {
System.out.println("出现该提示说明未调用redis缓存");
return new String("getRedisData:"+System.currentTimeMillis());
}
第一次访问http://localhost:7777/sc-user-server/getRedisData?access_token=......
控制台打印
出现该提示说明未调用redis缓存
接口返回
getRedisData:1580568642021
第二次访问http://localhost:7777/sc-user-server/getRedisData?access_token=......
控制台打印为空
接口仍然返回
getRedisData:1580568642021
说明redis缓存实现成功
缓存雪崩简单的理解就是:由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,造成系统的崩溃。
这里我们通过均摊分配Redis的key失效时间来解决(也可以选择使用消息中间件或者二级缓存来解决)
由于@Cacheable注解不支持配置过期时间,需要通过配置CacheManneg来配置默认的过期时间和针对每个类或者是方法进行缓存失效时间配置
cloud1.0-common工程
RedisConfig.java添加CacheManager配置
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new RedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
this.getRedisCacheConfigurationWithTtl(24*60*60 + (new Random().nextInt(5)+1)*60),//默认策略,未配置的 key会使用这个过期时间(失效时间加上1到5分钟的随机数,避免雪崩效应)
this.getRedisCacheConfigurationMap()//指定 多个key策略
);
}
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
//SsoCache和BasicDataCache进行过期时间配置
redisCacheConfigurationMap.put("SsoCache", this.getRedisCacheConfigurationWithTtl(24*60*60));
redisCacheConfigurationMap.put("BasicDataCache", this.getRedisCacheConfigurationWithTtl(30));
return redisCacheConfigurationMap;
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {//秒
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer)
).entryTtl(Duration.ofSeconds(seconds));//Duration.ofHours(1):一小时
return redisCacheConfiguration;
}
均摊缓存失效时间测试
访问sc-user-server服务的getRedisData接口
http://localhost:7777/sc-user-server/getRedisData?access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJPbmVUZXN0Iiwic2NvcGUiOlsic2VydmVyIl0sInJvbGVzIjpbbnVsbF0sIm9yZ2FuaXphdGlvbnMiOiJPbmVUZXN0IiwiZXhwIjoxNTgzNjk0ODA2LCJ1c2VyTmFtZSI6Ik9uZVRlc3QiLCJ1c2VySWQiOjYsImp0aSI6ImVkNTRkNjAyLTU5MzAtNGI5ZC04NGJiLTA1ZjZmOGMwMTRiYiIsImNsaWVudF9pZCI6ImNsaWVudF8yIn0.PPK4-BuHnmqvzszgiUeafR2tldxVRNeF-dsYkQS2o2MwEnl30jPWe1EX-q7xUS13LVDK9HAc52o1EE3VgOLa-cFg4yGgmhOF17YHBPuAgWZoqeV8TVS3jnKX_NKMZRMmfsT2Fy_ftNGbGbNNkoShoGaPTCjaaSkykpCU09JqxgXl8kP2xmo_C-XKN1juPW2z8ALzw7dlmFITaTLvB5DA7r-rqEFyD9x55unOKmfdhBStPs9LImz1mgxrRElJOY5be_vZPnVcAILwkV0GuUx8gGk67raeVvYHN3wNTujNEBEThhEegzGWLnOEE91tYGrRwkiqPMW32m_y3iIvq9mpDg
返回
getRedisData:1583653679897
通过RedisDesktopManager查看缓存信息
发现缓存时间已经由原来的85400加上了56秒的随机时间了
指定缓存策略
(上述代码我添加了SsoCache和MomentaryCache两个自定义策略,这里我就不一一测试了)
//两种方式都可以实现,可以使用value上的信息,来替换类上cacheNames的信息
@CacheConfig(cacheNames = "SsoCache")
public class TestEndPointController{
@RequestMapping("/getRedisData")
@Cacheable(keyGenerator="keyGenerator")
public String getRedisData() {......}
}
@Cacheable(value="MomentaryCache", keyGenerator="keyGenerator")
public String getRedisData() {......}