当前位置: 首页 > news >正文

海门建设厅网站wordpress怎样获取文章分类的id

海门建设厅网站,wordpress怎样获取文章分类的id,内部网站建设软件下载,h5小程序1、spring事务管理 1.1 Spring事务管理 声明式事务#xff1a; 1 通过XML配置#xff0c;声明某方法的事务特征 2、通过注解#xff0c;声明某方法的事务特征#xff0c;注解Transactional 1.2 Transactional 注解参数讲解 隔离级别传播行为回滚规则是否只读事务超时…1、spring事务管理 1.1 Spring事务管理 声明式事务 1 通过XML配置声明某方法的事务特征 2、通过注解声明某方法的事务特征注解Transactional  1.2 Transactional 注解参数讲解 隔离级别传播行为回滚规则是否只读事务超时 传播机制比较难理解这里要着重说一下 事务传播行为是为了解决业务层方法之间互相调用的事务问题。 当事务方法被另一个事务方法调用时必须指定事务应该如何传播。例如方法可能继续在现有事务中运行也可能开启一个新事务并在自己的事务中运行。 假如在A方法中调用了B方法而B方法是事务操作则A为B的外部事务。 1.TransactionDefinition.PROPAGATION_REQUIRED 是Spring事务管理中的一种传播行为它表示如果当前有事务就沿用当前事务如果没有就新建一个事务。这是最常用的传播行为。 优点可以保证事务的一致性和完整性也可以避免不必要的事务开启和关闭的开销 缺点如果事务方法嵌套调用内部事务和外部事务是同一个事务内部事务的回滚会导致外部事务也回滚这可能不是预期的结果。 2、TransactionDefinition.PROPAGATION_REQUIRES_NEW 无论当前是否有事务都会新建一个事务并暂停当前事务如果存在 优点可以保证内部事务和外部事务的独立性。内部事务的回滚并不会影响外部事务也可以避免内部事务受到外部事务的影响 缺点会增加事务的开启和关闭的开销也可能导致数据库的竞争和死锁 3、TransactionDefinition.PROPAGATION_NESTED: 如果当前存在事务就在嵌套事务内执行如果当前没有事务就执行与TransactionDefinition.PROPAGATION_REQUIRED类似的操作。也就是说 在外部方法开启事务的情况下,在内部开启一个新的事务作为嵌套事务存在。如果外部方法无事务则单独开启一个事务与 PROPAGATION_REQUIRED 类似。 4.TransactionDefinition.PROPAGATION_MANDATORY 如果当前存在事务则加入该事务如果当前没有事务则抛出异常。mandatory强制性 编程式事务 通过TransactionTemplate管理事务并通过它执行数据库的操作 package com.nowcoder.community.service;import com.nowcoder.community.dao.AlphaDao; import com.nowcoder.community.dao.DiscussPostMapper; import com.nowcoder.community.dao.UserMapper; import com.nowcoder.community.entity.DiscussPost; import com.nowcoder.community.entity.User; import com.nowcoder.community.util.CommunityUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate;import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.Date;Service //Scope(prototype) public class AlphaService {Autowiredprivate AlphaDao alphaDao;Autowiredprivate UserMapper userMapper;Autowiredprivate DiscussPostMapper discussPostMapper;Autowiredprivate TransactionTemplate transactionTemplate;public AlphaService() { // System.out.println(实例化AlphaService);}PostConstructpublic void init() { // System.out.println(初始化AlphaService);}PreDestroypublic void destroy() { // System.out.println(销毁AlphaService);}public String find() {return alphaDao.select();}// REQUIRED: 支持当前事务(外部事务),如果不存在则创建新事务.// REQUIRES_NEW: 创建一个新事务,并且暂停当前事务(外部事务).// NESTED: 如果当前存在事务(外部事务),则嵌套在该事务中执行(独立的提交和回滚),否则就会REQUIRED一样.//isolation 隔离性 propagation 传播机制Transactional(isolation Isolation.READ_COMMITTED, propagation Propagation.REQUIRED)public Object save1() {// 新增用户User user new User();user.setUsername(alpha);user.setSalt(CommunityUtil.generateUUID().substring(0, 5));user.setPassword(CommunityUtil.md5(123 user.getSalt()));user.setEmail(alphaqq.com);user.setHeaderUrl(http://image.nowcoder.com/head/99t.png);user.setCreateTime(new Date());userMapper.insertUser(user);// 新增帖子DiscussPost post new DiscussPost();post.setUserId(user.getId());post.setTitle(Hello);post.setContent(新人报道!);post.setCreateTime(new Date());discussPostMapper.insertDiscussPost(post);Integer.valueOf(abc);return ok;} //通过TransactionTemplate进行局部的事务回滚public Object save2() {transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);return transactionTemplate.execute(new TransactionCallbackObject() {Overridepublic Object doInTransaction(TransactionStatus status) {// 新增用户User user new User();user.setUsername(beta);user.setSalt(CommunityUtil.generateUUID().substring(0, 5));user.setPassword(CommunityUtil.md5(123 user.getSalt()));user.setEmail(betaqq.com);user.setHeaderUrl(http://image.nowcoder.com/head/999t.png);user.setCreateTime(new Date());userMapper.insertUser(user);// 新增帖子DiscussPost post new DiscussPost();post.setUserId(user.getId());post.setTitle(你好);post.setContent(我是新人!);post.setCreateTime(new Date());discussPostMapper.insertDiscussPost(post);Integer.valueOf(abc);return ok;}});}} 2、项目中cookie的使用及原理 什么是cookie即浏览器的一种缓存数据为了弥补http无状态的缺陷。 对于注册的验证码功能采用特定的验证码生成工具生成验证码在这里项目中并没有采用某一个变量去接收这个验证码的值而是采用了cookie即每个客户端都拥有它们自己的cookie假如采用全局变量去接收则很容易产生并发的问题采用cookie即将每个用户都隔离开来各自用各自浏览器的cookie。 对于登陆状态这个功能主要就是拿浏览器的cookie去redis中找状态是否过期。同样的并不会用项目中的某个变量去接收ticket这样同上会产生并发问题。cookie中存储ticket可以保证不同用户都可以存储它们自己的ticket并可以拿其去进行验证 cookie的用法如下 cookie.setPath是一个方法用于设置cookie的path属性。path属性指定了哪些URL的请求可以携带cookie。如果不设置path属性那么默认值是创建cookie的应用的路径这意味着只有同一个应用可以访问这个cookie。如果设置了path属性那么所有以该路径为前缀的URL都可以访问这个cookie。例如如果设置了cookie.setPath(“/test”)那么/test, /test/, /test/a, /test/b等URL都可以访问这个cookie但是/, /doc, /fr/test等URL则不能访问这个cookie。 response.addCookie是一个方法用于将cookie添加到HTTP响应中从而发送给客户端浏览器。这个方法需要一个Cookie对象作为参数Cookie对象可以用Cookie(name, value)构造器创建其中name和value是字符串类型。 例如如果要创建一个名为user值为Tom的cookie并将其添加到响应中可以使用以下代码 Cookie cookie new Cookie(“user”, “Tom”); response.addCookie(cookie); 这样客户端浏览器就会收到一个包含userTom的Set-Cookie头并将其保存在本地。 Cookie cookie new Cookie(ticket, map.get(ticket).toString());//设置可以携带cookie的路径cookie.setPath(contextPath);//设置cookie的生存时间cookie.setMaxAge(expiredSeconds);//将cookie添加到HTTP响应中从而发送给客户端浏览器response.addCookie(cookie); 3、登陆拦截器 拦截器示例 1、定义拦截器实现HandlerInterceptor根据自己的需求重写prehandle等方法 2、配置拦截器为他指定拦截、排除的路径 拦截器应用 1、在请求开始时查询登录用户 2、在本次请求中持有用户数据 3、在模板视图上显示用户数据 4、在请求结束时清理用户数据 示例如下代码所示 package com.nowcoder.community.controller.interceptor;import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;Component public class AlphaInterceptor implements HandlerInterceptor {private static final Logger logger LoggerFactory.getLogger(AlphaInterceptor.class);// 在Controller之前执行Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {logger.debug(preHandle: handler.toString());return true;}// 在Controller之后执行Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {logger.debug(postHandle: handler.toString());}// 在TemplateEngine之后执行Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {logger.debug(afterCompletion: handler.toString());} }需要配置webconfig使得拦截器生效 package com.nowcoder.community.config;import com.nowcoder.community.controller.interceptor.AlphaInterceptor; import com.nowcoder.community.controller.interceptor.LoginRequiredInterceptor; import com.nowcoder.community.controller.interceptor.LoginTicketInterceptor; import com.nowcoder.community.controller.interceptor.MessageInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;Configuration public class WebMvcConfig implements WebMvcConfigurer {Autowiredprivate AlphaInterceptor alphaInterceptor;Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(alphaInterceptor)//excludePathPatterns排除以下路径在以下路径拦截器并不会作用.excludePathPatterns(/**/*.css, /**/*.js, /**/*.png, /**/*.jpg, /**/*.jpeg) //以下路径拦截器生效.addPathPatterns(/register, /login);}}输入请求路径login看看日志输出检查拦截器是否发生作用 打印出handler说明login触发了LoginController类内部的方法从而拦截器效果 具体拦截器的操作见上一篇文章 3、用户信息的存储 ThreadLocalUserusers是一个用于存储每个线程独有的User对象的类它可以保证每个线程都可以访问自己的User对象而不会受到其他线程的影响ThreadLocal类提供了get()和set()方法来获取和设置当前线程的User对象。ThreadLocal类还可以通过重写initialValue()方法来指定User对象的初始值1 。 在这个项目中采用ThreadLocal来存储用户的信息确保每个用户线程访问到的都是自己的User对象不受到其他用户线程的影响 /*** 持有用户信息,用于代替session对象.*/ Component public class HostHolder {private ThreadLocalUser users new ThreadLocal();public void setUser(User user) {users.set(user);}public User getUser() {return users.get();}public void clear() {users.remove();}} 我们可以看看Threadlocal.set和get方法的源码可以看出对Threadlocal变量操作都是对于当前线程的变量进行操作具有隔离性其他线程无法操作当前线程内Threadlocal的值 /*** Sets the current threads copy of this thread-local variable* to the specified value. Most subclasses will have no need to* override this method, relying solely on the {link #initialValue}* method to set the values of thread-locals.** param value the value to be stored in the current threads copy of* this thread-local.*/public void set(T value) {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null)map.set(this, value);elsecreateMap(t, value);}/*** Returns the value in the current threads copy of this* thread-local variable. If the variable has no value for the* current thread, it is first initialized to the value returned* by an invocation of the {link #initialValue} method.** return the current threads value of this thread-local*/public T get() {Thread t Thread.currentThread();ThreadLocalMap map getMap(t);if (map ! null) {ThreadLocalMap.Entry e map.getEntry(this);if (e ! null) {SuppressWarnings(unchecked)T result (T)e.value;return result;}}return setInitialValue();} 4、fastjson的使用 即将java字符串对象等转换为json对象或将json对象转换为java字符串对象等输出结果如下图所示 //转换为json字符串public static String getJSONString(int code, String msg, MapString, Object map) {JSONObject json new JSONObject();json.put(code, code);json.put(msg, msg);if (map ! null) {for (String key : map.keySet()) {json.put(key, map.get(key));}}return json.toJSONString();}public static void main(String[] args) {MapString, Object map new HashMap();map.put(name, zhangsan);map.put(age, 25);System.out.println(getJSONString(0, ok, map));} 5、spring中的事务操作 Transaction注解解释可以看上一篇牛客论坛内容。 什么场景下需要事务回滚 当涉及到多个数据表或多个数据源的操作的时候就需要使用事务来保证事务操作的原子性。例如再电商系统中用户下单需要同时更新用户表、订单表、库存表等。如果一个操作失败需要回滚所有操作避免用户扣款但是订单未生成 或者库存未减少等问题。 或者如批量事务操作的时候一个两个事务分别要拿对方持有的行锁会产生死锁对于mysql来说死锁就是CPU飙升最后两个事务会发生回滚。 Transaction注解是一种声明式事务管理的方式它可以在类或者方法上使用表示该类或者方法需要进行事务管理Transaction注解在什么时候进行回滚操作取决于它的属性和异常情况。默认情况下Transaction注解只会在抛出运行时异常RuntimeException或者错误Error时才会回滚事务Transaction注解如何进行回滚操作有以下几种方式 在方法中抛出运行时异常或者错误让spring事务管理器捕获并回滚事务。在方法中使用try-catch语句块捕获异常并在catch中手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()方法来设置回滚标志在Transaction注解中使用rollbackFor属性来指定需要回滚的异常类数组当方法中抛出指定异常时则进行事务回滚 事务回滚的代码示例采用注解方式全局回滚 Service public class UserServiceImpl implements UserService {Autowiredprivate UserDAO userDAO;Autowiredprivate OrderDAO orderDAO;//使用Transactional注解开启事务管理Transactionalpublic void updateUserAndOrder(User user, Order order) {try {//更新用户表userDAO.updateUser(user);//更新订单表orderDAO.updateOrder(order);} catch (Exception e) {//如果发生异常抛出运行时异常让spring事务管理器捕获并回滚事务throw new RuntimeException(e);}} } 采TransactionTemplate进行方法体内的局部代码回滚代码示例 Service public class UserService {Autowiredprivate UserDAO userDAO;Autowiredprivate OrderDAO orderDAO;//注入TransactionTemplate对象Autowiredprivate TransactionTemplate transactionTemplate;//使用transactionTemplate.execute方法开启事务管理public void updateUserAndOrder(User user, Order order) {transactionTemplate.execute(new TransactionCallbackObject() {//在doInTransaction方法中编写事务操作的逻辑public Object doInTransaction(TransactionStatus status) {try {//更新用户表userDAO.updateUser(user);//更新订单表orderDAO.updateOrder(order);} catch (Exception e) {//如果发生异常设置回滚标志status.setRollbackOnly();}return null;}});} } 6、Redis优化登陆模块 6.1、验证码存储位置转移 原本将验证码存放于session中验证时需要拿用户的验证码与session的验证码做对比在这里采用redis存储验证码进行了优化。 使用redis存储验证码验证码需要频繁刷新和访问对性能要求较高并且验证码不需要永久保存通常在很短的时间内就会失效而且分布式部署时存在session问题 具体操作为了分辨这是哪一个用户登录所需要的验证码定义一个随机字符串key分别存储于浏览器的cookie中和redis中redis以随机字符串为键以验证码为值去判断与用户输入的验证码是否相同。  代码生成验证码和随机字符串随机字符串存入cookie中随机字符串还与验证码作为键值对存入redis中 RequestMapping(path /kaptcha, method RequestMethod.GET)public void getKaptcha(HttpServletResponse response/*, HttpSession session*/) {// 生成验证码String text kaptchaProducer.createText();BufferedImage image kaptchaProducer.createImage(text);// 将验证码存入session// session.setAttribute(kaptcha, text);// 验证码的归属String kaptchaOwner CommunityUtil.generateUUID();//设置过期时间Cookie cookie new Cookie(kaptchaOwner, kaptchaOwner);cookie.setMaxAge(60);cookie.setPath(contextPath);response.addCookie(cookie);// 将验证码存入RedisString redisKey RedisKeyUtil.getKaptchaKey(kaptchaOwner);redisTemplate.opsForValue().set(redisKey, text, 60, TimeUnit.SECONDS);// 将突图片输出给浏览器response.setContentType(image/png);try {OutputStream os response.getOutputStream();ImageIO.write(image, png, os);} catch (IOException e) {logger.error(响应验证码失败: e.getMessage());}} 登录时通过浏览器的cookie取得该用户的特有的标识符通过标识符找到redis中对应的值与用户输入的验证码进行比对并校验账号密码是否正确。 RequestMapping(path /login, method RequestMethod.POST)public String login(String username, String password, String code, boolean rememberme,Model model, /*HttpSession session, */HttpServletResponse response,CookieValue(kaptchaOwner) String kaptchaOwner) {// 检查验证码// String kaptcha (String) session.getAttribute(kaptcha);String kaptcha null;if (StringUtils.isNotBlank(kaptchaOwner)) {String redisKey RedisKeyUtil.getKaptchaKey(kaptchaOwner);kaptcha (String) redisTemplate.opsForValue().get(redisKey);}if (StringUtils.isBlank(kaptcha) || StringUtils.isBlank(code) || !kaptcha.equalsIgnoreCase(code)) {model.addAttribute(codeMsg, 验证码不正确!);return /site/login;}// 检查账号,密码int expiredSeconds rememberme ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS;MapString, Object map userService.login(username, password, expiredSeconds);if (map.containsKey(ticket)) {Cookie cookie new Cookie(ticket, map.get(ticket).toString());cookie.setPath(contextPath);cookie.setMaxAge(expiredSeconds);response.addCookie(cookie);return redirect:/index;} else {model.addAttribute(usernameMsg, map.get(usernameMsg));model.addAttribute(passwordMsg, map.get(passwordMsg));return /site/login;}} 6.2 登录凭证存储位置转移 将用户生成的登录凭证存储到redis中 登录的部分示例代码登录时用户生成一个登录凭证存进redis中也存进cookie中不安全设置状态为0即在线状态 退出登录即将登录状态改为1为失效状态 代码如下 public void logout(String ticket) { // loginTicketMapper.updateStatus(ticket, 1);String redisKey RedisKeyUtil.getTicketKey(ticket);LoginTicket loginTicket (LoginTicket) redisTemplate.opsForValue().get(redisKey);loginTicket.setStatus(1);redisTemplate.opsForValue().set(redisKey, loginTicket);} 检查登录状态即拿着cookie中的ticket去redis中找loginticket其实是很不安全的 代码如下 public LoginTicket findLoginTicket(String ticket) { // return loginTicketMapper.selectByTicket(ticket);String redisKey RedisKeyUtil.getTicketKey(ticket);return (LoginTicket) redisTemplate.opsForValue().get(redisKey);} 其实这里可以采用JWT token的方式实现单点登录可避免遭受攻击 6.2.1、什么是JWT Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519). 该token被设计为紧凑且安全的特别适用于分布式站点的单点登录SSO场景。 JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息以便于从资源服务器获取资源也可以增加一些额外的其它业务逻辑所必须的声明信息该token也可直接被用于认证也可被加密。 即一个token字符串里面可以保存用户的id等非秘密信息通过jwt加密服务端的密钥生成token相当于把用户信息压缩等到了服务端再进行解码 可以来一段代码示例可以清楚弄懂机制 将用户信息用jwt及服务端密钥生成token并将token存入服务端redis中用于验证用户的都登录状态 配置登录拦截器未登录不可访问的路径 package com.mzlu.blog.config;import com.mzlu.blog.handler.LoginInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;Configuration public class WebConfig implements WebMvcConfigurer { Autowired private LoginInterceptor loginInterceptor;Overridepublic void addCorsMappings(CorsRegistry registry) {//跨域配置不可设置为*不安全, 前后端分离项目可能域名不一致//本地测试 端口不一致 也算跨域registry.addMapping(/**).allowedOrigins(http://localhost:8080);}/*** 拦截器*/Overridepublic void addInterceptors(InterceptorRegistry registry) {//拦截test接口后续实际遇到需要拦截的接口时在配置为真正的拦截接口registry.addInterceptor(loginInterceptor).addPathPatterns(/test).addPathPatterns(/comments/create/change).addPathPatterns(/articles/publish);}}拦截器prehandle方法所做的事情 1、从请求头的Authorization中获取到用户的token如果请求头中没有token说明用户根本没有登录 2、如果说请求头中有token首先根据服务端的密钥进解密看这个token是否合法可能存在伪造情况), 3、再会去redis中看看登陆状态是否过期如果没有过期则将其token解密将用户信息解析出来放到threadlocal中。 package com.mzlu.blog.handler;import com.alibaba.fastjson.JSON; import com.mysql.cj.util.StringUtils; import com.mzlu.blog.dao.pojo.SysUsers; import com.mzlu.blog.service.LoginService; import com.mzlu.blog.utils.UserThreadLocal; import com.mzlu.blog.vo.ErrorCode; import com.mzlu.blog.vo.Result; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;Component Slf4j public class LoginInterceptor implements HandlerInterceptor {Autowiredprivate LoginService loginService;Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {if (!(handler instanceof HandlerMethod)){return true;}String token request.getHeader(Authorization);log.info(request start);String requestURI request.getRequestURI();log.info(request uri:{},requestURI);log.info(request method:{},request.getMethod());log.info(token:{}, token);log.info(request end);if (token null){Result result Result.fail(ErrorCode.NO_LOGIN.getCode(), 未登录);response.setContentType(application/json;charsetutf-8);response.getWriter().print(JSON.toJSONString(result));return false;}SysUsers sysUser loginService.checkToken(token);if (sysUser null){Result result Result.fail(ErrorCode.NO_LOGIN.getCode(), 未登录);response.setContentType(application/json;charsetutf-8);response.getWriter().print(JSON.toJSONString(result));return false;}//是登录状态放行//希望在controller中直接获取用户的信息怎么获取System.out.println(-------------------------------);System.out.println(yonghuxinxi sysUser);System.out.println(-------------------------------);UserThreadLocal.put(sysUser);return true;}/*** 删除ThreadLocal中的值* param request* param response* param handler* param ex* throws Exception*/Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//如果不删除ThreadLocal中的值便有内存泄露的风险UserThreadLocal.remove();} } JWT工具类生成token及根据密钥检验token的合法性 package com.mzlu.blog.utils;import io.jsonwebtoken.Jwt; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date; import java.util.HashMap; import java.util.Map;/*** JWT工具类*/ public class JWTUtils {private static final String jwtToken 123456Mszlu!#$$;public static String createToken(Long userId){MapString,Object claims new HashMap();claims.put(userId,userId);JwtBuilder jwtBuilder Jwts.builder().signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法秘钥为jwtToken.setClaims(claims) // body数据要唯一自行设置.setIssuedAt(new Date()) // 设置签发时间.setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 60 * 1000));// 一天的有效时间String token jwtBuilder.compact();return token;} //用户验证看看是否合法public static MapString, Object checkToken(String token){try {Jwt parse Jwts.parser().setSigningKey(jwtToken).parse(token);return (MapString, Object) parse.getBody();}catch (Exception e){e.printStackTrace();}return null;}public static void main(String[] args) {String tokenJWTUtils.createToken(100L);System.out.println(token);MapString,Object mapJWTUtils.checkToken(token);System.out.println(map.get(userId));}} 用户登录生成token将token存放进request域的“Authorzation”中和服务端的redis中 package com.mzlu.blog.service.impl;import com.alibaba.fastjson.JSON; import com.mysql.cj.util.StringUtils; import com.mzlu.blog.dao.pojo.SysUsers; import com.mzlu.blog.service.LoginService; import com.mzlu.blog.service.SysUserService; import com.mzlu.blog.utils.JWTUtils; import com.mzlu.blog.vo.ErrorCode; import com.mzlu.blog.vo.Result; import com.mzlu.blog.vo.params.LoginParam; import org.apache.commons.codec.digest.DigestUtils; import org.apache.ibatis.annotations.Mapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service;import java.util.Map; import java.util.concurrent.TimeUnit;Service public class LoginImpl implements LoginService {Autowiredprivate SysUserService sysUserService;Autowiredprivate RedisTemplateString,String redisTemplate;//设置加密盐private static final String slat mszlu!#;Overridepublic Result login(LoginParam loginParam) {/*** 1.检查参数是否合法* 2.根据用户名和密码去user表中查询是否存在* 3.如果不存在 登录失败* 4.如果存在使用jwt 生成token 返回给前端* 5.把token放入redis中redis tokenuser信息 设置过期时间(登录认证时先验证字符串是否合法再判断是否存在)*/String accountloginParam.getAccount();String passwordloginParam.getPassword();//判断用户名和密码是否为空if(StringUtils.isEmptyOrWhitespaceOnly(account)||StringUtils.isEmptyOrWhitespaceOnly(password)){return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());}password DigestUtils.md5Hex(password slat);//判断用户名和密码是否储存在数据库中SysUsers sysUserssysUserService.findUser(account,password);if(sysUsersnull){return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());}System.out.println(------------------------);System.out.print(sysusersysUsers.getId());String token JWTUtils.createToken(sysUsers.getId());//用redisTemplate向redis中存放键值过期时间redisTemplate.opsForValue().set(TOKEN_token, JSON.toJSONString(sysUsers),1, TimeUnit.DAYS);return Result.success(token);}Overridepublic SysUsers checkToken(String token) {//先检验token是否为空if(StringUtils.isEmptyOrWhitespaceOnly(token)){return null;}//再检验JWT用户中是否保存MapString,ObjectstringObjectMapJWTUtils.checkToken(token);if(stringObjectMapnull){return null;}//从redis拿出来看看键值是否为空String userJsonredisTemplate.opsForValue().get(TOKEN_token);System.out.println(这是jsonuserJson);if(StringUtils.isEmptyOrWhitespaceOnly(userJson)) {return null;}//用fastJson将字符串转化为类对象SysUsers sysUsersJSON.parseObject(userJson,SysUsers.class);return sysUsers;}6.2.2、JWT的组成 1、JWT生成编码后的样子 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q 复制代码 2、JWT由三部分构成 第一部分我们称它为头部header),第二部分我们称其为载荷payload, 类似于飞机上承载的物品)第三部分是签证signature). header jwt的头部承载两部分信息 声明类型这里是jwt 声明加密的算法 通常直接使用 HMAC SHA256 完整的头部就像下面这样的JSON { typ: JWT, alg: HS256} 复制代码 然后将头部进行base64加密该加密是可以对称解密的),构成了第一部分 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 复制代码 playload 载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品这些有效信息包含三个部分 标准中注册的声明 公共的声明 私有的声明 标准中注册的声明 (建议但不强制使用) iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识主要用来作为一次性token,从而回避重放攻击。 公共的声明 公共的声明可以添加任何的信息一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息因为该部分在客户端可解密. 私有的声明 私有声明是提供者和消费者所共同定义的声明一般不建议存放敏感信息因为base64是对称解密的意味着该部分信息可以归类为明文信息。 定义一个payload: { sub: 1234567890, name: John Doe, admin: true} 复制代码 然后将其进行base64加密得到Jwt的第二部分 eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9 复制代码 signature jwt的第三部分是一个签证信息这个签证信息由三部分组成 header (base64后的) payload (base64后的) secret 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串(头部在前)然后通过header中声明的加密方式进行加盐secret组合加密然后就构成了jwt的第三部分。 UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q 复制代码 密钥secret是保存在服务端的服务端会根据这个密钥进行生成token和验证所以需要保护好。 3、签名的目的 最后一步签名的过程实际上是对头部以及载荷内容进行签名。一般而言加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入产生同样的输出的概率极其地小有可能比我成世界首富的概率还小。所以我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。 所以如果有人对头部以及载荷的内容解码之后进行修改再进行编码的话那么新的头部和载荷的签名和之前的签名就将是不一样的。而且如果不知道服务器加密的时候用的密钥的话得出来的签名也一定会是不一样的。 服务器应用在接受到JWT后会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢别忘了我们在JWT的头部中已经用alg字段指明了我们的加密算法了。 如果服务器应用对头部和载荷再次以同样方法签名之后发现自己计算出来的签名和接受到的签名不一样那么就说明这个Token的内容被别人动过的我们应该拒绝这个Token返回一个HTTP 401 Unauthorized响应。 注意在JWT中不应该在载荷里面加入任何敏感的数据比如用户的密码。 4、如何应用 一般是在请求头里加入Authorization并加上Bearer标注 fetch(api/user/1, { headers: { Authorization: Bearer token }}) 复制代码 服务端会验证token如果验证通过就会返回相应的资源。 5、安全相关 不应该在jwt的payload部分存放敏感信息因为该部分是客户端可解密的部分。 保护好secret私钥该私钥非常重要。 如果可以请使用https协议 6、对Token认证的五点认识 一个Token就是一些信息的集合 在Token中包含足够多的信息以便在后续请求中减少查询数据库的几率 服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查 基于上一点你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端 因为token是被签名的所以我们可以认为一个可以解码认证通过的token是由我们系统发放的其中带的信息是合法有效的 三、传统的session认证 我们知道http协议本身是一种无状态的协议而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证那么下一次请求时用户还要再一次进行用户认证才行因为根据http协议我们并不能知道是哪个用户发出的请求所以为了让我们的应用能识别是哪个用户发出的请求我们只能在服务器存储一份用户登录的信息这份登录信息会在响应时传递给浏览器告诉其保存为cookie,以便下次请求时发送给我们的应用这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。 但是这种基于session的认证使应用本身很难得到扩展随着不同客户端用户的增加独立的服务器已无法承载更多的用户而这时候基于session认证应用的问题就会暴露出来。 基于session认证所显露的问题 Session: 每个用户经过我们的应用认证之后我们的应用都要在服务端做一次记录以方便用户下次请求的鉴别通常而言session都是保存在内存中而随着认证用户的增多服务端的开销会明显增大。 扩展性: 用户认证之后服务端做认证记录如果认证的记录被保存在内存中的话这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源这样在分布式的应用上相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。 CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获用户就会很容易受到跨站请求伪造的攻击。 基于token的鉴权机制 基于token的鉴权机制类似于http协议也是无状态的它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了这就为应用的扩展提供了便利。 流程上是这样的 用户使用用户名密码来请求服务器 服务器进行验证用户的信息 服务器通过验证发送给用户一个token 客户端存储token并在每次请求时附送上这个token值 服务端验证token值并返回数据 这个token必须要在每次请求时传递给服务端它应该保存在请求头里 另外服务端要支持CORS(跨来源资源共享)策略一般我们在服务端这么做就可以了 Access-Control-Allow-Origin:*。 四、token的优点 支持跨域访问: Cookie是不允许垮域访问的这一点对Token机制是不存在的前提是传输的用户认证信息通过HTTP头传输。 无状态(也称服务端可扩展行):Token机制在服务端不需要存储session信息因为Token 自身包含了所有登录用户的信息只需要在客户端的cookie或本地介质存储状态信息。 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料如javascriptHTML,图片等而你的服务端只要提供API即可。 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成只要在你的API被调用的时候你可以进行Token生成调用即可。 更适用于移动应用: 当你的客户端是一个原生平台iOS, AndroidWindows 8等时Cookie是不被支持的你需要通过Cookie容器进行处理这时采用Token认证机制就会简单得多。 CSRF:因为不再依赖于Cookie所以你就不需要考虑对CSRF跨站请求伪造的防范。 性能: 一次网络往返时间通过数据库查询session信息总比做一次HMACSHA256计算 的Token验证和解析要费时得多。 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候不再需要为登录页面做特殊处理。 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库.NET, Ruby, Java,Python, PHP和多家公司的支持如Firebase,Google, Microsoft。 因为json的通用性所以JWT是可以进行跨语言支持的像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。 因为有了payload部分所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息到服务端再进行解密减少了空间。 便于传输jwt的构成非常简单字节占用很小所以它是非常便于传输的。 它不需要在服务端保存会话信息, 所以它易于应用的扩展。 jwt仍然需要注意的地方 将token放在请求头中的Authorization中并不一定安全因为token可能会被截获或者泄露1。为了提高安全性您可以采取以下一些措施 使用https协议来加密传输token防止被中间人攻击使用较短的token过期时间减少token被盗用的风险使用一些额外的信息来增强token的安全性比如用户的IP地址、设备信息等使用一些开源的库或者框架来实现token的生成和验证比如spring security oauth2。 6.3 使用redis作为缓存缓存用户信息 将用户信息也存到redis中作为缓存如果有更改则更新缓存。 7、kafka消息队列的使用 消息队列的作用 1、通过异步处理提高系统性能减少响应所需时间 2、削峰/限流 3、降低系统耦合性 作为系统的消息通知比如一个用户给另一个用户点赞则另一个用户会收到系统发布点赞的消息 对于点赞、评论、关注三种不同的事件可以定义三种不同的topic 消息队列是可以异步的生产者生产出消息后放进消息队列后不用管后面的事情由消费者线程去解决它。 我们可以来看一下代码示例定义消费者consumer)和生产者(producer) 生产者定义主题即定义指定的消息队列往队列内发送消息 消费者定义需要监听的主题即分配好需要处理的消息通道有哪些进行处理 package com.nowcoder.community;import org.apache.kafka.clients.consumer.ConsumerRecord; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner;RunWith(SpringRunner.class) SpringBootTest ContextConfiguration(classes CommunityApplication.class) public class KafkaTests {Autowiredprivate KafkaProducer kafkaProducer;Testpublic void testKafka() { //生产者发送消息定义主题为“test”kafkaProducer.sendMessage(test, 你好);kafkaProducer.sendMessage(test, 在吗);try {Thread.sleep(1000 * 10);} catch (InterruptedException e) {e.printStackTrace();}}}Component class KafkaProducer {Autowiredprivate KafkaTemplate kafkaTemplate;public void sendMessage(String topic, String content) {kafkaTemplate.send(topic, content);}}Component class KafkaConsumer {//消费者处理监听到指定主题的消息进行消费KafkaListener(topics {test})public void handleMessage(ConsumerRecord record) {System.out.println(record.value());}} 在此项目中因为消息通知的内容是要写进数据库的因此比如当用户给某人点赞点赞事件触发了消息队列的使用A给B点赞之后生产者生产出点赞的消息内容如谁给谁点赞是对用户点赞还是用户的文章还是对用户的评论点赞消费者拿到后去处理将这些内容写入数据库 即点赞和点赞的内容写入数据库这两部分是异步的点赞事件无需等待数据写入数据库后才算完成。 代码如下所示 消费者 package com.nowcoder.community.event;import com.alibaba.fastjson.JSONObject; import com.nowcoder.community.entity.DiscussPost; import com.nowcoder.community.entity.Event; import com.nowcoder.community.entity.Message; import com.nowcoder.community.service.DiscussPostService; import com.nowcoder.community.service.ElasticsearchService; import com.nowcoder.community.service.MessageService; import com.nowcoder.community.util.CommunityConstant; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.annotation.KafkaListener; import org.springframework.stereotype.Component;import java.util.Date; import java.util.HashMap; import java.util.Map;Component public class EventConsumer implements CommunityConstant {private static final Logger logger LoggerFactory.getLogger(EventConsumer.class);Autowiredprivate MessageService messageService;Autowiredprivate DiscussPostService discussPostService;Autowiredprivate ElasticsearchService elasticsearchService;KafkaListener(topics {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})public void handleCommentMessage(ConsumerRecord record) {if (record null || record.value() null) {logger.error(消息的内容为空!);return;}Event event JSONObject.parseObject(record.value().toString(), Event.class);if (event null) {logger.error(消息格式错误!);return;}// 发送站内通知Message message new Message();message.setFromId(SYSTEM_USER_ID);message.setToId(event.getEntityUserId());message.setConversationId(event.getTopic());message.setCreateTime(new Date());MapString, Object content new HashMap();content.put(userId, event.getUserId());content.put(entityType, event.getEntityType());content.put(entityId, event.getEntityId());if (!event.getData().isEmpty()) {for (Map.EntryString, Object entry : event.getData().entrySet()) {content.put(entry.getKey(), entry.getValue());}}message.setContent(JSONObject.toJSONString(content));messageService.addMessage(message);}// 消费发帖事件KafkaListener(topics {TOPIC_PUBLISH})public void handlePublishMessage(ConsumerRecord record) {if (record null || record.value() null) {logger.error(消息的内容为空!);return;}Event event JSONObject.parseObject(record.value().toString(), Event.class);if (event null) {logger.error(消息格式错误!);return;}DiscussPost post discussPostService.findDiscussPostById(event.getEntityId());elasticsearchService.saveDiscussPost(post);}}生产者代码如下所示 package com.nowcoder.community.event;import com.alibaba.fastjson.JSONObject; import com.nowcoder.community.entity.Event; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.stereotype.Component;Component public class EventProducer {Autowiredprivate KafkaTemplate kafkaTemplate;// 处理事件public void fireEvent(Event event) {// 将事件发布到指定的主题kafkaTemplate.send(event.getTopic(), JSONObject.toJSONString(event));}}8、通过建立缓存优化网站的性能 因为对MYSQL的操作是读取磁盘速度并不快可以使用redis基于内存读取caffeine高性能本地缓存库作为缓存读取mysql的数据用户可以从缓存中读取干净的数据提高响应速度对于修改过的数据缓存会进行同步。 这个项目中采用多级缓存缓存文章列表和十大热门文章采用caffeine作为本地缓存针对单个用户的缓存采用redis作为分布式缓存(存储所有用户的缓存)用户读取数据时优先读取本地缓存如果本地缓存没有则去redis的缓存中读取如果还没有才去DB读取同步顺序倒过来即DB先同步redis中的缓存再去同步用户的本地缓存 即所有用户是共享分布式缓存的独享本地缓存caffeine) 8.1 有哪些常见的缓存 8.2 caffeine与redis相比相同和不同之处 caffeine是存在服务器的缓存它是一个基于Java的本地缓存库跟redis相比有以下相同和不同的地方 相同点都是用来存储键值对数据的缓存都支持过期策略和回收策略都可以提高数据访问的效率。不同点caffeine是一个内存缓存redis是一个分布式缓存caffeine只能在单个进程内使用redis可以在多个进程或者多个服务器之间共享数据caffeine使用Window TinyLfu回收策略redis使用LRU或者LFU回收策略caffeine不需要网络通信redis需要网络通信。 8.3  多级缓存的优势在于 多级缓存相比单级缓存的优势在于 可以利用不同层级的缓存特性比如应用层缓存可以提供最快的响应速度如caffeine)分布式缓存可以提供高可用性和一致性系统层缓存(操作系统层面)可以提供更大的容量和更低的成本等。可以降低数据库压力减少网络开销提高系统性能。可以增强系统的可用性和容错性当某一层缓存出现故障或者不可用时可以从其他层缓存或者数据库中获取数据。 8.4 基于注解的caffeine简单使用 1、引入maven配置 // 引入caffeine和spring cache的依赖 dependencygroupIdcom.github.ben-manes.caffeine/groupIdartifactIdcaffeine/artifactId /dependency dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-cache/artifactId /dependency2、在配置文件中加入配置 // 在配置文件中设置缓存的名称和参数 spring:cache:cache-names: user, product # 缓存名称caffeine:spec: maximumSize1000,expireAfterWrite10m # 公共参数user: maximumSize500,expireAfterAccess5m # 用户缓存参数 3、在项目启动类上添加EnableCaching注解开启缓存功能 // 在启动类上添加EnableCaching注解开启缓存功能 SpringBootApplication EnableCaching // 开启缓存功能 public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);} } 4、定义一个用户实体类 // 定义一个用户实体类使用lombok简化代码 Data // 自动生成get/set/toString等方法 AllArgsConstructor // 自动生成全参构造方法 NoArgsConstructor // 自动生成无参构造方法 public class User {private String id;private String name;private int age; } 5、定义一个用户服务接口类模拟数据库操作和缓存操作 // 定义一个用户服务接口类模拟数据库操作和缓存操作 public interface UserService {// 获取用户如果缓存中没有则从数据库或其他地方获取并放入缓存Cacheable(value user, key #id)User getUser(String id);// 更新用户同时更新数据库或其他地方的数据并更新缓存CachePut(value user, key #user.id)User updateUser(User user);// 删除用户同时删除数据库或其他地方的数据并删除缓存CacheEvict(value user, key #id)void deleteUser(String id); } 6、定义一个用户实现类实现接口方法并模拟数据库操作和缓存操作 // 定义一个用户服务实现类实现接口方法并模拟数据库操作和缓存操作 Service public class UserServiceImpl implements UserService {// 模拟一个数据库用map存储用户数据private MapString, User userMap new HashMap();// 初始化一些用户数据public UserServiceImpl() {userMap.put(1, new User(1, 张三, 20));userMap.put(2, new User(2, 李四, 21));userMap.put(3, new User(3, 王五, 22));}Overridepublic User getUser(String id) {System.out.println(从数据库中获取用户 id);return userMap.get(id);}Overridepublic User updateUser(User user) {System.out.println(更新数据库中的用户 user);userMap.put(user.getId(), user);return user;}Overridepublic void deleteUser(String id) {System.out.println(删除数据库中的用户 id);userMap.remove(id);} }7、定义一个测试控制器类用来测试缓存效果 // 定义一个测试控制器类用来测试缓存的效果 RestController RequestMapping(/user) public class UserController {Autowiredprivate UserService userService;// 根据id获取用户信息第一次访问会从数据库中获取之后会从缓存中获取GetMapping(/{id})public User getUser(PathVariable String id) {return userService.getUser(id);}// 更新用户信息会同时更新数据库和缓存PutMapping(/{id})public User updateUser(PathVariable String id, RequestParam String name, RequestParam int age) {User user new User(id, name, age);return userService.updateUser(user);}// 删除用户信息会同时删除数据库和缓存DeleteMapping(/{id})public void deleteUser(PathVariable String id) {userService.deleteUser(id);} }
http://www.hkea.cn/news/14466887/

相关文章:

  • 陕西秦地建设有限公司网站论坛前端模板
  • 中铁建设集团华东分公司网站江西网站备案
  • 东莞网站建设+旅游软件界面设计与色彩搭配
  • 上海互联网网站建设国内优秀食品包装设计
  • 网络营销如何进行网站推广红酒营销 网站建设
  • 特步的网站建设策划手机静态网站开发制作
  • 电信备案网站打不开网站建设贰金手指下拉壹玖
  • jsp asp php哪个做网站网站建设免费建站免费源代码
  • 网站开发的毕业设计论文框架清远市专业网站制作
  • 单页展示网站wordpress分类文章数
  • wordpress m1 cms360搜索怎么做网站自然优化
  • 汕头网站建设托管延安做网站的公司电话
  • 公司网站建设及安全解决方案软文案例400字
  • 网站子目录是什么crm营销
  • 如何做网站登录界面松江外贸网站建设
  • 唐山房产网站建设wordpress怎么修改菜单栏关键词
  • apache 配置网站陕西网站备案代理
  • 一个人只做网站的流程昆明网站开发多少钱
  • 哪做网站比较便宜做好的网站如何上线
  • 天津网站建设icp备大连服务公司 网站
  • 韩国flash网站外卖网站怎么做
  • 精品资源共享课网站建设新浪博客
  • 如何修改wordpress站杭州最新消息
  • 电子商务网站建设方案书的总结三亚百度推广开户
  • 蜘蛛云建站网站网站改版新闻稿
  • 加强网站集约化建设水果香精东莞网站建设技术支持
  • 化妆品网站建设建设网站的企业
  • 江阴网站制作建设网站教程
  • 网站模板下载网站有哪些品牌网站建设推广
  • 长沙公司做网站的价格做网站 空间还是服务器