SpringBoot Redis

SpringBoot Redis

  1. Redis 特性:高性能的key-value数据库

    • 支持数据的持久化,可将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
    • 支持String,Hash,list,Set,ZSet等数据结构的存储
    • 支持数据的备份(master-slave)
    • 支持分布式集群,横向扩展
  2. Spring: 封装了RedisTemplate对象来支持对redis的各种操作

    • RedisTemplate<K,V>: 默认采用JDK的序列化策略
    • StringRedisTemplate extends RedisTemplate<String, String> : 默认采用String的序列化策略
    • RedisTemplate中定义了对5种数据结构操作:
      • redisTemplate.opsForValue() 操作字符串
      • redisTemplate.opsForHash() 操作hash
      • redisTemplate.opsForList() 操作list
      • redisTemplate.opsForSet() 操作set
      • redisTemplate.opsForZSet() 操作有序set
  3. SpringBoot:自动化装配

    • org.springframework.boot.autoconfigure.data.redis.RedisProperties

        @ConfigurationProperties(prefix = "spring.redis")
        public class RedisProperties {
            private int database = 0;
            private String url;
            private String host = "localhost";
            private String password;
            private int port = 6379;
            private boolean ssl;
            private Duration timeout;
            private Sentinel sentinel;
            private Cluster cluster;
            private final Jedis jedis = new Jedis();
            private final Lettuce lettuce = new Lettuce();
            // getter & setter ...
      
            public static class Pool {
                private int maxIdle = 8;
                private int minIdle = 0;
                private int maxActive = 8;
                private Duration maxWait = Duration.ofMillis(-1);
                //getter & setter ...
            }
            public static class Cluster {
                private List<String> nodes;            //Comma-separated list of "host:port" pairs
                private Integer maxRedirects;
                //getter & setter ...
            }
            public static class Sentinel {
                private String master;
                private List<String> nodes;
                //getter & setter ...
            }
            public static class Jedis {
                private Pool pool;
                //getter & setter ...
            }
            public static class Lettuce {
                private Duration shutdownTimeout = Duration.ofMillis(100);
                private Pool pool;
                //getter & setter ...
            }
        }
      
    • org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
        @Configuration
        @ConditionalOnClass(RedisOperations.class)
        @EnableConfigurationProperties(RedisProperties.class)
        @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
        public class RedisAutoConfiguration {
            @Bean
            @ConditionalOnMissingBean(name = "redisTemplate")
            public RedisTemplate<Object, Object> redisTemplate(
                    RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
                RedisTemplate<Object, Object> template = new RedisTemplate<>();
                template.setConnectionFactory(redisConnectionFactory);
                return template;
            }
            @Bean
            @ConditionalOnMissingBean
            public StringRedisTemplate stringRedisTemplate(
                    RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
                StringRedisTemplate template = new StringRedisTemplate();
                template.setConnectionFactory(redisConnectionFactory);
                return template;
            }
        }
      
  4. 数据同步,eg: redis和mysql间的数据的同步

    • Read:
      • Read from redis -> Exist -> get Data
      • Read from redis -> None -> Read mysql -> write to redis
    • Write:
      • Write to mysql -> Success -> Write to Redis

Demo

  1. pom.xml

     <!-- SpringBoot -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter</artifactId>
     </dependency>
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
     </dependency>
     <!-- Springboot redis -->
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-redis</artifactId>
     </dependency>
    
  2. resources/application.yml

     spring:
       redis:
         host: localhost
         port: 6379
         password: 123456
         timeout: 30000
         jedis:
           pool:
             max-active: 8
             max-wait: 1
             max-idle: 8
             min-idle: 0
    
  3. RedisConfig

     @Configuration
     public class RedisConfig {
    
         @Bean
         @ConditionalOnMissingBean(name = "redisTemplate")        // create and inject when no bean which named `redisTemplate`
         public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
             RedisTemplate<Object, Object> template = new RedisTemplate<>();
             template.setConnectionFactory(redisConnectionFactory);
    
             // set serializer for key: use `RedisSerializer<String>`
             RedisSerializer<String> stringSerializer = new StringRedisSerializer();
             template.setKeySerializer(stringSerializer);
             template.setHashKeySerializer(stringSerializer);
    
             // set serializer for value: use `Jackson2JsonRedisSerializer<Object>`
             Jackson2JsonRedisSerializer<Object>    jsonSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
             ObjectMapper objectMapper = initObjectMapper();
             jsonSerializer.setObjectMapper(objectMapper);
             template.setValueSerializer(jsonSerializer);
             template.setHashValueSerializer(jsonSerializer);
    
             return template;
         }
    
         private ObjectMapper initObjectMapper(){
             ObjectMapper objectMapper = new ObjectMapper();
    
             //去除掉对getter和setter的依赖,ObjectMapper将通过反射机制直接操作Java对象上的字段
             objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    
             // DefaultTyping: 
             // 指定什么样的类型输入会被使用 ( eg: `NON_FINAL`表示对所有非final类型或者非final类型元素对象持久化)
             // 这样Json序列化/反序列化不需要知道具体子类的类型,只需要根据父类以及类别标识就能准确判断子类类型
             // 注:会存储类型信息(为了能准确的反序列多态类型的数据)
             objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    
             // disable Feature:
             // `DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES` (Json -> Object): 忽略json字符串中不识别的属性
             // `SerializationFeature.FAIL_ON_EMPTY_BEANS` (Object -> Json) 忽略无法转换的对象 “No serializer found for class com.xxx.xxx”
             objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 
             objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    
             // Serialization (Object -> Json): 
             // `NON_EMPTY`只序列化非空属性
             //objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
    
             return objectMapper;
         }
     }
    
  4. RedisService

     @Service
     public class RedisService {
    
         // @Autowired
         // private StringRedisTemplate redisTemplate;
    
         //  inject base on bean name
         // @Resource        
         // private RedisTemplate<String, Object> redisTemplate;
    
         // inject base on the bean type
         @Autowired
         private RedisTemplate<String, Object> redisTemplate;
    
         public Object get(String key) {
             return redisTemplate.opsForValue().get(key);
         }
         public void set(String key, Object value) {
             redisTemplate.opsForValue().set(key, value);
         }
         public void set(String key,Object value,int timeout){
             redisTemplate.opsForValue().set(key, value,timeout,TimeUnit.SECONDS);
         }
         public Boolean delete(String key){
             return redisTemplate.delete(key);
         }
         public void expire(String key,int timeout){
             redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
         }
     }
    

应用:Session 持久化

  • Session在服务端保存用户会话状态(如:用户登录信息等)
  • 在程序重启、多进程运行、负载均衡、跨域等情况时,会出现Session丢失或多进程、多个负载站点间状态不能共享的情况
  • 解决方案:将Session持久化存储以共享 ( Redis 是一个高性能的key-value数据库,可用来存储Session),eg:
    • NoSession(服务端自己生成一个序列码代替Session作为标识)
    • SpringSession(对每一个请求request进行封装,后续取到的不是HttpSession而是可持久化的SpringSession)

方案:NoSession

Scenarios:

  1. One App, Multiple Clients Login,only latest login valid:

    • Login Process:
      • Client1: login -> success
      • Client2: login -> success & expire Client1 login
    • Visit controlled resources:
      • Client1: 401 Unauthorized -> please login
      • Client2: success
    • Logout Process:
      • Client1: logout -> invalid token-> success
      • Client2: logout -> valid token -> success
    • Implement:
      • 使用Redis维护保存用户登陆信息:
        • <prefix>:<token>:<user> & expireTime
        • <prefix>:<user.id>:<token> & expireTime
      • HttpHeader中带有token:
        • <token>
      • login:
        • delete the two redis key-values (for invaliding other clients login)
        • generate new <token>
        • set the two redis key-values
        • set <token> to Http Response
      • logout:
        • get token1 from http header
        • get user.id from redis key-value <prefix>:<token1>:<user>
        • get token2 from redis key-value <prefix>:<user.id>:<token2>
        • can't get user.id || can't get token2 -> valid -> success
        • compare token1 & token2
          • match -> valid -> delete the two redis key-values -> success
          • unmatch -> invalid -> fail
      • getAuthentication
        • get user from redis key-value <prefix>:<token>:<user> (get token from http header)
  2. One AuthService,Multiple other Services call AuthService(seperate auth process)

    • AuthService API:
      • login
      • logout
      • getAuthentication
    • Service1 -> AuthService
    • Service2 -> AuthService
    • Implement:
      • 使用Redis维护保存用户登陆信息: (note: different services use different <prefix>)
        • <prefix>:<token>:<user> & expireTime
      • HttpHeader中带有token:
        • <token>
      • login:
        • generate new token
        • set redis key-value <prefix>:<token>:<user>
        • set <token> to HttpResponse
      • logout:
        • delete redis key-value <prefix>:<token>:<user> (get token from http header)
      • getAuthentication:
        • get user from redis key-value <prefix>:<token>:<user> (get token from http header)

方案:SpringSession

Refer to Spring Session - Spring Boot

spring-session

  • spring旗下的一个项目, 把servlet容器实现的HttpSession替换为spring-session的HttpSession
  • 核心组件:SessionRepositoryFilter 拦截Web请求,确保随后调用javax.servlet.http.HttpServletRequestgetSession(),会返回Spring Session的HttpSession实例,而不是应用服务器默认的HttpSession
    • HttpSessionIdResolver: resolveSessionIds,expireSession
    • SessionRepository: createSession,save,findById,deleteById
    • @Override doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain)
      • SessionRepositoryRequestWrapper (getSession -> return HttpSessionWrapper)
      • SessionRepositoryResponseWrapper
  • 使用Redis存储的SpringSession(默认使用前缀:spring:session),对于每一个session都会创建3组数据,eg:
    • spring:session:sessions:[sessionId]: hash结构,存储springsession的主要内容:
      • sessionAttr:[sessionId] 存储session信息(eg:实体类的序列化数据)
      • creationTime
      • maxInactiveInterval
      • lastAccessedTime
    • spring:session:sessions:expires:[sessionId]:string结构,value为空,ttl倒计时过期
    • spring:session:expirations:[expireTime]:set结构
      • expires:[sessionId] 一个会话一条
      • redis的ttl删除key是一个异步行为且是一个低优先级的行为,可能会导致session不被清除,于是引入了expirations这个key,来主动进行session的过期行为判断
  • Process:
    • 通过request的getSession(boolean create) 方法获取session
    • 根据sessionId 读取 spring:session:sessions:[sessionId] 的值
  • Scenarios:
    • One App,Multiple Clients Login => seperated,all success
    • One AuthService,Multiple other Services call AuthService => seperated,all successs
    • One App,Multiple ports (Nginx/Apache+Tomcat) => depends on session async strategy

方案:NoSession 示例

Dependency

pom.xml

<!-- SpringBoot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- for StringUtils -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<!-- for MD5 -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
</dependency>

Config

  1. resources/application.yml

     server:
           port: 8080
           servlet:
             context-path: /micro-auth
    
     spring:
       redis:
         host: localhost
         port: 6379
         password: 123456
         timeout: 30000
         jedis:
           pool:
             max-active: 8
             max-wait: 1
             max-idle: 8
             min-idle: 0
    
     # for authController        
     auth:
       usersessionHeader: usersession
       principalHeader: micro-auth
       expireTime: 180
    
  2. RedisConfig

     @Configuration
     public class RedisConfig {
         @Bean
         public RedisTemplate<Object,Object> jsonRedisTemplate(RedisConnectionFactory redisConnectionFactory){
             RedisTemplate<Object, Object> template = new RedisTemplate<Object,Object>();
             template.setConnectionFactory(redisConnectionFactory);
    
             RedisSerializer<String> stringSerializer = new StringRedisSerializer();
             template.setKeySerializer(stringSerializer);
             template.setHashKeySerializer(stringSerializer);
    
             Jackson2JsonRedisSerializer<Object>    jsonSerializer = initJsonSerializer();
             template.setValueSerializer(jsonSerializer);
             template.setHashValueSerializer(jsonSerializer);
    
             template.afterPropertiesSet();
    
             System.out.println("Create Customer JsonRedisTemplate-----");
             return template;
         }
         private Jackson2JsonRedisSerializer<Object> initJsonSerializer(){
             Jackson2JsonRedisSerializer<Object>    jsonSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
             ObjectMapper objectMapper = initObjectMapper();
             jsonSerializer.setObjectMapper(objectMapper);
             return jsonSerializer;
         }
         private ObjectMapper initObjectMapper(){
             ObjectMapper objectMapper = new ObjectMapper();
             objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
             objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
             objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); 
             objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
             //objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
             return objectMapper;
         }
     }
    

RedisService

@Service
public class RedisService {        

    @Autowired
    private RedisTemplate<Object, Object> jsonRedisTemplate;

    public Object get(String key) {
        return jsonRedisTemplate.opsForValue().get(key);
    }    
    public void set(String key, Object value) {
        jsonRedisTemplate.opsForValue().set(key, value);
    }
    public void set(String key,Object value,int timeout){
        jsonRedisTemplate.opsForValue().set(key, value,timeout,TimeUnit.SECONDS);
    }
    public Boolean delete(String key){
        return jsonRedisTemplate.delete(key);
    }
    public void expire(String key,int timeout){
        jsonRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }
}

AuthController

@RestController
public class AuthController {

    @Value("${auth.principalHeader}")
    private String principalHeader;

    @Value("${auth.expireTime}")
    private int expireTime;

    @Autowired
    private RedisService redisService;

    @Autowired
    private UserService userService;

    @GetMapping("/")
    public Object index(){
        return ResponseEntity.ok("This is micro-authService!");
    }

    @PostMapping("/login")
    public Object login(@RequestBody User loginUser,
            @RequestHeader(name="${auth.usersessionHeader}") String sessionKey, 
            @RequestHeader(name="${auth.principalHeader}",required=false) String principle,
            HttpServletResponse response){
        // db: verify 
        if(loginUser==null || StringUtils.isAnyBlank(loginUser.getName(),loginUser.getPassword()))
            return MicroResponse.InvalidRequest;

        // check principle
        if(principle!=null){
            User user=(User)this.redisService.get(sessionKey+":"+principle);
            if(user!=null 
                    && loginUser.getName().equals(user.getName()) 
                    && MD5Utils.getMD5Str(loginUser.getPassword()).equals(user.getPassword())){
                this.redisService.expire(sessionKey+":"+principle, expireTime);
                this.redisService.expire(sessionKey+":"+user.getId(), expireTime);
                response.setHeader(principalHeader, principle);
                return MicroResponse.success("login success");
            }
        }

        // check name & password
        User user=userService.findByNameAndPassword(loginUser);
        if(user==null)
            return MicroResponse.AuthenticationFail;

        // delete other clients login
        String oldToken = (String)this.redisService.get(sessionKey+":"+user.getId());
        if(user!=null)
            this.redisService.delete(sessionKey+":"+oldToken);
        this.redisService.delete(sessionKey+":"+user.getId());

        // set new
        String token = UUID.randomUUID().toString();
        this.redisService.set(sessionKey+":"+token,user,expireTime);
        this.redisService.set(sessionKey+":"+user.getId(),token,expireTime);
        response.setHeader(principalHeader, token);
        return MicroResponse.success("login success");
    }

    @GetMapping("/logout")
    public Object logout(@RequestHeader(name="${auth.principalHeader}") String principle,@RequestHeader(name="${auth.usersessionHeader}") String sessionKey){
        User user=(User)this.redisService.get(sessionKey+":"+principle);
        if(user==null)
            return MicroResponse.success("logout success");

        String token=(String)this.redisService.get(sessionKey+":"+user.getId());
        if(token==null)
            return MicroResponse.success("logout success");

        if(principle.equals(token)){
            this.redisService.delete(sessionKey+":"+user.getId());
            this.redisService.delete(sessionKey+":"+token);
            return MicroResponse.success("logout success");
        }
        return MicroResponse.fail("logout fail");
    }

    @GetMapping("/authentication")
    public Object getAuthentication(@RequestHeader(name="${auth.principalHeader}") String principle,@RequestHeader(name="${auth.usersessionHeader}") String sessionKey){
        return MicroResponse.success(redisService.get(sessionKey+":"+principle));
    }

    @PostMapping("/regist")
    public Object regist(@RequestBody User user){
        if(user==null || StringUtils.isAnyBlank(user.getName(),user.getPassword()))
            return MicroResponse.InvalidRequest;
        boolean result=userService.save(user);
        return new MicroResponse(result,result?1:0,user);
    }
}

// catch error!
@RestController
public class AuthErrorController implements ErrorController{

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping(value="/error")
    public Object onError(HttpServletResponse rs,Exception ex){
       HttpStatus status=HttpStatus.resolve(rs.getStatus());
       if(status!=null)
           return new MicroResponse(false,rs.getStatus(),status.getReasonPhrase());
       else
           return MicroResponse.fail(ex.getMessage());
    }
}

Service & Repository & Entity

Service: UserService

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    public User findByNameAndPassword (User user){
        Optional<User> result= this.userRepository.findByNameAndPassword(user.getName(),user.getPassword());
        if(result.isPresent())
            return result.get();
        return null;
    }

    public boolean save(User user){
        return this.userRepository.save(user);
    }
}

Repository: UserRepository

@Repository
public class UserRepository {

    //private final ConcurrentMap<Integer,User> users = new ConcurrentHashMap<Integer,User>();
    private final ConcurrentMap<String,User> users=new ConcurrentHashMap<String,User>();
    private final static AtomicInteger idGenerator = new AtomicInteger();

    @PostConstruct
    public void init(){
        users.put("admin", new User(idGenerator.incrementAndGet(),"admin",MD5Utils.getMD5Str("admin123")));
    }
    public boolean save(User user){
        Integer id = idGenerator.incrementAndGet();
        user.setId(id);
        user.setPassword(MD5Utils.getMD5Str(user.getPassword()));
        return users.put(user.getName(),user)==null;
    }
    public Collection<User> list(){
        return users.values();
    }
    public Optional<User> findByNameAndPassword(String name,String password){
        User user=users.get(name);
        if(user!=null && user.getPassword().equals(MD5Utils.getMD5Str(password)))
            return Optional.of(user);
        return Optional.empty();
    }
    public boolean existsByName(String name){
        return users.containsKey(name);
    }
}

Entity: User

public class User implements Serializable{
    private static final long serialVersionUID = -4198480470411674996L;
    private Integer id;
    private String name;
    private String password;

    public User(){}

    public User(Integer id,String name,String password){
        this.id=id;
        this.name=name;
        this.password=password;
    }
    @JsonIgnore
    public String getPassword() {
        return password;
    }
    @JsonProperty
    public void setPassword(String password) {
        this.password = password;
    }
    /* other getter & setter ... */
}

Utils: MD5Utils & MicroResponse

public class MD5Utils {
     public static String getMD5Str(String strValue) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            String newstr = Base64.encodeBase64String(md5.digest(strValue.getBytes()));
            return newstr;
        } catch (NoSuchAlgorithmException e) {
            //e.printStackTrace();
            System.out.println(e.getMessage());
        }
        return strValue;
    }
}
public class MicroResponse {
    public static final MicroResponse OK=new MicroResponse(true,1,null);
    public static final MicroResponse AuthenticationFail=new MicroResponse(false,2, "Authentication Fail");
    public static final MicroResponse UnAuthorized=new MicroResponse(false,3, "Not Authorized");
    public static final MicroResponse InvalidRequest=new MicroResponse(false,4,"Invalid Request");
    public static final MicroResponse Existed=new MicroResponse(false,5,"Already Existed");
    public static final MicroResponse NotExist=new MicroResponse(false,6,"Not Exist");

    public static MicroResponse success(Object data){
        return new MicroResponse(true,1,data);
    }
    public static MicroResponse fail(Object data){
        return new MicroResponse(false,0,data);
    }

    private boolean success;
    private Integer code;
    private Object data;

    public MicroResponse(boolean success, Integer code, Object data) {
        super();
        this.success = success;
        this.code = code;
        this.data = data;
    }
    /* getter & setter ...  */
}

Run and Visit

  1. main

     @SpringBootApplication
     public class AuthServiceApplication {
         public static void main(String[] args) {
             SpringApplication.run(AuthServiceApplication.class, args);
         }
     }
    
  2. Visit: http://localhost:8080/micro-auth

    • POST /regist
      • body: {"name":"Tom","password":"123123"}
    • POST /login
      • header: usersession:xx
      • body: {"name":"Tom","password":"123123"}
    • GET /logout
      • header: usersession:xx,micro-auth:xxxxxxxxxxxxxx
    • GET /authentication
      • header: usersession:xx,micro-auth:xxxxxxxxxxxxxx

Verify

  1. POST /regist

     > curl -i -H "Content-Type: application/json" -H "usersession:s1" -X POST -d '{"name":"Tom","password":"123123"}' http://localhost:8080/micro-auth/regist
    
     HTTP/1.1 200
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 05:38:22 GMT
    
     {"success":true,"code":1,"data":{"id":2,"name":"Tom"}}
    
  2. POST /login

     > curl -i -H "Content-Type: application/json" -H "usersession:s1" -X POST -d '{"name":"Tom","password":"123123"}' http://localhost:8080/micro-auth/login
     HTTP/1.1 200
     micro-auth: 1d0fa647-9dbe-410a-8d9c-0e1c973a98e2
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 05:39:33 GMT
    
     {"success":true,"code":1,"data":"login success"}
    
     # check redis:
     redis:6379> keys *
     1) "s1:2"
     2) "s1:1d0fa647-9dbe-410a-8d9c-0e1c973a98e2"
    
     redis:6379> get s1:2
     "\"1d0fa647-9dbe-410a-8d9c-0e1c973a98e2\""
    
     redis:6379> get s1:1d0fa647-9dbe-410a-8d9c-0e1c973a98e2
     "[\"com.cj.auth.entity.User\",{\"id\":2,\"name\":\"Tom\",\"password\":\"Qpf0SxOVUjUkWySXOZ16kw==\"}]"
    
     redis:6379> ttl s1:2
     (integer) 100
    
     redis:6379> ttl s1:1d0fa647-9dbe-410a-8d9c-0e1c973a98e2
     (integer) 99
    
  3. GET /authentication

     > curl -i -H "micro-auth:1d0fa647-9dbe-410a-8d9c-0e1c973a98e2" -H "usersession:s1" -X GET http://localhost:8080/micro-auth/authentication
    
     HTTP/1.1 200
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 05:40:49 GMT
    
     {"success":true,"code":1,"data":{"id":2,"name":"Tom"}}
    
  4. GET /logout

     > curl -i -H "micro-auth: 1d0fa647-9dbe-410a-8d9c-0e1c973a98e2" -H "usersession:s1" -X GET http://localhost:8080/micro-auth/logout
    
     HTTP/1.1 200
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 05:04:30 GMT
    
     {"success":true,"code":1,"data":"logout success"}
    
     # check redis:
     redis:6379> keys *
     (empty list or set)
    

Client: call AuthService

使用RestTemplate方式call发布的AuthService ( 注:也可考虑使用Dobbo等其他RPC方式,AuthService发布方式也需要改变):

  • resources/application.yml

      server:
        port: 9080
        servlet:
          context-path: /micro-service1
      auth:
          url: http://localhost:8080/micro-auth
          principalHeader: micro-auth
          usersessionHeader: usersession
          usersessionKey: s1
    
  • Controller

      @RestController
      @RequestMapping("/auth")
      public class Service1Controller {
          @Autowired
          private AuthCallService authCallService;
    
          @PostMapping("/login")
          public Object login(@RequestBody User loginUser){
              // ...
          }
    
          @PostMapping("/logout")
          public Object logout(){
              // ...
          }
    
          @GetMapping("/authentication")
          public Object getAuthentication(@RequestHeader(name="${auth.principalHeader}",required=false) String principle){
              return AuthCallService.getAuthentication(principle);
          }
      }
    
  • AuthCallService

      @Service
      public class AuthCallService {
    
          @Value("${auth.url}")
          private String authURL;
    
          @Value("${auth.principalHeader}")
          private String principalHeader;
    
          @Value("${auth.usersessionHeader}")
          private String usersessionHeader;
    
          @Value("${auth.usersessionKey}")
          private String usersessionKey;
    
          @Autowired
          private RestTemplate restTemplate;
    
          public Object login(){
              //...
          }
    
          public Object logout(){
              //...
          }
    
          public Object getAuthentication(String principle){
              HttpHeaders headers = new HttpHeaders(); 
              headers.set(principalHeader,principle); 
              headers.set(usersessionHeader,usersessionKey); 
              headers.setContentType(MediaType.APPLICATION_JSON);
              HttpEntity<String> entity=new HttpEntity<String>(null,headers);
              HttpEntity<Map> response=restTemplate.exchange(authURL+"/authentication",HttpMethod.GET,entity,Map.class);
              return response.getBody();
          }
      }
    
  • main

      @SpringBootApplication
      public class ServiceApplication {
          public static void main(String[] args) {
              SpringApplication.run(ServiceApplication.class, args);
          }
      }
    
  • Visit: http://localhost:9080/micro-service1

    • /login
    • /logout
    • /getAuthentication

Client:简化测试版

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ServiceCallAuthNoSessionTest {

    @Autowired
    private TestRestTemplate restTemplate;

    private String authURL="http://localhost:8080/micro-auth";
    private String principalHeader="micro-auth";
    private String usersessionHeader="usersession";
    private String usersessionKey="s1";

    String principle="a37377ec-dc92-41f8-95af-4d0a494f3eb8";

    @Test
    public void callTest(){

        //getAuthentication
        callGetAuthentication();

        // login
        callLoginTest();

        // callGetAuthentication
        callGetAuthentication();

        // logout
        callLogoutTest();

        // getAuthentication
        callGetAuthentication();
    }

    @Test
    public void callLoginTest(){
        System.out.println("call login...");

        HttpHeaders headers = new HttpHeaders(); 
        headers.set(usersessionHeader,usersessionKey); 
        headers.setContentType(MediaType.APPLICATION_JSON);

        Map<String,String> userMap=new HashMap<String,String>();
        userMap.put("name", "Tom");
        userMap.put("password", "123123");

        HttpEntity<Map> entity=new HttpEntity<Map>(userMap,headers);
        HttpEntity<Map> response=restTemplate.exchange(authURL+"/login",HttpMethod.POST,entity,Map.class);
        System.out.println(response);

        principle=(String)response.getHeaders().getFirst(principalHeader);
        System.out.println("token:"+principle);
    }

    @Test
    public void callLogoutTest(){
        System.out.println("call logout...");
        HttpHeaders headers = new HttpHeaders(); 
        headers.set(principalHeader,principle); 
        headers.set(usersessionHeader,usersessionKey); 
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity=new HttpEntity<String>(null,headers);
        HttpEntity<String> response=restTemplate.exchange(authURL+"/logout",HttpMethod.GET,entity,String.class);
        System.out.println(response);
    }

    @Test
    public void callGetAuthentication(){
        System.out.println("call getAuthentication...");
        HttpHeaders headers = new HttpHeaders(); 
        headers.set(principalHeader,principle); 
        headers.set(usersessionHeader,usersessionKey); 
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity=new HttpEntity<String>(null,headers);
        HttpEntity<Map> response=restTemplate.exchange(authURL+"/authentication",HttpMethod.GET,entity,Map.class);
        System.out.println(response);
    }
}

Run Junit Test: callTest

call getAuthentication...
<200,{success=true, code=1, data=null},{Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Thu, 07 Feb 2019 06:37:22 GMT]}>

call login...
<200,{success=true, code=1, data=login success},{micro-auth=[80d89cb8-f8e3-4b6c-8344-06d9e3a88d41], Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Thu, 07 Feb 2019 06:37:22 GMT]}>
token:80d89cb8-f8e3-4b6c-8344-06d9e3a88d41

call getAuthentication...
<200,{success=true, code=1, data={id=2, name=Tom}},{Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Thu, 07 Feb 2019 06:37:22 GMT]}>

call logout...
<200,{"success":true,"code":1,"data":"logout success"},{Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Thu, 07 Feb 2019 06:37:22 GMT]}>

call getAuthentication...
<200,{success=true, code=1, data=null},{Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Thu, 07 Feb 2019 06:37:22 GMT]}>

方案:SpringSession 示例

Dependency

pom.xml

<!-- SpringBoot -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Springboot redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Spring Session -->
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

Config

  1. resources/application.yml

     server:
       port: 8080
       servlet:
         context-path: /micro-auth      
     spring:
       redis:
         host: localhost
         port: 6379
         password: 123456
         timeout: 30000
         jedis:
           pool:
             max-active: 8
             max-wait: 1
             max-idle: 8
             min-idle: 0      
     #  session:
     #    store-type: redis  
     #     timeout: 180  
     #    redis:
     #      namespace: sps        # default prefix is `spring:session`
     #      flush-mode: on-save
    
  2. RedisConfig (使用@EnableRedisHttpSession或在application.yml中配置spring.session)

     @Configuration
     @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 180,
         redisFlushMode=RedisFlushMode.ON_SAVE,
         redisNamespace="sps")
     public class RedisConfig {
     }
    

Controller

AuthSessionController

@RestController
@RequestMapping("/session")
public class AuthSessionController {

    @Autowired
    private UserService userService;

    @GetMapping("/")
    public Object index(){
        return ResponseEntity.ok("This is micro-authService using springsession!");
    }

    @PostMapping("/login")
    public Object login(@RequestBody User loginUser,HttpServletRequest request){
        if(loginUser==null || StringUtils.isAnyBlank(loginUser.getName(),loginUser.getPassword()))
            return MicroResponse.InvalidRequest;

        // check name & password
        User user=userService.findByNameAndPassword(loginUser);
        if(user==null)
            return MicroResponse.AuthenticationFail;

        HttpSession session=request.getSession();
        session.setAttribute(session.getId(), user);
        System.out.println(session.getId());
        return MicroResponse.success(user);
    }

    @GetMapping("/logout")
    public Object logout(HttpServletRequest request){
        HttpSession session=request.getSession(false);
        if(session!=null){
            session.removeAttribute(session.getId());
            System.out.println(session.getId());
        }else
            System.out.println("logout: session is null");
        return MicroResponse.OK;
    }

    @GetMapping("/authentication")
    public Object getAuthentication(HttpServletRequest request /*HttpSession session*/){
        HttpSession session=request.getSession(false);
        if(session!=null){
            System.out.println(session.getId());
            return MicroResponse.success(session.getAttribute(session.getId()));
        }
        System.out.println("getAuthentication: session is null");
        return MicroResponse.success(null);
    }
}

Service & Repository & Entity

UserService & UserRepository & User & MicroResponse 均同上

Run

  1. main

     @SpringBootApplication
     public class AuthServiceApplication {
         public static void main(String[] args) {
             SpringApplication.run(AuthServiceApplication.class, args);
         }
     }
    
  2. Visit: http://localhost:8080/micro-auth/session

    • POST /login
      • body: {"name":"Tom","password":"123123"}
    • GET /logout
    • GET /authentication

Verify

  1. clear redis records

     redis:6379> FLUSHALL
     OK
     redis:6379> keys *
     (empty list or set)
    
  2. POST /login

     > curl -c cookie.txt -i -H "Content-Type:application/json" -X POST -d '{"name": "admin", "password":"admin123"}' http://localhost:8080/micro-auth/session/login
    
     HTTP/1.1 200
     Set-Cookie: SESSION=ODY1NDBhZDUtYzNmNy00NTg4LTg4ZjYtMDMxZWVlYzE2YTBm; Path=/micro-auth/; HttpOnly
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 15:17:57 GMT
    
     {"success":true,"code":1,"data":{"id":1,"name":"admin"}}
    
     # check redis:
     redis:6379> keys *
     1) "sps:sessions:86540ad5-c3f7-4588-88f6-031eeec16a0f"
     2) "sps:sessions:expires:86540ad5-c3f7-4588-88f6-031eeec16a0f"
     3) "sps:expirations:1549552860000"
    
     redis:6379> hgetall sps:sessions:36a41b20-a02e-4685-b359-a2b9b0aec2b1
     1) "creationTime"
     2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01h\xc8ef "
     3) "maxInactiveInterval"
     4) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\xb4"
     5) "sessionAttr:36a41b20-a02e-4685-b359-a2b9b0aec2b1"
     6) "\xac\xed\x00\x05sr\x00\x17com.cj.auth.entity.User\xc5\xbc\x00A\xb4\xb2z\x8c\x02\x00\x03L\x00\x02idt\x00\x13Ljava/lang/Integer;L\x00\x04namet\x00\x12Ljava/lang/String;L\x00\bpasswordq\x00~\x00\x02xpsr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01t\x00\x05admint\x00\x18AZICOnu9cyUFFvBp3xi1AA=="
     7) "lastAccessedTime"
     8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01h\xc8ef "
    
  3. GET /authentication

     > curl -b cookie.txt -i -H "Content-Type:application/json" -X GET http://localhost:8080/micro-auth/session/authentication
    
  4. GET /logout

     > curl -b cookie.txt -i -H "Content-Type:application/json" -X GET http://localhost:8080/micro-auth/session/logout
    

扩展:使用json方式序列化对象到Redis

使用Jackson2JsonRedisSerializer解析Redis Value值

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 180,
    redisFlushMode=RedisFlushMode.ON_SAVE,
    redisNamespace="sps")
public class RedisConfig {
    /* @Bean RedisTemplate<Object,Object> jsonRedisTemplate : 同上面NoSession示例 */

    /* SessionRepository: 
     * 定义了创建、保存、删除以及检索session的方法
     * (将Session实例真正保存到数据存储的逻辑是在这个接口的实现中编码完成的)
     * 
     * RedisOperationsSessionRepository implements SessionRepository:
     * 会在Redis中创建、存储和删除session
     * 
     * {@link RedisHttpSessionConfiguration} 
    */

    /* Method1: */
    //    @SuppressWarnings("unchecked")
    //    @Bean
    //    public SessionRepository<?> sessionRepository( @Qualifier("jsonRedisTemplate") RedisOperations<Object, Object> redisTemplate){
    //        RedisOperationsSessionRepository sessionRepository =  new RedisOperationsSessionRepository(redisTemplate);
    //        // sessionRepository.setDefaultSerializer(initJsonSerializer());
    //        sessionRepository.setDefaultSerializer((RedisSerializer<Object>) redisTemplate.getValueSerializer());
    //        sessionRepository.setDefaultMaxInactiveInterval(180);
    //        sessionRepository.setRedisKeyNamespace("sps");
    //        sessionRepository.setRedisFlushMode(RedisFlushMode.ON_SAVE);
    //        System.out.println("Create Customer RedisOperationsSessionRepository --- ");
    //        return sessionRepository;
    //    }

    /* Method2 - Recomend */
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer(@Qualifier("jsonRedisTemplate") RedisOperations<Object, Object> redisTemplate){
        return (RedisSerializer<Object>)redisTemplate.getValueSerializer();
    }
}

Verify again

  1. POST /login

     > curl -c cookie.txt -i -H "Content-Type:application/json" -X POST -d '{"name": "admin", "password":"admin123"}' http://localhost:8080/micro-auth/session/login
     HTTP/1.1 200
     Set-Cookie: SESSION=MzAxZWJkNjMtOWYzMy00NWJiLWFhZjMtMGM0ZjJlMzIyYWM1; Path=/micro-auth/; HttpOnly
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 16:27:43 GMT
    
     {"success":true,"code":1,"data":{"id":1,"name":"admin"}}
    
     # check redis:
     redis:6379> keys *
     1) "sps:sessions:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
     2) "sps:sessions:expires:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
     3) "sps:expirations:1549558500000"
    
     redis:6379> hgetall sps:sessions:293d00d7-359f-4b82-87be-bbbe1fc64c8d
     1) "creationTime"
     2) "1549558319123"
     3) "maxInactiveInterval"
     4) "180"
     5) "sessionAttr:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
     6) "[\"com.cj.auth.entity.User\",{\"id\":1,\"name\":\"admin\",\"password\":\"AZICOnu9cyUFFvBp3xi1AA==\"}]"
     7) "lastAccessedTime"
     8) "1549558319123"
    
     redis:6379> ttl sps:sessions:expires:293d00d7-359f-4b82-87be-bbbe1fc64c8d
     (integer) 158
    
     redis:6379> smembers sps:expirations:1549558500000
     1) "\"expires:293d00d7-359f-4b82-87be-bbbe1fc64c8d\""
    
     # check cookie
     > cat cookie.txt
     # Netscape HTTP Cookie File
     # http://curl.haxx.se/docs/http-cookies.html
     # This file was generated by libcurl! Edit at your own risk.
    
     HttpOnly_localhost    FALSE    /micro-auth/    FALSE    0    SESSION    MzAxZWJkNjMtOWYzMy00NWJiLWFhZjMtMGM0ZjJlMzIyYWM1
    
  2. GET /authentication

     > curl -b cookie.txt -i -H "Content-Type:application/json" -X GET http://localhost:8080/micro-auth/session/authentication
    
     HTTP/1.1 200
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 16:27:48 GMT
    
     {"success":true,"code":1,"data":{"id":1,"name":"admin"}}
    
     # check redis:
     redis:6379> keys *
     1) "sps:expirations:1549558560000"        # new
     2) "sps:sessions:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
     3) "sps:sessions:expires:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
    
     redis:6379> hgetall sps:sessions:293d00d7-359f-4b82-87be-bbbe1fc64c8d
     1) "creationTime"
     2) "1549558319123"
     3) "maxInactiveInterval"
     4) "180"
     5) "sessionAttr:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
     6) "[\"com.cj.auth.entity.User\",{\"id\":1,\"name\":\"admin\",\"password\":\"AZICOnu9cyUFFvBp3xi1AA==\"}]"
     7) "lastAccessedTime"
     8) "1549558358599"                        # changed
    
     redis:6379> ttl sps:sessions:expires:293d00d7-359f-4b82-87be-bbbe1fc64c8d
     (integer) 166                            # changed
    
  3. GET /logout

     > curl -b cookie.txt -i -H "Content-Type:application/json" -X GET http://localhost:8080/micro-auth/session/logout
    
     HTTP/1.1 200
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 16:28:38 GMT
    
     {"success":true,"code":1,"data":null}
    
     # check redis:
     redis:6379> keys *
     1) "sps:sessions:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
     2) "sps:expirations:1549558680000"                                # new
     3) "sps:sessions:expires:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
    
     redis:6379> hgetall sps:sessions:293d00d7-359f-4b82-87be-bbbe1fc64c8d
     1) "creationTime"
     2) "1549558319123"
     3) "maxInactiveInterval"
     4) "180"
     5) "sessionAttr:293d00d7-359f-4b82-87be-bbbe1fc64c8d"
     6) ""                                    # removed
     7) "lastAccessedTime"
     8) "1549558447429"                        # changed
    
     redis:6379> ttl sps:sessions:expires:293d00d7-359f-4b82-87be-bbbe1fc64c8d
     (integer) 151
    
  4. GET /authentication

     > curl -b cookie.txt -i -H "Content-Type:application/json" -X GET http://localhost:8080/micro-auth/session/authentication
    
     HTTP/1.1 200
     Content-Type: application/json;charset=UTF-8
     Transfer-Encoding: chunked
     Date: Thu, 07 Feb 2019 16:29:58 GMT
    
     {"success":true,"code":1,"data":null}
    
     # check redis:
     redis:6379> keys *
     1) "sps:sessions:301ebd63-9f33-45bb-aaf3-0c4f2e322ac5"
    
     redis:6379> keys *
     (empty list or set)
    

Client:简化测试版

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ServiceCallAuthNoSessionTest {

    @Autowired
    private TestRestTemplate restTemplate;
    private String authURL="http://localhost:8080/micro-auth/session";
    private String cookie="";

    private String name="admin";
    private String password="admin123";

    @Test
    public void callTest(){

        //getAuthentication
        callGetAuthentication();

        // login
        callLoginTest();

        // callGetAuthentication
        callGetAuthentication();

        // logout
        callLogoutTest();

        // getAuthentication
        callGetAuthentication();
    }

    @Test
    public void callLoginTest(){
        System.out.println("call login...");

        HttpHeaders headers = new HttpHeaders(); 
        headers.setContentType(MediaType.APPLICATION_JSON);

        Map<String,String> userMap=new HashMap<String,String>();
        userMap.put("name", name);
        userMap.put("password", password);

        HttpEntity<Map> entity=new HttpEntity<Map>(userMap,headers);
        HttpEntity<Map> response=restTemplate.exchange(authURL+"/login",HttpMethod.POST,entity,Map.class);
        System.out.println(response);

        cookie=(String)response.getHeaders().getFirst(HttpHeaders.SET_COOKIE);
        System.out.println("cookie:"+cookie);
    }

    @Test
    public void callLogoutTest(){
        System.out.println("call logout...");
        HttpHeaders headers = new HttpHeaders(); 
        headers.set(HttpHeaders.COOKIE,cookie); 
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity=new HttpEntity<String>(null,headers);
        HttpEntity<String> response=restTemplate.exchange(authURL+"/logout",HttpMethod.GET,entity,String.class);
        System.out.println(response);
    }

    @Test
    public void callGetAuthentication(){
        System.out.println("call getAuthentication...");
        HttpHeaders headers = new HttpHeaders(); 
        headers.set(HttpHeaders.COOKIE,cookie); 
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity<String> entity=new HttpEntity<String>(null,headers);
        HttpEntity<Map> response=restTemplate.exchange(authURL+"/authentication",HttpMethod.GET,entity,Map.class);
        System.out.println(response);
    }
}

Run Junit Test: callTest

call getAuthentication...
<200,{success=true, code=1, data=null},{Set-Cookie=[SESSION=ZmM1NDQ1M2EtZTU4ZC00NTNmLWI5ODEtNWQ0YmUyODI3MmE0; Path=/micro-auth/; HttpOnly], Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Sat, 09 Feb 2019 07:02:26 GMT]}>
call login...
<200,{success=true, code=1, data={id=1, name=admin}},{Set-Cookie=[SESSION=ZmYxNDJiNTctNGU5Ny00MzFjLWFkMWYtNzkwMTJlMmUzZjIy; Path=/micro-auth/; HttpOnly], Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Sat, 09 Feb 2019 07:02:26 GMT]}>
cookie:SESSION=ZmYxNDJiNTctNGU5Ny00MzFjLWFkMWYtNzkwMTJlMmUzZjIy; Path=/micro-auth/; HttpOnly
call getAuthentication...
<200,{success=true, code=1, data={id=1, name=admin}},{Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Sat, 09 Feb 2019 07:02:26 GMT]}>
call logout...
<200,{"success":true,"code":1,"data":null},{Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Sat, 09 Feb 2019 07:02:26 GMT]}>
call getAuthentication...
<200,{success=true, code=1, data=null},{Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Sat, 09 Feb 2019 07:02:26 GMT]}>

Reference

My demo: auth-demo