内容简介:上一节开发了用户服务,即将开发的是用户服务EdgeService,从这个调用关系,可以看到用户的EdgeService是一个服务的服务,首选调用用户服务,对用户信息基本的操作,调用信息服务实现发送短信,发送邮件,还需要实现登录和注册的功能,并且登录是一个单点登录需要支持其他的系统,支持课程的登录的EdgeService,对他的要求是无状态的,还需要集中式的缓存redis。这么多服务集中于一身说明它是一个非常复杂的服务,不过也没关系,我们从头到尾把他开发完成。源码:https://github.com/li
上一节开发了用户服务,即将开发的是用户服务EdgeService,从这个调用关系,可以看到用户的EdgeService是一个服务的服务,首选调用用户服务,对用户信息基本的操作,调用信息服务实现发送短信,发送邮件,还需要实现登录和注册的功能,并且登录是一个单点登录需要支持其他的系统,支持课程的登录的EdgeService,对他的要求是无状态的,还需要集中式的缓存redis。这么多服务集中于一身说明它是一个非常复杂的服务,不过也没关系,我们从头到尾把他开发完成。源码:https://github.com/limingios/msA-docker
新建maven模块user-edge-service
- 引入user-thrift-service-api 和 message-thrift-service-api的pom文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.3.RELEASE</version> </parent> <modelVersion>4.0.0</modelVersion> <groupId>com.idig8</groupId> <artifactId>user-edge-service</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> <version>0.10.0</version> </dependency> <dependency> <groupId>com.idig8</groupId> <artifactId>user-thrift-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.idig8</groupId> <artifactId>message-thrift-service-api</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot</artifactId> <version>RELEASE</version> <scope>compile</scope> </dependency> </dependencies> </project>
-
redis的 工具 类,用于无状态的存储,token信息 保存用的userInfo
RedisConfig
package com.idig8.user.redis; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; /** * Redis缓存配置类 */ @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.password}") private String password; //缓存管理器 @Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); //设置缓存过期时间 cacheManager.setDefaultExpiration(10000); return cacheManager; } @Bean public JedisConnectionFactory redisConnectionFactory() { JedisConnectionFactory factory = new JedisConnectionFactory(); factory.setHostName(host); factory.setPort(port); factory.setTimeout(timeout); factory.setPassword(password); return factory; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory){ StringRedisTemplate template = new StringRedisTemplate(factory); setSerializer(template);//设置序列化工具 template.afterPropertiesSet(); return template; } private void setSerializer(StringRedisTemplate template){ Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setValueSerializer(jackson2JsonRedisSerializer); } }
RedisClient
package com.idig8.user.redis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; /** * Created by liming */ @Component public class RedisClient { @Autowired private RedisTemplate redisTemplate; public <T> T get(String key) { return (T)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 void expire(String key, int timeout) { redisTemplate.expire(key, timeout, TimeUnit.SECONDS); } }
- Response 和 LoginResponse 统一返回字符串
Response
package com.idig8.user.response; import java.io.Serializable; /** * Created by liming */ public class Response implements Serializable { public static final Response USERNAME_PASSWORD_INVALID = new Response("1001", "username or password invalid"); public static final Response MOBILE_OR_EMAIL_REQUIRED = new Response("1002", "mobile or email is required"); public static final Response SEND_VERIFYCODE_FAILED = new Response("1003", "send verify code failed"); public static final Response VERIFY_CODE_INVALID = new Response("1004", "verifyCode invalid"); public static final Response SUCCESS = new Response(); private String code; private String message; public Response() { this.code = "0"; this.message = "success"; } public Response(String code, String message) { this.code = code; this.message = message; } public static Response exception(Exception e) { return new Response("9999", e.getMessage()); } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
LoginResponse
package com.idig8.user.response; /** * Created by liming */ public class LoginResponse extends Response { private String token; public LoginResponse(String token) { this.token = token; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } }
- 客户端访问通过和服务端相同的协议进行通信
package com.idig8.user.thrift; import com.idig8.thrift.message.MessageService; import com.idig8.thrift.user.UserService; import org.apache.thrift.TServiceClient; import org.apache.thrift.protocol.TBinaryProtocol; import org.apache.thrift.protocol.TProtocol; import org.apache.thrift.transport.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @Component public class ServiceProvider { @Value("${thrift.user.ip}") private String serverIp; @Value("${thrift.user.port}") private int serverPort; @Value("${thrift.message.ip}") private String messageServerIp; @Value("${thrift.message.port}") private int messageServerPort; private enum ServiceType { USER, MESSAGE } public UserService.Client getUserService() { return getService(serverIp, serverPort, ServiceType.USER); } public MessageService.Client getMessasgeService() { return getService(messageServerIp, messageServerPort, ServiceType.MESSAGE); } public <T> T getService(String ip, int port, ServiceType serviceType) { TSocket socket = new TSocket(ip, port, 3000); TTransport transport = new TFramedTransport(socket); try { transport.open(); } catch (TTransportException e) { e.printStackTrace(); return null; } TProtocol protocol = new TBinaryProtocol(transport); TServiceClient result = null; switch (serviceType) { case USER: result = new UserService.Client(protocol); break; case MESSAGE: result = new MessageService.Client(protocol); break; } return (T)result; } }
-
controller 引入thrift的service方法和 redis 的操作工具类 用于redis的操作
> 因为userInfo是通过thrift自动升成的,里面很多方法太过麻烦,不利于开发查看数据内容,最好的方式自己创建一个对象,将自动升成userInfo转换成自定义的UserDTO,USerDTO最好的方式是在thrift的工程中进行的。如果多个项目,比较方便,单独的用户的edgeservice中进行DTO的话,只能他自己用业务不清晰。
package com.idig8.user.controller; import com.idig8.thrift.user.UserInfo; import com.idig8.thrift.user.dto.UserDTO; import com.idig8.user.redis.RedisClient; import com.idig8.user.response.LoginResponse; import com.idig8.user.response.Response; import com.idig8.user.thrift.ServiceProvider; import org.apache.commons.lang.StringUtils; import org.apache.thrift.TException; import org.apache.tomcat.util.buf.HexUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import java.security.MessageDigest; import java.util.Random; @Controller public class UserController { @Autowired private ServiceProvider serviceProvider; @Autowired private RedisClient redisClient; @RequestMapping(value = "/login",method = RequestMethod.POST) @ResponseBody public Response login(@RequestParam("username")String username, @RequestParam("password")String password){ // 1. 验证用户名密码 UserInfo userInfo = null; try { userInfo = serviceProvider.getUserService().getUserByName(username); } catch (TException e) { e.printStackTrace(); return Response.USERNAME_PASSWORD_INVALID; } if (userInfo == null){ return Response.USERNAME_PASSWORD_INVALID; } if(!userInfo.getPassword().equalsIgnoreCase(md5(password))){ return Response.USERNAME_PASSWORD_INVALID; } // 2. 生成token String token = genToken(); // 3. 缓存用户 //因为userInfo是通过thrift自动升成的,里面很多方法太过麻烦,不利于开发查看数据内容 //最好的方式自己创建一个对象,将自动升成userInfo转换成自定义的UserDTO redisClient.set(token,toDTO(userInfo)); return new LoginResponse(token); } @RequestMapping(value = "/sendVerifyCode", method = RequestMethod.POST) @ResponseBody public Response sendVerifyCode(@RequestParam(value="mobile", required = false) String mobile, @RequestParam(value="email", required = false) String email) { String message = "Verify code is:"; String code = randomCode("0123456789", 6); try { boolean result = false; if(StringUtils.isNotBlank(mobile)) { result = serviceProvider.getMessasgeService().sendMobileMessage(mobile, message+code); redisClient.set(mobile, code); } else if(StringUtils.isNotBlank(email)) { result = serviceProvider.getMessasgeService().sendEmailMessage(email, message+code); redisClient.set(email, code); } else { return Response.MOBILE_OR_EMAIL_REQUIRED; } if(!result) { return Response.SEND_VERIFYCODE_FAILED; } } catch (TException e) { e.printStackTrace(); return Response.exception(e); } return Response.SUCCESS; } @RequestMapping(value="/register", method = RequestMethod.POST) @ResponseBody public Response register(@RequestParam("username") String username, @RequestParam("password") String password, @RequestParam(value="mobile", required = false) String mobile, @RequestParam(value="email", required = false) String email, @RequestParam("verifyCode") String verifyCode) { if(StringUtils.isBlank(mobile) && StringUtils.isBlank(email)) { return Response.MOBILE_OR_EMAIL_REQUIRED; } if(StringUtils.isNotBlank(mobile)) { String redisCode = redisClient.get(mobile); if(!verifyCode.equals(redisCode)) { return Response.VERIFY_CODE_INVALID; } }else { String redisCode = redisClient.get(email); if(!verifyCode.equals(redisCode)) { return Response.VERIFY_CODE_INVALID; } } UserInfo userInfo = new UserInfo(); userInfo.setUsername(username); userInfo.setPassword(md5(password)); userInfo.setMobile(mobile); userInfo.setEmail(email); try { serviceProvider.getUserService().regiserUser(userInfo); } catch (TException e) { e.printStackTrace(); return Response.exception(e); } return Response.SUCCESS; } private UserDTO toDTO(UserInfo userInfo) { UserDTO userDTO = new UserDTO(); BeanUtils.copyProperties(userInfo, userDTO); return userDTO; } private String genToken() { return randomCode("0123456789abcdefghijklmnopqrstuvwxyz", 32); } private String randomCode(String s, int size) { StringBuilder result = new StringBuilder(size); Random random = new Random(); for(int i=0;i<size;i++) { int loc = random.nextInt(s.length()); result.append(s.charAt(loc)); } return result.toString(); } private String md5(String password) { try { MessageDigest md5 = MessageDigest.getInstance("MD5"); byte[] md5Bytes = md5.digest(password.getBytes("utf-8")); return HexUtils.toHexString(md5Bytes); } catch (Exception e) { e.printStackTrace(); } return null; } }
测试准备工作
- 数据库内添加一条记录(如果不添加记录会报getUsername是null,通过在线网站生成MD5的密码加密)
- 启动user-thrift-service 和 message-thrift-service 2个服务
- 启动user-edge-service
- 工具调用接口访问user-edge-service里面的接口,查看是否正常返回调用服务是否成功
- 登录操作查看是否redis内有存储内容
流程梳理
- 建立2个thrift api项目 user 和 message 通过thrift文件生成对应语言的方法。
- 建立2个服务端项目,各自引入user和message的api项目。
- 多种语言比较特殊,例如message里面需要两边都通过 python 端需要通过thirft生成对应的python代码方便python制作server端。java端调用需要通过
thirft升成对应的 java 代码方便其他项目的引用。 - 如果都是单语言的话,都是java的话,只需要生成一个thrift的java代码,和对应的server端服务端代码就可以了。
- 对于使用端只需要引用2个api就可以实现RPC的调用。但是需要记住的是他们之前的协议和传输内容必须一致才可以完成通信。
user-edge-service-client 新建–单点登录的服务
package com.idig8.user.client; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.idig8.thrift.user.dto.UserDTO; import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.impl.client.DefaultHttpClient; import org.codehaus.jackson.map.ObjectMapper; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.TimeUnit; /** * Created by Michael on 2017/10/31. */ public abstract class LoginFilter implements Filter { private static Cache<String, UserDTO> cache = CacheBuilder.newBuilder().maximumSize(10000) .expireAfterWrite(3, TimeUnit.MINUTES).build(); public void init(FilterConfig filterConfig) throws ServletException { } public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)servletRequest; HttpServletResponse response = (HttpServletResponse)servletResponse; String token = request.getParameter("token"); if(StringUtils.isBlank(token)) { Cookie[] cookies = request.getCookies(); if(cookies!=null) { for(Cookie c : cookies) { if(c.getName().equals("token")) { token = c.getValue(); } } } } UserDTO userDTO = null; if(StringUtils.isNotBlank(token)) { userDTO = cache.getIfPresent(token); if(userDTO==null) { userDTO = requestUserInfo(token); if(userDTO!=null) { cache.put(token, userDTO); } } } if(userDTO==null) { response.sendRedirect("http://www.mooc.com/user/login"); return; } login(request, response, userDTO); filterChain.doFilter(request, response); } protected abstract String userEdgeServiceAddr(); protected abstract void login(HttpServletRequest request, HttpServletResponse response, UserDTO userDTO); private UserDTO requestUserInfo(String token) { String url = "http://"+userEdgeServiceAddr()+"/user/authentication"; HttpClient client = new DefaultHttpClient(); HttpPost post = new HttpPost(url); post.addHeader("token", token); InputStream inputStream = null; try { HttpResponse response = client.execute(post); if(response.getStatusLine().getStatusCode()!= HttpStatus.SC_OK) { throw new RuntimeException("request user info failed! StatusLine:"+response.getStatusLine()); } inputStream = response.getEntity().getContent(); byte[] temp = new byte[1024]; StringBuilder sb = new StringBuilder(); int len = 0; while((len = inputStream.read(temp))>0) { sb.append(new String(temp,0,len)); } UserDTO userDTO = new ObjectMapper().readValue(sb.toString(), UserDTO.class); return userDTO; } catch (IOException e) { e.printStackTrace(); } finally { if(inputStream!=null) { try{ inputStream.close(); }catch(Exception e) { e.printStackTrace(); } } } return null; } public void destroy() { } }
需要引入单点登录的模块进行实现的功能
PS:其实通过梳理发现这个还是有套路可寻的如何多语言进行通信,先生成对应的语言的代码,然后通过rpc的服务端和客户端,他们之前进行协议话的通信,服务端完成自身的业务逻辑,客户端就获取返回的结果。
>>原创文章,欢迎转载。转载请注明:转载自IT人故事会,谢谢!
>>原文链接地址:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 『高级篇』docker之开发用户服务(12)
- NFS服务的用户身份映射 荐
- 微软Azure企业用户须知:无服务器计算服务使用教程
- 译见|构建用户管理微服务(二):实现领域模型
- CentOS 7搭建基于虚拟用户的FTP服务
- 如何取得服务器上的用户组列表?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Web Anatomy
Robert Hoekman Jr.、Jared Spool / New Riders / 2009-12-11 / USD 39.99
At the start of every web design project, the ongoing struggles reappear. We want to design highly usable and self-evident applications, but we also want to devise innovative, compelling, and exciting......一起来看看 《Web Anatomy》 这本书的介绍吧!