微服务(十一)—— 单点登录

上一篇: 微服务(十)—— Feign的使用.

目录

      • 1. 介绍
        • (1)session同步
        • (2)session黏着(放在服务端)
        • (3)将信息存放到cookie客户端
        • (4) SSO单点登录
      • 2. 实现过程
        • 2.1 思路
        • 2.2 实现

1. 介绍

在单服务下,用户通过浏览器登录,通过会话管理保存用户登录信息,Cookie通过在客户端记录信息确定用户身份,Session通过在服务端记录信息确定用户身份。
当浏览器访问服务器时,会创建一个Session对象,同时Session对象里会随机生成一个id值,然后通过response返回给浏览器保存在cookie里,当浏览器下一次访问服务器时,会根据浏览器所带的cookie里的id找到对应的session。
但是对于微服务来说,session保存在不同的服务器,所以用户登录后访问不同的服务就需要重新登录,者显然是不合理的。
这就需要解决session问题,有下面几种实现方式:

(1)session同步

Tomcat支持动态的将某个Tomcat下的session复制到其它的Tomcat中。
微服务(十一)—— 单点登录_第1张图片
这种方式在集群数量比较少的时候,使用还可以。如果集群数量庞大,都需要复制session,这时候会因为网络延迟,或者session比较大,同步慢等问题。

(2)session黏着(放在服务端)

微服务(十一)—— 单点登录_第2张图片
通过对IP地址或者域名地址进行hash;解决了第一个网络延迟同步慢的问题。
缺点: 用户浏览器的IP地址hash后,满足单调性。可能会造成资源分配的不均衡,就不能达到负载均衡的目的。

(3)将信息存放到cookie客户端

session存在服务器端,会对服务器端有一定的压力,如果将信息保存到cookie中,不但减轻了服务器的压力,同时每个客户端的压力也很小。
缺点: cookie可以被禁用,cookie要随着浏览器传递,增大了传送的内容,而且cookie大小也有限制。

(4) SSO单点登录

微服务(十一)—— 单点登录_第3张图片
将session从系统中独立出来。建立一个专门的服务去保存session 信息,其它服务需要session信息的时候都去请求这个服务。一般都是保存在Redis中,因为Redis的访问速度快。

2. 实现过程

我在系统中使用的是第四种方式——SSO单点登录。

2.1 思路

首先用户登录,登录成功后将用户信息存储在Redis数据库中,并且将键的过期时间设置为 1 小时。我这里是用用户名为键,用户对象信息为值,我将用户对象转换为json格式存储在数据库中。因为我在建立用户表的时候,将用户名加了唯一索引 并且注册的时候做了唯一判断,所以不存在键冲突的情况。
然后告诉前端,每次向后端发送请求的时候将用户名放到请求头中,后端在需要用户登录的接口先从请求头中获取用户名,然后从Redis中根据键取出值,如果值为空,说明用户没有登录,返回一个需要登录的信息;如果不为空,说明已经成功登录,就不需要再登录。并且将该信息再次存在Redis,并且将过期时间设置为 1 小时。
这样不仅解决了单点登录的问题,还解决了登录过期重新登录的问题。

2.2 实现

首先,写一个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).

你可能感兴趣的:(微服务,微服务)