STS创建Spring Boot项目实战(Rest接口、数据库、用户认证、分布式Token JWT、Redis操作、日志和统一异常处理)

来源:互联网 发布:宁夏广电网络总经理 编辑:程序博客网 时间:2024/06/06 04:00

非常感谢  http://blog.csdn.net/he90227/article/details/53308222

目录(?)[+]



1.项目创建


1、新建工程




2、选择打包方式,这边可以选择为打包为Jar包,或者传统的打包为War包



3、选择开发过程中使用到的技术,这边我选择的是Rest Repositories



4、新建测试用Controller



    文件内容如下

[java] view plain copy
  1. package com.xiaofangtech.example;  
  2.   
  3. import org.springframework.web.bind.annotation.RequestMapping;  
  4. import org.springframework.web.bind.annotation.RestController;  
  5.   
  6. @RestController  
  7. public class HelloController {  
  8.     @RequestMapping("/greeting")  
  9.     public String hello()  
  10.     {  
  11.         return "Hello world";  
  12.     }  
  13. }  

5、以Srping Boot App 方式运行



正常运行后控制台如下



6、测试运行


至此,一个最简单的hello world的工程创建运行完成


7、打包部署

   7.1 打包为可运行jar包

   使用mvn package 进行打包

   



 然后run ,运行成功后如下生成jar包






 7.2 打包为传统的war包 

       当第2步中选择的打包方式为war时,执行7.1中mvn package时,生成的包就是war包

     



运行war包跟运行jar包一样,找到war包所在目录,注解运行war包


D:\new_tech\spring-suite-tool\workspace\workspace1\demo1\target>Java -jar demo1-0.0.1-SNAPSHOT.war


访问服务接口: localhost:8080/greeting



2.代码实现连接数据实现Rest接口和Basic 基础认证

0.引入pom依赖

[html] view plain copy
  1. <dependency>  
  2.             <groupId>org.projectlombok</groupId>  
  3.             <artifactId>lombok</artifactId>  
  4.         </dependency>  
  5.   
  6.         <!-- 使用MySQL数据库,并用Spring Data JPA来作为数据库访问 -->  
  7.         <dependency>  
  8.             <groupId>org.springframework.boot</groupId>  
  9.             <artifactId>spring-boot-starter-data-jpa</artifactId>  
  10.         </dependency>  
  11.         <dependency>  
  12.             <groupId>mysql</groupId>  
  13.             <artifactId>mysql-connector-java</artifactId>  
  14.         </dependency>  




1.代码整体结构



2.注册Filter过滤器的两种方式:
        1.在自定义的Filter上使用注解:
           
[java] view plain copy
  1. /* 
  2.  * Filter实现简单的Http Basic 认证  
  3.  */  
  4. @Component  
  5. @WebFilter(filterName = "httpBasicAuthorizedFilter", urlPatterns="/user/*")  
  6. public class HttpBasicAuthorizeFilter implements Filter {  

       2.在配置类中定义Filter

[java] view plain copy
  1. @Bean    
  2.     public FilterRegistrationBean  filterRegistrationBean() {    
  3.         FilterRegistrationBean registrationBean = new FilterRegistrationBean();    
  4.         HttpBasicAuthorizeFilter httpBasicFilter = new HttpBasicAuthorizeFilter();    
  5.         registrationBean.setFilter(httpBasicFilter);    
  6.         List<String> urlPatterns = new ArrayList<String>();    
  7.         urlPatterns.add("/user/*");    
  8.         registrationBean.setUrlPatterns(urlPatterns);    
  9.         return registrationBean;    
  10.     }    


3.数据库和Rest接口操作效果展示:



4.过滤器效果展示
  
代码中固定用户名密码都为test,所以对接口进行请求时,需要添加以下认证头信息

Authorization: Basic dGVzdDp0ZXN0

dGVzdDp0ZXN0 为 test:test 经过base64编码后的结果


如果未添加认证信息或者认证信息错误,返回没有权限的错误信息




当认证信息正确,返回请求结果



    3.自定义Properties解析类和分布式Token JWT用户校验

        1.自定义Properties解析类的使用规则

                      1.定义Properties配置文件   ---- jwt.properties
                          
    [html] view plain copy
    1. jwt.info.clientId=098f6bcd4621d373cade4e832627b4f6  
    2. jwt.info.base64Secret=MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=  
    3. jwt.info.name=restapiuser  
    4. jwt.info.expiresSecond=172800  


                      2.自定义解析类                      ---- JwtInfo.java  指定配置文件地址和配置前缀,属性是前缀之后的名称
    [java] view plain copy
    1. package com.jay.properties;  
    2.   
    3. import org.springframework.boot.context.properties.ConfigurationProperties;  
    4. /* 
    5.  * 自定义配置文件的解析类 
    6.  */  
    7. @ConfigurationProperties(prefix = "jwt.info", locations = "classpath:/config/jwt.properties")  
    8. public class JwtInfo {  
    9.     private String clientId;    
    10.     private String base64Secret;    
    11.     private String name;    
    12.     private int expiresSecond;  
    13.     public String getClientId() {  
    14.         return clientId;  
    15.     }  
    16.     public void setClientId(String clientId) {  
    17.         this.clientId = clientId;  
    18.     }  
    19.     public String getBase64Secret() {  
    20.         return base64Secret;  
    21.     }  
    22.     public void setBase64Secret(String base64Secret) {  
    23.         this.base64Secret = base64Secret;  
    24.     }  
    25.     public String getName() {  
    26.         return name;  
    27.     }  
    28.     public void setName(String name) {  
    29.         this.name = name;  
    30.     }  
    31.     public int getExpiresSecond() {  
    32.         return expiresSecond;  
    33.     }  
    34.     public void setExpiresSecond(int expiresSecond) {  
    35.         this.expiresSecond = expiresSecond;  
    36.     }  
    37.       
    38. }  



                      3.启动类或配置类中,指定自定义Properties解析类
    [java] view plain copy
    1. package com.jay;  
    2.   
    3. import org.springframework.boot.SpringApplication;  
    4. import org.springframework.boot.autoconfigure.SpringBootApplication;  
    5. import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties.Jwt;  
    6. import org.springframework.boot.context.properties.EnableConfigurationProperties;  
    7. import org.springframework.boot.web.servlet.ServletComponentScan;  
    8.   
    9. import com.jay.properties.JwtInfo;  
    10.   
    11. @SpringBootApplication  
    12. @EnableConfigurationProperties(JwtInfo.class)  //加载自定义的properties解析类  
    13. public class Demo1Application {  
    14.   
    15.     public static void main(String[] args) {  
    16.         SpringApplication.run(Demo1Application.class, args);  
    17.     }  
    18. }  

                   4.输出配置文件信息         ---- JwtInfoController.java

    [java] view plain copy
    1. package com.jay.controller;  
    2.   
    3. import org.springframework.beans.factory.annotation.Autowired;  
    4. import org.springframework.web.bind.annotation.RequestMapping;  
    5. import org.springframework.web.bind.annotation.RequestMethod;  
    6. import org.springframework.web.bind.annotation.RestController;  
    7.   
    8. import com.jay.properties.JwtInfo;  
    9. import com.jay.vo.ResultMsg;  
    10. import com.jay.vo.ResultStatusCode;  
    11.   
    12. @RestController  
    13. @RequestMapping("/jwt")  
    14. public class JwtInfoController {  
    15.   
    16.     @Autowired  
    17.     private JwtInfo jwtInfo;  
    18.   
    19.     @RequestMapping(value = "/info", method = RequestMethod.GET)  
    20.     public Object getJwtInfo() {  
    21.         return new ResultMsg<JwtInfo>(true, ResultStatusCode.OK.getErrorCode(), ResultStatusCode.OK.getErrorMsg(), jwtInfo);  
    22.     }  
    23. }  

                   5.效果展示
                  


              2.使用分布式token  JWT进行用户认证


                  

    jwt(json web token)

    用户发送按照约定,向服务端发送 Header、Payload 和 Signature,并包含认证信息(密码),验证通过后服务端返回一个token,之后用户使用该token作为登录凭证,适合于移动端和api


    jwt使用流程



                  1.添加 JWT依赖
    [html] view plain copy
    1. <!-- JWT Json Web Token 依赖 -->  
    2.         <dependency>  
    3.             <groupId>io.jsonwebtoken</groupId>  
    4.             <artifactId>jjwt</artifactId>  
    5.             <version>0.7.0</version>  
    6.         </dependency>  

                 2.编写Jwt解析类和Jwt过滤器
                 
    [java] view plain copy
    1. package com.jay.util.jwt;  
    2.   
    3. import java.security.Key;  
    4. import java.util.Date;  
    5.   
    6. import javax.crypto.spec.SecretKeySpec;  
    7. import javax.xml.bind.DatatypeConverter;  
    8.   
    9. import io.jsonwebtoken.Claims;  
    10. import io.jsonwebtoken.JwtBuilder;  
    11. import io.jsonwebtoken.Jwts;  
    12. import io.jsonwebtoken.SignatureAlgorithm;  
    13.   
    14. /* 
    15.  * 构造及解析jwt的工具类 
    16.  */  
    17. public class JwtHelper {  
    18.     public static Claims parseJWT(String jsonWebToken, String base64Security){  
    19.         try  
    20.         {  
    21.             Claims claims = Jwts.parser()  
    22.                        .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))  
    23.                        .parseClaimsJws(jsonWebToken).getBody();  
    24.             return claims;  
    25.         }  
    26.         catch(Exception ex)  
    27.         {  
    28.             return null;  
    29.         }  
    30.     }  
    31.       
    32.     /** 
    33.      * 生成token 
    34.      *  
    35.      * @author hetiewei 
    36.      * @date 2016年10月18日 下午2:51:38 
    37.      * @param name     keyId 
    38.      * @param userId    
    39.      * @param role 
    40.      * @param audience   接收者 
    41.      * @param issuer     发行者 
    42.      * @param TTLMillis  过期时间(毫秒) 
    43.      * @param base64Security 
    44.      * @return 
    45.      */  
    46.     public static String createJWT(String name, String userId, String role,   
    47.             String audience, String issuer, long TTLMillis, String base64Security)   
    48.     {  
    49.         SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;  
    50.            
    51.         long nowMillis = System.currentTimeMillis();  
    52.         Date now = new Date(nowMillis);  
    53.            
    54.         //生成签名密钥  
    55.         byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(base64Security);  
    56.         Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());  
    57.            
    58.           //添加构成JWT的参数  
    59.         JwtBuilder builder = Jwts.builder().setHeaderParam("typ""JWT")  
    60.                                         .claim("role", role)  
    61.                                         .claim("unique_name", name)  
    62.                                         .claim("userid", userId)  
    63.                                         .setIssuer(issuer)  
    64.                                         .setAudience(audience)  
    65.                                         .signWith(signatureAlgorithm, signingKey);  
    66.          //添加Token过期时间  
    67.         if (TTLMillis >= 0) {  
    68.             long expMillis = nowMillis + TTLMillis;  
    69.             Date exp = new Date(expMillis);  
    70.             builder.setExpiration(exp).setNotBefore(now);  
    71.         }  
    72.            
    73.          //生成JWT  
    74.         return builder.compact();  
    75.     }   
    76. }  




    [java] view plain copy
    1. package com.jay.filter;  
    2.   
    3. import java.io.IOException;  
    4.   
    5. import javax.servlet.Filter;  
    6. import javax.servlet.FilterChain;  
    7. import javax.servlet.FilterConfig;  
    8. import javax.servlet.ServletException;  
    9. import javax.servlet.ServletRequest;  
    10. import javax.servlet.ServletResponse;  
    11. import javax.servlet.http.HttpServletRequest;  
    12. import javax.servlet.http.HttpServletResponse;  
    13.   
    14. import org.springframework.beans.factory.annotation.Autowired;  
    15. import org.springframework.web.context.support.SpringBeanAutowiringSupport;  
    16.   
    17. import com.fasterxml.jackson.databind.ObjectMapper;  
    18. import com.jay.properties.JwtInfo;  
    19. import com.jay.util.jwt.JwtHelper;  
    20. import com.jay.vo.ResultMsg;  
    21. import com.jay.vo.ResultStatusCode;  
    22.   
    23. /* 
    24.  * 用于JWT认证的过滤器 
    25.  */  
    26. public class JwtAuthorizeFilter implements Filter{  
    27.       
    28.     /* 
    29.      * 注入配置文件类 
    30.      */  
    31.     @Autowired  
    32.     private JwtInfo jwtInfo;  
    33.   
    34.     @Override  
    35.     public void destroy() {  
    36.           
    37.     }  
    38.   
    39.     @Override  
    40.     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)  
    41.             throws IOException, ServletException {  
    42.          ResultMsg<Object> resultMsg;    
    43.             HttpServletRequest httpRequest = (HttpServletRequest)request;    
    44.             String auth = httpRequest.getHeader("Authorization");    
    45.             if ((auth != null) && (auth.length() > 7))    
    46.             {    
    47.                 String HeadStr = auth.substring(06).toLowerCase();    
    48.                 if (HeadStr.compareTo("bearer") == 0)    
    49.                 {    
    50.                         
    51.                     auth = auth.substring(7, auth.length());     
    52.                     if (JwtHelper.parseJWT(auth, jwtInfo.getBase64Secret()) != null)    
    53.                     {    
    54.                         chain.doFilter(request, response);    
    55.                         return;    
    56.                     }    
    57.                 }    
    58.             }    
    59.               
    60.             //验证不通过  
    61.             HttpServletResponse httpResponse = (HttpServletResponse) response;    
    62.             httpResponse.setCharacterEncoding("UTF-8");      
    63.             httpResponse.setContentType("application/json; charset=utf-8");     
    64.             httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);    
    65.         
    66.             //将验证不通过的错误返回  
    67.             ObjectMapper mapper = new ObjectMapper();    
    68.                 
    69.             resultMsg = new ResultMsg<Object>(true, ResultStatusCode.INVALID_TOKEN.getErrorCode(), ResultStatusCode.INVALID_TOKEN.getErrorMsg(), null);    
    70.             httpResponse.getWriter().write(mapper.writeValueAsString(resultMsg));    
    71.             return;    
    72.     }  
    73.   
    74.     @Override  
    75.     public void init(FilterConfig filterConfig) throws ServletException {  
    76.         SpringBeanAutowiringSupport.processInjectionBasedOnServletContext(this, filterConfig.getServletContext());  
    77.     }  
    78.   
    79. }  







                3.在Jwt配置类中,添加过滤器
                 
    [java] view plain copy
    1. package com.jay.config;  
    2.   
    3. import java.util.ArrayList;  
    4. import java.util.List;  
    5.   
    6. import org.springframework.boot.context.embedded.FilterRegistrationBean;  
    7. import org.springframework.context.annotation.Bean;  
    8. import org.springframework.context.annotation.Configuration;  
    9.   
    10. import com.jay.filter.JwtAuthorizeFilter;  
    11.   
    12.   
    13. /* 
    14.  * 注册jwt认证过滤器 
    15.  */  
    16. @Configuration  
    17. public class JwtConfig {  
    18.       
    19.     /* 
    20.      * 注册过滤器类和过滤的url 
    21.      */  
    22.     @Bean  
    23.     public FilterRegistrationBean basicFilterRegistrationBean(){  
    24.         FilterRegistrationBean registrationBean = new FilterRegistrationBean();  
    25.         JwtAuthorizeFilter filter = new JwtAuthorizeFilter();  
    26.         registrationBean.setFilter(filter);  
    27.           
    28.         List<String> urlPatterns = new ArrayList<>();  
    29.         urlPatterns.add("/user/*");  
    30.           
    31.         registrationBean.setUrlPatterns(urlPatterns);  
    32.         return registrationBean;  
    33.     }  
    34. }  

     
                
                4.效果展示:
                           1. 获取token,传入用户认证信息





                        2.使用上面获取的token进行接口调用,     未使用token,获取token错误,或者token过期时



                   3.使用正确的token时
                       

                        



    特别注意:
                  JWT使用时,可以通过Cookie机制,自动的传递!!!


    4.Redis + Cookie 机制,进行验证码的校验


    1.添加redis和captcha库依赖

    [html] view plain copy
    1. <!-- 整合redis -->  
    2.          <dependency>    
    3.             <groupId>org.springframework.boot</groupId>    
    4.             <artifactId>spring-boot-starter-redis</artifactId>    
    5.         </dependency>    
    6.         <!-- 第三方验证码库 -->  
    7.         <dependency>  
    8.             <groupId>cn.apiclub.tool</groupId>  
    9.             <artifactId>simplecaptcha</artifactId>  
    10.             <version>1.2.2</version>  
    11.         </dependency>  


    2.redis配置

    [html] view plain copy
    1. ##Redis配置  
    2. spring.redis.database=1  
    3. spring.redis.host=localhost  
    4. #spring.redis.password=password  
    5. spring.redis.port=6379  
    6. spring.redis.timeout=2000  
    7. spring.redis.pool.max-idle=8  
    8. spring.redis.pool.min-idle=0  
    9. spring.redis.pool.max-active=8  
    10. spring.redis.pool.max-wait=-1  


    3.Redis配置类,实例化Redis模板

    [java] view plain copy
    1. package com.jay.config;  
    2.   
    3. import org.springframework.context.annotation.Bean;  
    4. import org.springframework.context.annotation.Configuration;  
    5. import org.springframework.data.redis.connection.RedisConnectionFactory;  
    6. import org.springframework.data.redis.core.RedisTemplate;  
    7. import org.springframework.data.redis.core.StringRedisTemplate;  
    8. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;  
    9.   
    10. import com.fasterxml.jackson.annotation.JsonAutoDetect;  
    11. import com.fasterxml.jackson.annotation.PropertyAccessor;  
    12. import com.fasterxml.jackson.databind.ObjectMapper;  
    13.   
    14. @Configuration  
    15. public class RedisConfig {  
    16.       
    17.     // 定义Redis模板  
    18.     @Bean  
    19.     public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {  
    20.         StringRedisTemplate template = new StringRedisTemplate(factory);  
    21.         // 设置序列化工具, 这样缓存的Bean就不需要再试下Serializable接口  
    22.         setSerrializer(template);  
    23.         template.afterPropertiesSet();  
    24.         return template;  
    25.     }  
    26.   
    27.     // 设置序列化  
    28.     private void setSerrializer(StringRedisTemplate template) {  
    29.         Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);  
    30.         ObjectMapper om = new ObjectMapper();  
    31.         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);  
    32.         om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);  
    33.         jackson2JsonRedisSerializer.setObjectMapper(om);  
    34.         template.setValueSerializer(jackson2JsonRedisSerializer);  
    35.     }  
    36. }  


    4.Controller层操作代码

    [java] view plain copy
    1. package com.jay.controller;  
    2.   
    3. import java.io.ByteArrayOutputStream;  
    4. import java.io.IOException;  
    5. import java.util.UUID;  
    6. import java.util.concurrent.TimeUnit;  
    7.   
    8. import javax.imageio.ImageIO;  
    9. import javax.servlet.http.Cookie;  
    10. import javax.servlet.http.HttpServletRequest;  
    11. import javax.servlet.http.HttpServletResponse;  
    12.   
    13. import org.springframework.beans.factory.annotation.Autowired;  
    14. import org.springframework.data.redis.core.RedisTemplate;  
    15. import org.springframework.http.HttpRequest;  
    16. import org.springframework.http.MediaType;  
    17. import org.springframework.stereotype.Controller;  
    18. import org.springframework.web.bind.annotation.PathVariable;  
    19. import org.springframework.web.bind.annotation.RequestMapping;  
    20. import org.springframework.web.bind.annotation.RequestMethod;  
    21. import org.springframework.web.bind.annotation.ResponseBody;  
    22.   
    23. import com.jay.util.CookieUtils;  
    24. import com.jay.vo.ResultMsg;  
    25. import com.jay.vo.ResultStatusCode;  
    26.   
    27. import cn.apiclub.captcha.Captcha;  
    28. import cn.apiclub.captcha.backgrounds.GradiatedBackgroundProducer;  
    29. import cn.apiclub.captcha.gimpy.FishEyeGimpyRenderer;  
    30. import io.swagger.annotations.ApiOperation;  
    31.   
    32. @Controller  
    33. @RequestMapping("/redis")  
    34. public class RedisCaptchaController {  
    35.   
    36.     @Autowired  
    37.     private RedisTemplate<String, String> redisTemplate;  
    38.   
    39.     private static int captchaExpires = 3 * 60// 超时时间3min,验证码超时,自动冲redis中删除  
    40.     private static int captchaW = 200;  
    41.     private static int captchaH = 60;  
    42.     private static String cookieName = "CaptchaCode";  
    43.   
    44.     @RequestMapping(value = "getcaptcha", method = RequestMethod.GET, produces = MediaType.IMAGE_PNG_VALUE)  
    45.     public @ResponseBody byte[] getCaptcha(HttpServletResponse response) {  
    46.         // 生成验证码  
    47.         String uuid = UUID.randomUUID().toString();  
    48.         Captcha captcha = new Captcha.Builder(captchaW, captchaH).addText()  
    49.                 .addBackground(new GradiatedBackgroundProducer()).gimp(new FishEyeGimpyRenderer()).build();  
    50.   
    51.         // 将验证码以<key,value>形式缓存到redis  
    52.         redisTemplate.opsForValue().set(uuid, captcha.getAnswer(), captchaExpires, TimeUnit.SECONDS);  
    53.   
    54.         // 将验证码key,及验证码的图片返回  
    55.         Cookie cookie = new Cookie(cookieName, uuid);  
    56.         response.addCookie(cookie);  
    57.         ByteArrayOutputStream bao = new ByteArrayOutputStream();  
    58.         try {  
    59.             ImageIO.write(captcha.getImage(), "png", bao);  
    60.             return bao.toByteArray();  
    61.         } catch (IOException e) {  
    62.             return null;  
    63.         }  
    64.     }  
    65.       
    66.     /* 
    67.      * 说明: 
    68.      *    1.captchaCode来自客户端的Cookie,在访问时,通过服务端设置 
    69.      *    2.captcha是用户填写的验证码,将用户填写的验证码和通过captchaCode从redis中获取的验证码进行对比即可 
    70.      *  
    71.      */  
    72.     @ApiOperation(value = "验证码校验")  
    73.     @RequestMapping(value = "/captcha/check/{captcha}")  
    74.     @ResponseBody  
    75.     public ResultMsg<Object> checkCaptcha(@PathVariable("captcha") String captcha, HttpServletRequest request){  
    76.           
    77.         String captchaCode = CookieUtils.getCookie(request, cookieName);  
    78.           
    79.         ResultMsg<Object> result;  
    80.           
    81.         try{  
    82.         if (captcha == null)    
    83.         {    
    84.             throw new Exception();    
    85.         }    
    86.           
    87.         //redis中查询验证码  
    88.         String captchaValue = redisTemplate.opsForValue().get(captchaCode);  
    89.           
    90.         if (captchaValue == null) {  
    91.             throw new Exception();  
    92.         }  
    93.           
    94.         if (captchaValue.compareToIgnoreCase(captcha) != 0) {  
    95.             throw new Exception();  
    96.         }  
    97.           
    98.         //验证码匹配成功,redis则删除对应的验证码  
    99.         redisTemplate.delete(captchaCode);  
    100.                   
    101.         return new ResultMsg<Object>(true, ResultStatusCode.OK.getErrorCode(), ResultStatusCode.OK.getErrorMsg(), null);  
    102.   
    103.         }catch (Exception e) {  
    104.             result = new ResultMsg<Object>(false, ResultStatusCode.INVALID_CAPTCHA.getErrorCode(), ResultStatusCode.INVALID_CAPTCHA.getErrorMsg(), null);  
    105.         }  
    106.         return result;  
    107.     }  
    108.       
    109.   
    110. }  


    5.效果展示

    1.访问生成验证码



    2.验证验证码



    项目源码下载:下载地址


    阅读全文
    0 0