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

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

 

单点登录原理技术学习,更多知识请访问https://www.itkc8.com

 

 

 

 

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.projectlombokgroupId>  
  3.             <artifactId>lombokartifactId>  
  4.         dependency>  
  5.   
  6.           
  7.         <dependency>  
  8.             <groupId>org.springframework.bootgroupId>  
  9.             <artifactId>spring-boot-starter-data-jpaartifactId>  
  10.         dependency>  
  11.         <dependency>  
  12.             <groupId>mysqlgroupId>  
  13.             <artifactId>mysql-connector-javaartifactId>  
  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 urlPatterns = new ArrayList();    
  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(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.   
  2.         <dependency>  
  3.             <groupId>io.jsonwebtokengroupId>  
  4.             <artifactId>jjwtartifactId>  
  5.             <version>0.7.0version>  
  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 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(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. }  
  80.  

     

     

     

     

     

                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 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.   
    2.          <dependency>    
    3.             <groupId>org.springframework.bootgroupId>    
    4.             <artifactId>spring-boot-starter-redisartifactId>    
    5.         dependency>    
    6.           
    7.         <dependency>  
    8.             <groupId>cn.apiclub.toolgroupId>  
    9.             <artifactId>simplecaptchaartifactId>  
    10.             <version>1.2.2version>  
    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 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 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.         // 将验证码以形式缓存到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 checkCaptcha(@PathVariable("captcha") String captcha, HttpServletRequest request){  
    76.           
    77.         String captchaCode = CookieUtils.getCookie(request, cookieName);  
    78.           
    79.         ResultMsg 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(true, ResultStatusCode.OK.getErrorCode(), ResultStatusCode.OK.getErrorMsg(), null);  
    102.   
    103.         }catch (Exception e) {  
    104.             result = new ResultMsg(false, ResultStatusCode.INVALID_CAPTCHA.getErrorCode(), ResultStatusCode.INVALID_CAPTCHA.getErrorMsg(), null);  
    105.         }  
    106.         return result;  
    107.     }  
    108.       
    109.   
    110. }  
    111.  

      5.效果展示

       

      1.访问生成验证码

       

       

      2.验证验证码

       

       

      项目源码下载:下载地址

      单点登录原理技术学习,更多知识请访问https://www.itkc8.com

       

      你可能感兴趣的:(系统架构,用户系统)