今天接着上篇博文 java开发常用的工具以及配置类进行总结。
9 分布式开发登录工具类
登录业务开发,传统的基于Session的登录方案,在用户登录成功后,服务器会为用户创建一个会话(session),并生成一个唯一的session ID。服务器将session ID 存储在内存或数据库中,并将其发送给客户端,通常是通过设置一个名为"JSESSIONID"的Cookie。客户端在后续的请求中将该session ID带上,服务器通过session ID来查找对应的会话,验证用户的身份和权限。这种方案需要在服务器端维护session的状态,因此在分布式环境下不太适用。使用基于Token的登录方案使用无状态的方式进行身份验证和授权。当用户登录成功后,服务器会生成一个Token,并将其发送给客户端。客户端将Token存储在本地,通常是在本地存储(如LocalStorage)或Cookie中。客户端在后续的请求中将Token发送给服务器进行验证。服务器验证Token的合法性、有效性和授权信息,而无需在服务器端存储任何会话状态。常见的Token技术包括JWT(JSON Web Token)
使用JWT,第一步,pom.xml引入依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.7.0version>
dependency>
第二步 创建JWT的工具类
/**
* JWT用法:
* client接收服务端返回的jwt,将其存储到Cookie或者localStorage中.
* 此后,client在于服务器交互都会携带jwt,存储在Cookie中,可以自动发送,但不会跨域。
* 因此一般是将它放入到HTTP请求头或者POST请求的数据主体中。
*/
public class JwtUtils {
private static long tokenExpiration = 24*60*60*1000; // 过期时间
private static String tokenSignKey = "A1t2g3uigu123456"; // token签名参数;
private static Key getKeyInstance(){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] bytes = DatatypeConverter.parseBase64Binary(tokenSignKey);
return new SecretKeySpec(bytes,signatureAlgorithm.getJcaName());
}
// 创建jwt;
public static String createToken(Long userId, String userName) {
String token = Jwts.builder()
.setSubject("SRB-USER") // 设置JWT的主题(Subject),表示该JWT所代表的实体或用户。
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) // 过期时间
.claim("userId", userId) // 设置JWT的自定义声明(Claims),可以在该方法中添加自定义的键值对作为JWT的附加信息。
.claim("userName", userName)
.signWith(SignatureAlgorithm.HS512, getKeyInstance()) // 设置JWT的签名算法和密钥,用于对JWT进行签名。
.compressWith(CompressionCodecs.GZIP)
.compact(); // 将JWT的参数和签名生成最终的JWT字符串表示。
return token; // 返回token字符串;
}
/**
* 判断token是否有效
* @param token
* @return
*/
public static boolean checkToken(String token) {
if(StringUtils.isEmpty(token)) {
return false;
}
try {
Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
// 可以根据创建token字符串 得到用户Id 因为生成token的主体 我们加入了用户Id;
public static Long getUserId(String token) {
Claims claims = getClaims(token);
Integer userId = (Integer)claims.get("userId");
return userId.longValue();
}
// 可以根据创建token字符串 得到用户名 因为生成token的主体 我们加入了用户名;
public static String getUserName(String token) {
Claims claims = getClaims(token);
return (String)claims.get("userName");
}
public static void removeToken(String token) {
//jwttoken无需删除,客户端扔掉即可。
}
/**
* 校验token并返回Claims
* @param token
* @return
*/
private static Claims getClaims(String token) {
if(StringUtils.isEmpty(token)) {
// 这里可以加入一个消息,提示未登录
throw new BusinessException(ResponseEnum.LOGIN_AUTH_ERROR);
}
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
Claims claims = claimsJws.getBody();
return claims;
} catch (Exception e) {
throw new BusinessException(ResponseEnum.LOGIN_AUTH_ERROR);
}
}
}
该工具类常用于判断用户登录,以及需要登录的场景,根据token取出用户信息。因此,还是很重要的。下面用jwt测试一下 创建token字符串 和根据token字符串取出token载荷数据部分。
/**
* 测试jwt创建和解析;
*/
public class JwtTests {
//过期时间,毫秒,24小时
private static long tokenExpiration = 24*60*60*1000; // 过期时间1天;
//秘钥
private static String tokenSignKey = "baidu123";
/**
* 测试创建jwt;
*/
@Test
public void testCreateToken(){
String token = Jwts.builder()
.setHeaderParam("typ", "JWT") // 令牌类型,目前就是JWT;
.setHeaderParam("alg", "HS256") // 签名算法,这里是 HS256
.setSubject("baidu-user") // 令牌主题
.setIssuer("baidu-admin") // 签发者
.setAudience("baidu") // 接收者
.setIssuedAt(new Date()) // 签发时间
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) //过期时间(当前时间+时间戳)
.setNotBefore(new Date(System.currentTimeMillis() + 20*1000)) //20秒后可用
.setId(UUID.randomUUID().toString()) // 每次生成jwt都是唯一标识,避免重放攻击;
/**
载 荷,自定义信息;
*/
.claim("nickname", "boger").claim("avatar", "1.jpg")
.signWith(SignatureAlgorithm.HS256, tokenSignKey) //签名哈希
.compact(); //把以上信息组装 转换成字符串
System.out.println(token);
/**
* 生成的token字符串
* eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiYWlkdS11c2VyIiwiaXNzIjoiYmFpZHUtYWRtaW4iLCJhdWQiOiJiYWlkdSIsImlhdCI6MTY4NjM2NDQwOSwiZXhwIjoxNjg2NDUwODA5LCJuYmYiOjE2ODYzNjQ0MjksImp0aSI6ImE3MmRlMWFhLWUyN2UtNDFhNi05MzQ5LWZhMzM0OGM2MmY2OCIsIm5pY2tuYW1lIjoiYm9nZXIiLCJhdmF0YXIiOiIxLmpwZyJ9.d_Kz8EX6SqjHWF17KilboLhmKrdoC1Pnf1Kltg2OYHo
*/
}
/**
* 解析jwt字符串;
*/
@Test
public void testGetUserInfo(){
// 传入上面生成的jwt字符串;
/**
* 三部分组成 jwt头 载荷 签名哈希 用 . 隔开
*/
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiYWlkdS11c2VyIiwiaXNzIjoiYmFpZHUtYWRtaW4iLCJhdWQiOiJiYWlkdSIsImlhdCI6MTY4NjM2NDQwOSwiZXhwIjoxNjg2NDUwODA5LCJuYmYiOjE2ODYzNjQ0MjksImp0aSI6ImE3MmRlMWFhLWUyN2UtNDFhNi05MzQ5LWZhMzM0OGM2MmY2OCIsIm5pY2tuYW1lIjoiYm9nZXIiLCJhdmF0YXIiOiIxLmpwZyJ9.d_Kz8EX6SqjHWF17KilboLhmKrdoC1Pnf1Kltg2OYHo";
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
Claims claims = claimsJws.getBody(); // 获取有效载荷;
/**
* 从载荷获取信息;
*/
String subject = claims.getSubject();
String issuer = claims.getIssuer();
String audience = claims.getAudience();
Date issuedAt = claims.getIssuedAt();
Date expiration = claims.getExpiration();
Date notBefore = claims.getNotBefore();
String id = claims.getId();
System.out.println(subject);
System.out.println(issuer);
System.out.println(audience);
System.out.println(issuedAt);
System.out.println(expiration);
System.out.println(notBefore);
System.out.println(id);
String nickname = (String)claims.get("nickname");
String avatar = (String)claims.get("avatar");
System.out.println(nickname);
System.out.println(avatar);
}
}
下图是根据token取出数据的截图
可以看出 取出了数据部分。测试时候,过期时间不要设置太短
10 Redis使用及其工具类
在开发中,将经常读取的数据存储在Redis中,以减少对后端数据库的访问压力。例如,可以将数据库查询结果、API响应结果或计算结果存储在Redis缓存中,提高系统的读取性能和响应速度。
要使用Redis第一步,引入依赖pom.xml
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatypegroupId>
<artifactId>jackson-datatype-jsr310artifactId>
dependency>
引入后两个依赖,主要是解决redis存储键值对的json序列化。便于阅读和读取,以便于后续业务开发。
第二步 项目配置文件配置redis。这里以yml格式文件为例。 yml文件配置 注意层级
spring:
redis:
host: 127.0.0.1 # redis的主机,这里是本地主机;
port: 6379 # redis端口默认 6379
database: 0 # 使用的数据库;
password: #默认为空
timeout: 3000ms #最大等待时间,超时则抛出异常,否则请求一直等待
lettuce:
pool:
max-active: 20 #最大连接数,负值表示没有限制,默认8
max-wait: -1 #最大阻塞等待时间,负值表示没限制,默认-1
max-idle: 8 #最大空闲连接,默认8
min-idle: 0 #最小空闲连接,默认0
第三步 添加java配置类 RedisConfig
/**
* redis配置;
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
/**
* 设置redis连接池;
*/
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
/**
* 首先解决key的序列化方式 不使用默认的jdk序列化方式;
*/
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
/**
* 解决value的序列化方式 不使用默认的jdk序列化方式;
*/
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
/**
* 序列化时将类的数据类型存入json,以便反序列化的时候转换成正确的类型
*/
ObjectMapper objectMapper = new ObjectMapper();
//objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
/**
* 将当前对象的数据类型也存入序列化的结果字符串中;
*/
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
/**
* 解决jackson2无法反序列化LocalDateTime的问题
*/
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}
配置了解决存入的键值序列化方式。便于阅读。 如果不配置的话,那么在redis存入的键值阅读性差。比如在之前开发中,将前端经常访问的数据字典存入到redis,如下,进行单元测试。
@Test
public void saveDict(){
Dict dict = dictMapper.selectById(1); // 根据数据id为1的查询字典
/**
* 测试结果
* 打开redis可视化连接工具
* 输入 keys *
* 得到: "\xac\xed\x00\x05t\x00\x04dict"
* 获取对应的值
* 输入 get "\xac\xed\x00\x05t\x00\x04dict" 得到的是 二进制字符串;
* RedisTemplate默认使用了JDK的序列化方式存储了key和value,我们使用json序列化; 因此需要配置序列化
*/
redisTemplate.opsForValue().set("dict",dict,5, TimeUnit.MINUTES); // 将查询到的数据存入redis,过期时间5分钟;
}
11 日志文件德输出格式工具类
创建Springboot项目,如果使用自定义的控制台日志,可读性不太好。而且,如果需要根据日志文件,针对输出信息分析,那么需要将日志输出到文件。
在项目resources文件夹下面创建logback-spring.xml。名字不要改
<configuration>
<contextName>随便取名,一般是项目名,不要中文contextName>
<property name="log.path" value="文件目录自定义" />
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) %highlight([%-5level]) %green(%logger) %msg%n"/>
<property name="FILE_LOG_PATTERN"
value="%date{yyyy-MM-dd HH:mm:ss} [%-5level] %thread %file:%line %logger %msg%n" />
<property name="ENCODING"
value="UTF-8" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}pattern>
<charset>${ENCODING}charset>
encoder>
appender>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${log.path}/log.logfile>
<append>trueappend>
<encoder>
<pattern>${FILE_LOG_PATTERN}pattern>
<charset>${ENCODING}charset>
encoder>
appender>
<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/log-rolling.logfile>
<encoder>
<pattern>${FILE_LOG_PATTERN}pattern>
<charset>${ENCODING}charset>
encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/info/log-rolling-%d{yyyy-MM-dd}.%i.logfileNamePattern>
<maxHistory>15maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>1KBmaxFileSize>
timeBasedFileNamingAndTriggeringPolicy>
rollingPolicy>
appender>
<springProfile name="dev,test">
<logger name="自定义" level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ROLLING_FILE" />
logger>
springProfile>
<springProfile name="prod">
<logger name="自定义" level="ERROR">
<appender-ref ref="CONSOLE" />
<appender-ref ref="ROLLING_FILE" />
logger>
springProfile>
configuration>
配置了输出目录,控制台输出日志样式 生产环境 开发环境 滚动日志等。注意上面的目录自定义的,自己需要保持一致。
12 网关gate_way的使用
通过网关的统一入口和功能,可以实现请求的转发、路由、认证、授权、负载均衡、限流、熔断等,提高分布式系统的可靠性、可扩展性和安全性。网关还能够屏蔽后端服务的具体实现细节,提供统一的API接口,简化客户端的调用和维护工作。
项目中使用网关gate_way,一般是单独基于maven创建一个spring boot项目。
第一步 引入依赖。pom.xml
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
第二步 网关项目主启动类 添加注解 @EnableDiscoveryClient // 注册中心发现;
第三步 配置文件配置路由 这里以yml文件为例
server:
port: 81 # 服务端口
spring:
profiles:
active: dev # 环境设置
application:
name: service-gateway # 服务名
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos服务地址
gateway:
discovery:
locator:
enabled: true # gateway可以发现nacos中的微服务,并自动生成转发路由
# 微服务路由配置; 简化路由访问;
routes:
- id: service-A # 对应service-A微服务;
uri: lb://service-A
predicates:
- Path=/*/A/** # 路由匹配;
- id: service-B #对应service-B微服务;
uri: lb://service-B
predicates:
- Path=/*/B/**
- id: service-C #对应service-C 微服务;
uri: lb://service-C
predicates:
- Path=/*/C/**
配置文件 配置了网关端口。 以及nacos地址(这里需要引入nacos的依赖,一般将微服务项目需要的公共依赖,会单独创建一个微服务,作为服务的公共依赖如service-base模块)。微服务路由配置,配置了微服务模块A B C三个模块,如果还有其他模块,后续还会添加到这里。并且路由保持一致。如微服务模块A,里面的路由带有 /A/。
同时,网关微服务还需要配置,跨域处理。 未搭建网关微服务时,解决跨域,我们采用的是在 Controller类添加@CrossOrgin注解。
/**
* 利用gateway跨域配置; Controller类就不需要@CrossOrgin注解
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true); //是否允许携带cookie
config.addAllowedOrigin("*"); //可接受的域,是一个具体域名或者*(代表任意域名)
config.addAllowedHeader("*"); //允许携带的头
config.addAllowedMethod("*"); //允许访问的方式
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
今天就到这里吧 ,下期再见。