SpringBoot Redis
Redis 特性:高性能的key-value数据库
- 支持数据的持久化,可将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
- 支持String,Hash,list,Set,ZSet等数据结构的存储
- 支持数据的备份(master-slave)
- 支持分布式集群,横向扩展
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
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; } }
数据同步,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
- Read:
Demo
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>
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
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; } }
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:
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 gettoken2
-> valid -> success - compare
token1
&token2
- match -> valid -> delete the two redis key-values -> success
- unmatch -> invalid -> fail
- get
- getAuthentication
- get
user
from redis key-value<prefix>:<token>
:<user>
(gettoken
from http header)
- get
- 使用Redis维护保存用户登陆信息:
- Login Process:
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
- generate new
- logout:
- delete redis key-value
<prefix>:<token>
:<user>
(gettoken
from http header)
- delete redis key-value
- getAuthentication:
- get
user
from redis key-value<prefix>:<token>
:<user>
(gettoken
from http header)
- get
- 使用Redis维护保存用户登陆信息: (note: different services use different
- AuthService API:
方案:SpringSession
Refer to Spring Session - Spring Boot
spring-session
- spring旗下的一个项目, 把servlet容器实现的HttpSession替换为spring-session的HttpSession
- 核心组件:
SessionRepositoryFilter
拦截Web请求,确保随后调用javax.servlet.http.HttpServletRequest
的getSession()
,会返回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]
的值
- 通过request的
- 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
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
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
main
@SpringBootApplication public class AuthServiceApplication { public static void main(String[] args) { SpringApplication.run(AuthServiceApplication.class, args); } }
Visit:
http://localhost:8080/micro-auth
- POST
/regist
- body:
{"name":"Tom","password":"123123"}
- body:
- POST
/login
- header:
usersession:xx
- body:
{"name":"Tom","password":"123123"}
- header:
- GET
/logout
- header:
usersession:xx
,micro-auth:xxxxxxxxxxxxxx
- header:
- GET
/authentication
- header:
usersession:xx
,micro-auth:xxxxxxxxxxxxxx
- header:
- POST
Verify
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"}}
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
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"}}
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
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
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
main
@SpringBootApplication public class AuthServiceApplication { public static void main(String[] args) { SpringApplication.run(AuthServiceApplication.class, args); } }
Visit:
http://localhost:8080/micro-auth/session
- POST
/login
- body:
{"name":"Tom","password":"123123"}
- body:
- GET
/logout
- GET
/authentication
- POST
Verify
clear redis records
redis:6379> FLUSHALL OK redis:6379> keys * (empty list or set)
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 "
GET
/authentication
> curl -b cookie.txt -i -H "Content-Type:application/json" -X GET http://localhost:8080/micro-auth/session/authentication
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
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
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
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
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]}>