上一篇: 微服务(十)—— Feign的使用.
在单服务下,用户通过浏览器登录,通过会话管理保存用户登录信息,Cookie通过在客户端记录信息确定用户身份,Session通过在服务端记录信息确定用户身份。
当浏览器访问服务器时,会创建一个Session对象,同时Session对象里会随机生成一个id值,然后通过response返回给浏览器保存在cookie里,当浏览器下一次访问服务器时,会根据浏览器所带的cookie里的id找到对应的session。
但是对于微服务来说,session保存在不同的服务器,所以用户登录后访问不同的服务就需要重新登录,者显然是不合理的。
这就需要解决session问题,有下面几种实现方式:
Tomcat支持动态的将某个Tomcat下的session复制到其它的Tomcat中。
这种方式在集群数量比较少的时候,使用还可以。如果集群数量庞大,都需要复制session,这时候会因为网络延迟,或者session比较大,同步慢等问题。
通过对IP地址或者域名地址进行hash;解决了第一个网络延迟同步慢的问题。
缺点: 用户浏览器的IP地址hash后,满足单调性。可能会造成资源分配的不均衡,就不能达到负载均衡的目的。
session存在服务器端,会对服务器端有一定的压力,如果将信息保存到cookie中,不但减轻了服务器的压力,同时每个客户端的压力也很小。
缺点: cookie可以被禁用,cookie要随着浏览器传递,增大了传送的内容,而且cookie大小也有限制。
将session从系统中独立出来。建立一个专门的服务去保存session 信息,其它服务需要session信息的时候都去请求这个服务。一般都是保存在Redis中,因为Redis的访问速度快。
我在系统中使用的是第四种方式——SSO单点登录。
首先用户登录,登录成功后将用户信息存储在Redis数据库中,并且将键的过期时间设置为 1 小时。我这里是用用户名为键,用户对象信息为值,我将用户对象转换为json格式存储在数据库中。因为我在建立用户表的时候,将用户名加了唯一索引 并且注册的时候做了唯一判断,所以不存在键冲突的情况。
然后告诉前端,每次向后端发送请求的时候将用户名放到请求头中,后端在需要用户登录的接口先从请求头中获取用户名,然后从Redis中根据键取出值,如果值为空,说明用户没有登录,返回一个需要登录的信息;如果不为空,说明已经成功登录,就不需要再登录。并且将该信息再次存在Redis,并且将过期时间设置为 1 小时。
这样不仅解决了单点登录的问题,还解决了登录过期重新登录的问题。
首先,写一个Redis的配置类:RedisConfig
package com.aiun.common.config;
/**
* Redis配置类
* @author lenovo
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
//为string类型key设置序列器
redisTemplate.setKeySerializer(new StringRedisSerializer());
//为string类型value设置序列器
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//为hash类型key设置序列器
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//为hash类型value设置序列器
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
引入连接Redis依赖,这个依赖是在SpringBoot里的,所以SpringBoot指定了依赖,这里就不需要了。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
在application.yml配置文件里配置Redis的连接信息
# redis 缓存
spring:
redis:
timeout: 10000 # 连接超时时间
host: localhost # Redis服务器地址
port: 6379 # Redis服务器端口
password: 123456
database: 0 # 选择哪个库,默认0库
lettuce:
pool:
max-active: 1024 # 最大连接数,默认 8
max-wait: 10000 # 最大连接阻塞等待时间,单位毫秒,默认 -1
max-idle: 200 # 最大空闲连接,默认 8
min-idle: 5 # 最小空闲连接,默认 0
然后在需要使用Redis的类里面,注入RedisTemplate
用户登录的业务逻辑实现:
package com.aiun.user.service.impl;
/**
* 用户模块实现类
* @author lenovo
*/
@Service("iUserService")
public class UserServiceImpl implements IUserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public ServerResponse<User> login(String userName, String password) {
int resultCount = userMapper.checkUsername(userName);
if (resultCount <= 0) {
return ServerResponse.createByErrorMessage("用户名不存在");
}
//密码登录并MD5加密
String md5password = MD5Utils.MD5EncodeUtf8(password);
User user = userMapper.selectLogin(userName, md5password);
if (user == null) {
return ServerResponse.createByErrorMessage("密码错误");
}
String key = userName;
String token = JsonUtils.object2JsonStr(user);
ValueOperations<String, String>valueOperations = redisTemplate.opsForValue();
// 用户登录成功后,将该用户对象作为token值保存到redis里,并且设置过期时间为1小时
valueOperations.set(key, token, 1, TimeUnit.HOURS);
return ServerResponse.createBySuccess("登录成功", user);
}
//......
}
然后在需要进行登录验证的接口进行登录的验证,这里以订单的Controller层为例;
因为多个接口都需要验证,所以这里定义一个公共的私有方法:
/**
* 判断用户登录是否过期
*/
private ServerResponse<User> loginHasExpired(HttpServletRequest request) {
String key = request.getHeader(UserConst.AUTHORITY);
ValueOperations<String, String> valueOperations = redisTemplate.opsForValue();
String value = valueOperations.get(key);
if (StringUtils.isEmpty(value)) {
return ServerResponse.createByErrorMessage(ResponseCode.NEED_LOGIN.getCode(), ResponseCode.NEED_LOGIN.getDesc());
}
User user = JsonUtils.jsonStr2Object(value, User.class);
if (!key.equals(user.getUsername())) {
return ServerResponse.createByErrorMessage(ResponseCode.NEED_LOGIN.getCode(), ResponseCode.NEED_LOGIN.getDesc());
}
// 登录成功,重新设置键的过期时间
valueOperations.set(key, value, 1, TimeUnit.HOURS);
return ServerResponse.createBySuccess(user);
}
然后在需要判断用户登录的接口调用该方法就行。
/**
* 订单查询
* @param request 请求
* @param orderNo 订单号
* @param pageNum 当前页
* @param pageSize 页大小
* @return 返回结果
*/
@PostMapping("manage/search")
@ApiOperation(value = "订单查询")
public ServerResponse<PageInfo> orderSearch(HttpServletRequest request, Long orderNo, @RequestParam(value = "pageNum", defaultValue = "1") int pageNum, @RequestParam(value = "pageSize", defaultValue = "10") int pageSize) {
ServerResponse hasLogin = loginHasExpired(request);
if (hasLogin.isSuccess()) {
User user = (User) hasLogin.getData();
if (user.getRole() == UserConst.Role.ROLE_ADMIN) {
return iOrderService.manageSearch(orderNo, pageNum, pageSize);
} else {
return ServerResponse.createByErrorMessage("无权限操作,需要管理员权限");
}
}
return hasLogin;
}
如果登录就会获取到用户信息,如果没有登录,就返回一个需要登录的信息。
下一篇: 微服务(十二)—— 配置中心(backend-config-server).